首先,先来看一下这个模块的基本功能和原理。 HC-SR04超声波测距模块可提供2cm-400cm的非接触式距离感测功能,测距精度可达高到3mm;模块包括超声波发射器、接收器与控制电路。像智能小车的测距以及转向,或是一些项目中,常常会用到。智能小车测距可以及时发现前方的障碍物,使智能小车可以及时转向,避开障碍物。 注意是5v输入,但是我用stm32 的3.3v输入也是没有问题的。 6 T6 a" K- {$ j& z0 C
: Y6 K' w9 ^. G9 J7 N6 u
二.工作原理 1.给超声波模块接入电源和地。2 i' O: u7 s, {, D5 x0 Z7 N2 R0 ^
2.给脉冲触发引脚(trig)输入一个长为20us的高电平方波 3.输入方波后,模块会自动发射8个40KHz的声波,与此同时回波引脚(echo)端的电平会由0变为1;(此时应该启动定时器计时)0 E- o+ s* t( Z
4.当超声波返回被模块接收到时,回波引 脚端的电平会由1变为0;(此时应该停止定时器计数),定时器记下的这个时间即为超声波由发射到返回的总时长。
7 `. J9 Z4 r% d: e/ i- v4 k# I6 s 5.根据声音在空气中的速度为344米/秒,即可计算出所测的距离。 要学习和应用传感器,学会看懂传感器的时序图是很关键的,所以我们来看一下HC-SR04的时序触发图。 # o7 H& Y' K7 ~" J# w3 K
我们来分析一下这个时序图,先由触发信号启动HC-RS04测距模块,也就是说,主机要先发送至少10us的高电平,触发HC-RS04,模块内部发出信号是传感器自动回应的,我们不用去管它。输出回响信号是我们需要关注的。信号输出的高电平就是超声波发出到重新返回接收所用的时间。用定时器,可以把这段时间记录下来,算出距离,别忘了结果要除于2,因为总时间是发送和接收的时间总和。 下面是亲测可用的驱动程序。 芯片型号为stm32f103zet6,超声波测距后通过串口打印到电脑上面。 驱动和测距; - //超声波测距$ J5 v+ f: [6 u( E
- " ?4 }& j n! ^ t; @& Y
- #include "hcsr04.h"6 R& W7 _ ?. y4 F
-
+ m( F# [( }8 m/ A - #define HCSR04_PORT GPIOB
/ X8 b: R' ?& N6 s. }8 Y - #define HCSR04_CLK RCC_APB2Periph_GPIOB3 \# C( B8 E6 F
- #define HCSR04_TRIG GPIO_Pin_5, |% k6 @* v$ ?. J* g% G
- #define HCSR04_ECHO GPIO_Pin_6
9 X2 h% W$ A# |4 O, I8 [ - 1 ]# o" _9 a4 e* {
- #define TRIG_Send PBout(5)
! N# b# y, s% ~' f" `- G I0 i - #define ECHO_Reci PBin(6)# U; s# ~. _2 X) X8 v) Y3 |5 T; n# L
& T! {% V: W7 w9 z& b0 t& X7 {- u16 msHcCount = 0;//ms计数
# Q8 _7 `7 a% d
8 h$ y- n9 M) h* L: e9 H- void Hcsr04Init()' l5 ]' E9 n2 T; }# f
- {
6 }) v) x; F, O+ l: z7 C" I - TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //生成用于定时器设置的结构体; ^/ ^ b+ S9 x# H& G- b U4 g
- GPIO_InitTypeDef GPIO_InitStructure;
1 s7 M! ~$ Z% b; p% r+ _ - RCC_APB2PeriphClockCmd(HCSR04_CLK, ENABLE);4 w7 g! `' a) T7 y
-
% e5 v3 F0 c7 } - //IO初始化
* F4 w: `+ {$ f! n+ s$ Q6 C - GPIO_InitStructure.GPIO_Pin =HCSR04_TRIG; //发送电平引脚
! l0 G. U# }7 x$ G2 J$ v6 X - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;: I S. A6 k8 k
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出' X4 w5 r/ T: k3 v
- GPIO_Init(HCSR04_PORT, &GPIO_InitStructure);
2 v: a' p1 J0 x: }% w: y+ o( K$ L - GPIO_ResetBits(HCSR04_PORT,HCSR04_TRIG);2 Y4 L& ]9 k3 ~
- 6 i! Z- p# N: C
- GPIO_InitStructure.GPIO_Pin = HCSR04_ECHO; //返回电平引脚
g6 }6 f$ L5 K& ] - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
4 p7 z0 M/ N' N# V) Q; D2 | - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure); " }# b: r6 B# K+ c9 J& X
- GPIO_ResetBits(HCSR04_PORT,HCSR04_ECHO);
* a+ ]2 e, T1 }8 |8 C5 R6 e -
2 L8 q( h- X' E' i" ?3 D7 L; l3 m N - //定时器初始化 使用基本定时器TIM65 K& A2 f, f @% Z/ C3 G/ O: I0 E7 L
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //使能对应RCC时钟1 R8 A' H6 M& M2 I+ G6 k
- //配置定时器基础结构体6 _# P+ v! R- H% v
- TIM_DeInit(TIM2);, Q( C% l7 v2 ~+ T8 {
- TIM_TimeBaseStructure.TIM_Period = (1000-1); //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到1000为1ms
( P7 p$ Q$ o8 I! E0 C+ c - TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //设置用来作为TIMx时钟频率除数的预分频值 1M的计数频率 1US计数
, `( L4 ]+ w& P. C% L- a: `5 x - TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//不分频- |1 C6 c: T t. n# u1 [
- TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式6 Z/ \* _* ^6 t. C* L9 z
- TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位 ( a5 p4 i6 ?0 _) w. ^ ^- ]& V
- $ q/ q# ?2 E3 _ Z1 M/ _+ k" \6 U
- TIM_ClearFlag(TIM6, TIM_FLAG_Update); //清除更新中断,免得一打开中断立即产生中断
) b, z! @' G# N. a" D# K - TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); //打开定时器更新中断+ K& |. J' j) L5 \, V! H& ?
- hcsr04_NVIC();
% F, W3 |. S3 m0 f$ Y - TIM_Cmd(TIM6,DISABLE);
8 Z. h; ^, i+ m; R, ]. z - }
+ T8 e4 \- e3 h) R1 c1 ~) L" A: X - ) L2 h8 l8 x7 h
- 6 Z2 t: t! T( u# Y' D; F: N
- //tips:static函数的作用域仅限于定义它的源文件内,所以不需要在头文件里声明
* \4 A; [, s+ [, h* U7 t \' m - static void OpenTimerForHc() //打开定时器
) O3 S! Y3 X! s# J$ ~ - {
6 M' J3 t: F" p- g! g - TIM_SetCounter(TIM6,0);//清除计数9 P7 F5 F3 w$ _4 t( A" ?' x
- msHcCount = 0;
d4 d t( |$ y6 t8 ` - TIM_Cmd(TIM6, ENABLE); //使能TIMx外设
' _5 L( O& a9 W7 p$ r - }: m; g+ S% R7 H' R
- ; G" a8 D: U8 O% H2 b
- static void CloseTimerForHc() //关闭定时器- C4 H6 z5 Y& J/ i
- {
4 | q. O" Y% \ - TIM_Cmd(TIM6, DISABLE); //使能TIMx外设1 S+ }9 h$ s [+ ~8 I) \5 o. `
- }
- _% X. \% C# j$ B- T -
; f) c7 k b- t* x k - 1 {/ A5 `& E) C1 _4 t
- //NVIC配置 j; R- k3 G' ?% v# {6 @
- void hcsr04_NVIC()+ s: q* b' v8 @
- {2 A( v& m1 E3 u8 `( c# f$ s
- NVIC_InitTypeDef NVIC_InitStructure;8 l6 K( J1 P J6 C* t
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
7 {/ d" C0 q- O# S# B -
0 J+ u6 R& k8 e, N3 _; N - NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn; //选择串口1中断
# k% B. I2 f# p4 U - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占式中断优先级设置为1+ a* G$ B+ I4 F6 t* J. d* z: Z0 r
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应式中断优先级设置为1
$ X4 R' G' G3 p- U( I - NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断/ m: }& D. |/ t' h5 z
- NVIC_Init(&NVIC_InitStructure);2 T' l, K0 f! a) c" S
- }; N7 D6 u0 o' H5 ]
% R& H- C4 V: D
: f* C( l, n' s, `0 k% h- //定时器6中断服务程序
4 w5 P ?& `& P& M: L: o$ X& Q - void TIM6_IRQHandler(void) //TIM3中断
* z4 F! j0 T) y1 _2 y3 j - {
( _( S9 X+ l7 v - if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否! \2 _& K: ?. \/ `/ U2 P# v
- {
Y( R( i; j( N' o! m# `; X - TIM_ClearITPendingBit(TIM6, TIM_IT_Update ); //清除TIMx更新中断标志
" b: T& E: u3 r - msHcCount++;
. h; V( |8 K8 D" n. i+ g( g - }
9 q/ p& _4 q6 W, H0 H - }
2 H- a+ `' l$ l- q# x* J -
$ {4 X+ I3 a, T- b- @8 c1 s - . o' [1 E$ Q$ b8 _; \/ ^$ M
- //获取定时器时间8 g- G$ s6 e' l7 T" F% Y3 v
- u32 GetEchoTimer(void)
. x. q6 F& a% y: D, r - {3 ^, q0 @. R, J
- u32 t = 0;+ x5 C; A3 d, }, T8 M! O/ O
- t = msHcCount*1000;//得到MS
0 y9 Q1 Q Y! y) @; F. @9 [ - t += TIM_GetCounter(TIM6);//得到US
* V8 m6 c7 H+ h: B3 K% X6 }: [+ A - TIM6->CNT = 0; //将TIM2计数寄存器的计数值清零
$ `7 t% w2 {, S7 G& B1 d5 a - Delay_Ms(50);
" P4 a8 @+ a3 \7 n - return t;3 \$ ^0 f( V. Z/ a$ g% r1 |5 @: [" D
- }
2 ?9 @1 |/ X' F7 I' ~, c - i; I5 P) R* O3 f3 X* T
. q; `3 _/ B; c- //一次获取超声波测距数据 两次测距之间需要相隔一段时间,隔断回响信号1 [6 X9 ~7 ?8 G6 |
- //为了消除余震的影响,取五次数据的平均值进行加权滤波。" G5 y# K0 I$ @8 |+ \9 o3 r0 h
- float Hcsr04GetLength(void )
! h0 e. Y5 L& v! ^; m# G% Y" l - {( e/ c( `( K( @' ]# G* K
- u32 t = 0;4 T1 r% s0 t6 a7 {- Q5 S
- int i = 0;/ i' @4 b( J0 J( ~. ~8 U
- float lengthTemp = 0;
6 g" E; @: d& z - float sum = 0;9 o) t8 s+ s: B" \
- while(i!=5)* O5 _# }8 l, X* p9 J
- {
( d8 ^' H3 E C8 j - TRIG_Send = 1; //发送口高电平输出: q, h& F* E% h+ K
- Delay_Us(20);
8 P/ \, O; u6 s+ W+ l5 T4 T: ~2 `. e - TRIG_Send = 0;: C" \5 S% C! l
- while(ECHO_Reci == 0); //等待接收口高电平输出
7 E9 i6 r9 Z' A: v% z- @" q7 _ - OpenTimerForHc(); //打开定时器% n2 C7 ^) s# t9 D! t9 w
- i = i + 1;1 R0 ]6 t3 x+ p+ _2 B8 A( \) d5 Y) Q
- while(ECHO_Reci == 1);
' e, r4 b7 C- S/ K) S: K N - CloseTimerForHc(); //关闭定时器
% {# s6 O$ K, l [% e0 o - t = GetEchoTimer(); //获取时间,分辨率为1US% j! b5 b, V$ y: s; Z' ^
- lengthTemp = ((float)t/58.0);//cm; I$ e0 z; M0 [( n
- sum = lengthTemp + sum ;: t- v8 `9 K( q5 V9 y% f* F' W
-
0 e& a: L9 p q" b f- J, c# v - }6 o+ a9 F( C/ U; q6 a
- lengthTemp = sum/5.0;
$ h" y. c" c L( G0 B ^ - return lengthTemp;* l S: T3 @7 y; T- M6 _
- }4 Z2 K% A5 _5 F$ ^4 P7 N5 f" Z O
$ g8 B8 n" {. J
- M( F! x) O& U X- /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
. j5 Y2 ?0 _( Z" q* }5 N8 V* { - ** 函数名称: Delay_Ms_Ms3 a; Z5 @) E, ~: r# U: g9 A( |
- ** 功能描述: 延时1MS (可通过仿真来判断他的准确度)
6 e, a; U) z+ c) Q4 q( r# } - ** 参数描述:time (ms) 注意time<65535% q c% S% I" ?9 E9 C: E
- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/- ~+ ~0 O. [3 K, r4 m/ K
- void Delay_Ms(uint16_t time) //延时函数
3 M1 r( b9 {( u8 i0 c2 t2 @ - {
) x9 Z+ w; V V$ F - uint16_t i,j;& J, g( f5 A9 \6 k% |
- for(i=0;i<time;i++)
$ Z4 x' z4 U- G. z5 W, L - for(j=0;j<10260;j++);2 {! B9 e7 M' n& v1 v+ S: _( W$ N* ]
- }& y8 j: p1 [- h
- /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::, B$ L0 f, s" J) \2 V! y
- ** 函数名称: Delay_Ms_Us
+ x1 q. g$ ?; T- U. R% {$ ] - ** 功能描述: 延时1us (可通过仿真来判断他的准确度)
' c) L+ n6 X. G( G - ** 参数描述:time (us) 注意time<65535
0 |9 G- e. a- [* h) @& M - :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
* H* e( A3 h! Z1 s1 Y - void Delay_Us(uint16_t time) //延时函数$ z" j0 E3 G* Q! ]. ^% [. _' F& p5 I
- {
% r2 s! g3 m( ]3 A H: h, g - uint16_t i,j;4 T* r8 `: v5 A6 }. ]9 s% V/ W
- for(i=0;i<time;i++)* ]4 A! p' E& c
- for(j=0;j<9;j++);
1 h8 n1 C- E( A" k2 U$ ] - }
复制代码
( s' y3 T' G! u但是关于USART的函数我就不往上写了,这个简单的串口打印大家应该都会写。下面简单贴一下我的主函数吧。 - /*
: ~3 z6 y* ]' r l0 w+ d$ G" \/ M - 教训:实验前一定要检查引脚连接是否正确,万不可搞错,不然又要烧坏芯片!!!!6 L/ y& B3 D, p0 L2 v
- & d- @/ y# S/ X0 J* d9 i
- */+ X) e# Y7 m+ j& {
, X/ P5 s R; i* v n- W- #include "hcsr04.h"% z) {# ~" [/ m ?1 r- i1 }
- #include "chao_usart.h"
+ T! m. N1 t) L. ^4 ^2 x# ~ - / F& i' O+ E3 k4 k! x
- int main()
/ r$ Z9 \- W0 {$ M3 p - {
$ [! c' V# E2 F -
, ^! ]3 P+ x3 w+ f - float length;
( D/ \) p% |/ g+ U9 g -
K1 ~: K3 J& h2 S5 Z - GPIO_cfg();0 {, o: O' Z+ T9 R; I8 D" C
- NVIC_cfg();
2 P4 q# k8 y7 K4 e3 ? - USART_cfg(); 4 k' Q7 e/ _; N
- printf("串口初始化成功!\n");" |3 b" q0 a5 @% t3 W
- 3 f* ?! h! x) M% [& F2 L% ^
- Hcsr04Init();
# f4 @3 p" _7 q* z$ n - printf("超声波初始化成功!\n");//测试程序是否卡在下面两句上面
) U9 o8 f1 v; M3 z8 K' E0 Q
" f4 ^3 R/ U* X+ B5 m- length = Hcsr04GetLength();
2 l5 \; }2 ?: M# x; J* \ - printf("距离为:%.3f\n",length);! Y8 b- \* j% D8 ~1 ~/ Y
- / }8 g. o1 E; V0 L1 g. i) f
-
! ]# Y9 U2 j7 d8 e9 `! {. ?: E+ ~ - }
复制代码 $ ] p# B" L& X( j4 f7 I3 M- |6 v
7 o) j) P7 N5 X( G v) T) f
实验结果: ! \ y, i, v" N! @* z
) c2 x1 M1 o: D. `
& o5 q: \, i1 v8 Z% i# W% v: P
好了,其实这个模块很简单,但是要是把他用的很好的话还是比较困难的,比如用超声波做一个四轴定高的程序,还是有一定的挑战性的。 写这篇博客的目的不仅仅是介绍这个模块的使用,其实这种使用介绍网上一搜一大把,我只是想纪录一下,我在做这个模块的时候遇到的一些其他的问题。 其中有一个小插曲,就是当吧写好的程序烧进去之后,运行时总是出现每次返回一个同样的比正常值小的多的数据,比如说0.034cm,这明显是一个错误的数据,但是刚开始的时候,不知道为什么 总是这样,多次复位从新上电总是这一个数据。让我很是苦恼。但是幸运的是,在这样的情况中间,他又会有时出现一两个正常的的数据,让你有点摸不着头脑。 上网查了一下才慢慢明白,这种现象叫做“余震”,网上关于余震的解释大致有三种: 1、探头的余震。即使是分体式的,发射头工作完后还会继续震一会,这是物理效应,也就是余震。这个余震信号也会向外传播。如果你的设计是发射完毕后立刻切换为接收状态(无盲区),那么这个余震波会通过壳体和周围的空气,直接到达接收头、干扰了检测(注:通常的测距设计里,发射头和接收头的距离很近,在这么短的距离里超声波的检测角度是很大的,可达180度)。% Z, O: M7 B) D' c! D0 Z8 ?( |. J. N4 i
2、壳体的余震。就像敲钟一样,能量仍来自发射头。发射结束后,壳体的余震会直接传导到接收头,当然这个时间很短,但已形成了干扰。另外,在不同的环境温度下,壳体的硬度和外形会有所变化,其余震有时长、有时短、有时干扰大、有时干扰小,这是设计工业级产品时必须要考虑的问题。7 s. F0 I. `# F) A/ S+ h8 |
3、电路串扰。超声波发射时的瞬间电流很大,例如某种工业级连续测距产品瞬间电流会有15A,通常的产品也能达到1A,瞬间这么大的电流会对电源有一定影响,并干扰接收电路。通过改善电源设计可以缓解这种情况,但在低成本设计中很难根除。所以每次发射完毕,接收电路还需要一段时间稳定工作状态。在此期间,其输出的信号很难使用。 . X3 O: j" E! G, h2 j' I* C
消除上述现象的方法之一就是在检测的时候多次循环检测,取平均值,也就是加权平均滤波,一个简单的滤波处理。就是下面这一段: - int i = 0;' G" Z' o/ V s1 K. r
- float lengthTemp = 0;$ e6 @: _+ v, O3 C
- float sum = 0;; @( n% Q- c d+ ?
- while(i!=5)
# P1 e/ e$ ]- }+ C1 p* X( u - {6 @* x4 J9 Q, O: h
- TRIG_Send = 1; //发送口高电平输出: b: h% N k. T5 ^2 J: r
- Delay_Us(20);
- `3 f r F* H0 h! o! C& E - TRIG_Send = 0;
" t9 D% A, J; n* V2 E/ p - while(ECHO_Reci == 0); //等待接收口高电平输出! M9 ]. E# r+ G5 s: h7 c
- OpenTimerForHc(); //打开定时器5 C5 {4 q' h0 ^9 I. |/ F
- i = i + 1;, K4 W# ^, I# V/ d7 e
- while(ECHO_Reci == 1);
4 {. ?% \3 n6 H* K - CloseTimerForHc(); //关闭定时器
7 D! K$ j# _1 @0 V' m, ] - t = GetEchoTimer(); //获取时间,分辨率为1US
3 {: y" k* L; e7 c7 ] - lengthTemp = ((float)t/58.0);//cm( {' U& b, f& W+ |( s" E
- sum = lengthTemp + sum ;
( R7 W d; g$ n) ^. m" Y - , [/ v& L- F# U! X! D, a
- }3 _1 `2 X- S/ G) M+ V
- lengthTemp = sum/5.0;
! z* P7 s3 A2 D Y7 G$ ] - return lengthTemp;
复制代码
) s3 s& V! `" h
, p7 w: a7 w' R; \% @- Z加了这个之后,基本上就没有出现余震现象了。 还有一点就是测试程序前一定要检查引脚有没有接错,不管多有把握,也要看一遍,不然很容易出大事的,一个芯片也许就因为你的大意给GG了。切记,这个应该也算我们这个行业的基本素养吧。
3 r* C- U7 N: [/ w6 w: v3 t |