|
首先,先来看一下这个模块的基本功能和原理。 HC-SR04超声波测距模块可提供2cm-400cm的非接触式距离感测功能,测距精度可达高到3mm;模块包括超声波发射器、接收器与控制电路。像智能小车的测距以及转向,或是一些项目中,常常会用到。智能小车测距可以及时发现前方的障碍物,使智能小车可以及时转向,避开障碍物。 注意是5v输入,但是我用stm32 的3.3v输入也是没有问题的。 ( E$ \, M: e# g$ X! |0 T, n) k
+ _3 Q7 x. p! H5 W' c! b) T
二.工作原理 1.给超声波模块接入电源和地。 U* f' s9 @8 _
2.给脉冲触发引脚(trig)输入一个长为20us的高电平方波 3.输入方波后,模块会自动发射8个40KHz的声波,与此同时回波引脚(echo)端的电平会由0变为1;(此时应该启动定时器计时)
( x0 q! v5 D$ S 4.当超声波返回被模块接收到时,回波引 脚端的电平会由1变为0;(此时应该停止定时器计数),定时器记下的这个时间即为超声波由发射到返回的总时长。
1 z& g9 p f( k8 e: c 5.根据声音在空气中的速度为344米/秒,即可计算出所测的距离。 要学习和应用传感器,学会看懂传感器的时序图是很关键的,所以我们来看一下HC-SR04的时序触发图。
* e: @! h2 }; U% H 我们来分析一下这个时序图,先由触发信号启动HC-RS04测距模块,也就是说,主机要先发送至少10us的高电平,触发HC-RS04,模块内部发出信号是传感器自动回应的,我们不用去管它。输出回响信号是我们需要关注的。信号输出的高电平就是超声波发出到重新返回接收所用的时间。用定时器,可以把这段时间记录下来,算出距离,别忘了结果要除于2,因为总时间是发送和接收的时间总和。 下面是亲测可用的驱动程序。 芯片型号为stm32f103zet6,超声波测距后通过串口打印到电脑上面。 驱动和测距; - //超声波测距* B* Q) K& p& p7 r) Y
- ( n3 E" F ^' _ T$ u/ g
- #include "hcsr04.h"; }* t4 m; J& @8 s! K: o; ]
- 9 D/ g. u$ I( n: F% s( F* c
- #define HCSR04_PORT GPIOB
* V. |) N# x4 Y' @1 t3 _% W - #define HCSR04_CLK RCC_APB2Periph_GPIOB
8 w+ a( e5 q! z9 d) Y5 z6 O - #define HCSR04_TRIG GPIO_Pin_5
1 ~' v) j% `5 j7 C1 W - #define HCSR04_ECHO GPIO_Pin_6
, R8 K9 G/ G( u( i5 n4 E! e- e& | - ( F r9 e! N& q+ |' x
- #define TRIG_Send PBout(5)
5 H; F( @% j+ z9 w! c - #define ECHO_Reci PBin(6)- b( Z5 J# ~5 {& ]/ z
- ) n: B) g! P" \; f4 g8 m3 ~
- u16 msHcCount = 0;//ms计数# V0 P9 }0 C1 Z
- ) ~, a" H/ ]! G6 {5 ^% e
- void Hcsr04Init()5 X6 l$ S) M& `
- { : O4 Y3 F, B( k, P k% n: d, F
- TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //生成用于定时器设置的结构体
$ }$ O# l$ o4 F/ s @: Q' m - GPIO_InitTypeDef GPIO_InitStructure;3 t$ H/ n0 B" |; H5 Q; p6 N
- RCC_APB2PeriphClockCmd(HCSR04_CLK, ENABLE);% k% E* @$ k9 e0 @6 e
-
, m% _: m* s5 T - //IO初始化
1 Z% g8 \. i! I5 d8 F - GPIO_InitStructure.GPIO_Pin =HCSR04_TRIG; //发送电平引脚$ f. ?( Z# H$ h6 R) y. N; ?% F
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;9 g1 c2 N* @6 T$ y8 a
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出% Q0 c' H, c; S5 h0 M
- GPIO_Init(HCSR04_PORT, &GPIO_InitStructure);
! X; X0 K# f* P4 q- T( [3 l8 g - GPIO_ResetBits(HCSR04_PORT,HCSR04_TRIG);
7 o4 k; g; U1 x - , W: b1 b( F5 O L
- GPIO_InitStructure.GPIO_Pin = HCSR04_ECHO; //返回电平引脚2 H' c% S2 H6 ~# \: P5 ?
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
2 L/ A6 G8 c& C: g - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure); + w# e( w+ v4 c
- GPIO_ResetBits(HCSR04_PORT,HCSR04_ECHO);
' t0 v# u+ Z. b- n7 k1 w0 W7 z* L+ L - R( ~$ T# I8 o( Z) x3 i/ B! V0 |( u/ J
- //定时器初始化 使用基本定时器TIM6
8 A1 {, K' ~) v6 b* b( N - RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //使能对应RCC时钟) Q8 n. H& b4 M+ w
- //配置定时器基础结构体4 {; L/ q6 Y, N& M$ B/ x# H
- TIM_DeInit(TIM2); Z* ~3 ]" ^6 r: u5 Z- M$ ?. v' K
- TIM_TimeBaseStructure.TIM_Period = (1000-1); //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到1000为1ms
$ ?; J% s7 x( H3 I Z5 q I$ d - TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //设置用来作为TIMx时钟频率除数的预分频值 1M的计数频率 1US计数
4 C# m, p9 j) R' {6 ] - TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//不分频3 O! u9 v7 }; }, z+ n% \$ h
- TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
R. Z# P$ T! o* Y& m0 k - TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
5 _. O$ s* K/ \+ O, R- c( D -
* h; S/ `$ H9 h& s; c - TIM_ClearFlag(TIM6, TIM_FLAG_Update); //清除更新中断,免得一打开中断立即产生中断1 N" V. S' J: v
- TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); //打开定时器更新中断
% E% J' S( v4 ?+ ^' I/ y& D - hcsr04_NVIC();
$ X/ y6 X1 R/ F6 T- G \ - TIM_Cmd(TIM6,DISABLE);
4 o+ @( U3 o4 T% S: f0 _ - }% ~7 E% B$ y0 p# g }9 }9 L7 ~
- " T4 c% ` A6 _& {# {8 B8 W. }7 m
8 V6 r+ F% o) {* P) q6 A- //tips:static函数的作用域仅限于定义它的源文件内,所以不需要在头文件里声明$ s" j" A5 C) G) M! S- r
- static void OpenTimerForHc() //打开定时器
: u, ?6 Q/ @" U: x0 p8 v0 { - {2 W) g# r* G* @+ D
- TIM_SetCounter(TIM6,0);//清除计数
4 l4 E+ V2 h) H' K! S - msHcCount = 0;
8 w6 Q7 q$ v/ M6 X - TIM_Cmd(TIM6, ENABLE); //使能TIMx外设
5 K. B! X+ F: l+ ~( F - }
x8 c( {& ~$ { -
: L" A( ?$ d4 R$ {. D - static void CloseTimerForHc() //关闭定时器, I& \* {4 F! A* ~( L4 x* p' C3 O
- {
9 s# }3 `( ^& m: g$ D3 `) p! N% q - TIM_Cmd(TIM6, DISABLE); //使能TIMx外设
" N! j( J" G& m+ h' D* g - }& G. ~7 l0 z' z; H+ p3 Z" d
- . f" Y$ t" C- q* V# j! M
-
* P+ x# }* G/ \6 b# r6 P5 k- T - //NVIC配置" C( r3 Y0 t3 i3 o& `6 Q% _
- void hcsr04_NVIC()$ Z3 C3 @5 r2 v' v
- {
* _ |8 ?8 j) Y' s - NVIC_InitTypeDef NVIC_InitStructure;. V- I0 A2 P4 N1 A: R) y+ M% I
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);- I0 t5 X0 D+ e6 c
-
4 y- N. w2 r- w p - NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn; //选择串口1中断# h! A( ~. p: Z6 C
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占式中断优先级设置为1
! U" F1 b. ^6 L+ t7 @ - NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应式中断优先级设置为10 M( t+ J! E. u/ e5 x1 o
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断
1 p. r4 O: z# g* `( k' P0 {1 R - NVIC_Init(&NVIC_InitStructure);' b" ~' i0 [) F" n* @ z* V/ {
- }
* |1 k9 K2 t3 }' l# m4 X
( X2 p+ f; O, p' A
! J. U% W% a3 s6 x- e- ^- //定时器6中断服务程序2 c, ]0 p- X' v
- void TIM6_IRQHandler(void) //TIM3中断6 |6 r+ ]# s- v9 o8 g& Q/ R: P
- {: n% z! |$ t3 b3 h. ?3 Q
- if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否) g' \/ l" X% g6 a. Y
- {
6 M, _/ k% ~& J% L) E# K! P( E - TIM_ClearITPendingBit(TIM6, TIM_IT_Update ); //清除TIMx更新中断标志
* L- c, L6 o# p4 h/ b5 Z - msHcCount++;; q* J/ i8 @8 L5 A; }( n; c7 a
- }/ u6 q& ?0 T" b. l
- }
$ d3 @# n% l3 ~# }2 ~5 Q* V1 D" ^ - p5 P! }5 Y' I; w
- . p/ n' ?, A. h% E
- //获取定时器时间7 E8 N( H' `! E( ~ F
- u32 GetEchoTimer(void)
- u0 [0 Q( ~2 A/ U( F. ] - {
) V0 O" O7 T9 [. [ - u32 t = 0;
* A" U1 `8 k6 w7 d& i9 X; B4 H& v - t = msHcCount*1000;//得到MS: Q j+ R- U/ u
- t += TIM_GetCounter(TIM6);//得到US/ p& |3 g$ ], V+ n( {5 @9 q1 H
- TIM6->CNT = 0; //将TIM2计数寄存器的计数值清零8 Q. t$ k4 X! j f% N* X6 P
- Delay_Ms(50);
2 n% _3 m, E" O - return t;7 v: y7 \- U- `: A
- }. W' ^# Q8 q9 |# m& n: {
-
) e- w5 _# C M5 i
5 n6 K; G9 @. G% J5 r+ E. g% a, u- //一次获取超声波测距数据 两次测距之间需要相隔一段时间,隔断回响信号
5 \- ^7 x: C! Q - //为了消除余震的影响,取五次数据的平均值进行加权滤波。
" }- W1 Y4 w" E5 i0 v8 b0 r - float Hcsr04GetLength(void )
. \2 g9 k f) z% T* b R - {. j9 b& i( Y( w8 r( g
- u32 t = 0;
! a& m) Y) S. D1 ~ - int i = 0;
; f) Z" b5 X! G3 V3 p/ o - float lengthTemp = 0;
8 n a& \" e5 M% ? J- Y - float sum = 0;
; z* x) i+ a+ |+ N) \6 M - while(i!=5)
% B, @: a' W" P( Y - {
' d3 m! ^ w, m - TRIG_Send = 1; //发送口高电平输出3 }" r) C! _9 [2 L( A, ^2 z
- Delay_Us(20);3 q8 _' c$ p# M% |; W Q* {7 C+ J
- TRIG_Send = 0;
( W! B# l3 Z. q) C9 _& D - while(ECHO_Reci == 0); //等待接收口高电平输出, v1 {: ?/ e6 N4 _3 m+ a. \4 `5 Y
- OpenTimerForHc(); //打开定时器
1 s: E7 s& D2 a" J/ G. z - i = i + 1;0 w$ S5 e# T, S8 |$ F
- while(ECHO_Reci == 1);) l/ J8 O8 p& S) R9 a: d# z
- CloseTimerForHc(); //关闭定时器7 }, ?' s2 h( R+ T" s
- t = GetEchoTimer(); //获取时间,分辨率为1US
6 O2 r8 O1 R) C4 V1 Q - lengthTemp = ((float)t/58.0);//cm
: i8 J" r$ M) I$ o! ] w( N- i - sum = lengthTemp + sum ;6 K) M( K5 ~+ t4 [' C# Y; P
-
: r, X u% ?! g - }) v: U) q5 X/ y& j3 b7 i; s
- lengthTemp = sum/5.0;
, E1 V9 _$ @* X5 f y - return lengthTemp;
2 a3 {! p8 r9 z1 ~. s% m" h6 } - }+ s) c7 A+ U! ]3 X
4 V. X" O% M* ?1 ~3 y
7 v% M8 Z8 I+ ~/ t- /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::8 {" a! ^* D, [6 z: I8 Z9 y' [7 h( f
- ** 函数名称: Delay_Ms_Ms
" D$ D8 j% l1 F4 o: w3 T - ** 功能描述: 延时1MS (可通过仿真来判断他的准确度)
! X0 K( B: @$ f; q0 p' N4 A - ** 参数描述:time (ms) 注意time<65535# m! s+ t( ?/ s" D( o3 A6 Z0 o* Z% x
- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/6 n5 U* Y0 c8 T' \
- void Delay_Ms(uint16_t time) //延时函数
) y) k9 C; r: U, i/ V' P* p - {
$ L# z$ l4 X. s, @2 f, _ - uint16_t i,j;
* ]1 v {7 A3 P6 f - for(i=0;i<time;i++)7 d" y0 q; _$ u# U. a, A, ^
- for(j=0;j<10260;j++);% [% k4 P$ Z v. a8 q. ]; }6 e
- }
, O; R$ _$ d% i: t4 Z. j1 [ - /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# y7 o/ U& w. u! F/ ], [ - ** 函数名称: Delay_Ms_Us% `1 C) {. }; `" ^& P* W8 l
- ** 功能描述: 延时1us (可通过仿真来判断他的准确度)
( ~9 n8 X( \4 v9 J2 E - ** 参数描述:time (us) 注意time<65535
# D4 ~, L. B6 j - :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
+ {0 Z1 e; e N- n) ` - void Delay_Us(uint16_t time) //延时函数
% r d$ q0 H: P# j - {
6 X G+ K1 t' v" ~ - uint16_t i,j;
1 I5 e: H3 G2 A0 _% ?, @ - for(i=0;i<time;i++)
6 X) L3 L" R u# g9 w" _ - for(j=0;j<9;j++);2 w9 j7 l; \- K- ^: k8 t
- }
复制代码 ; ?+ b: D F ?! N: H( @' q
但是关于USART的函数我就不往上写了,这个简单的串口打印大家应该都会写。下面简单贴一下我的主函数吧。 - /*
. Q( ]5 e. E- v- i$ ? - 教训:实验前一定要检查引脚连接是否正确,万不可搞错,不然又要烧坏芯片!!!!
1 Q, `9 Q- K1 @
5 r- u+ N) p1 U! G- */
/ g; R8 r0 h" L) Q. M/ Z
; B: {' A6 q7 O R ]0 z' }- #include "hcsr04.h"
. W+ Z+ L' O9 h# ^- ^" p1 b - #include "chao_usart.h", ~8 z- L. `& ?5 s9 y& g5 T# Z- W
- / y% d+ `3 l, O6 @( @
- int main()5 S: ^1 r1 j* I- n6 S% T2 G- q
- {/ q e3 B, F* T4 q0 w9 f
-
$ Z2 F. C w% o3 Q - float length;% S+ V; w: R* P6 ], m
-
5 F8 u0 |3 e1 v: g+ X - GPIO_cfg();. T- H8 K1 q! O/ t' m3 J
- NVIC_cfg();& L; S1 Z( k9 ~
- USART_cfg();
; z Z% y$ h0 P+ ^* C4 B7 j" { - printf("串口初始化成功!\n");' n5 G$ {: }8 a1 M1 C8 s* c/ Y
-
& `3 p7 |2 N2 E I6 k- `: Y - Hcsr04Init();
8 ]5 w! D2 S) O! r - printf("超声波初始化成功!\n");//测试程序是否卡在下面两句上面
' X% t; L3 { S& W2 O - 2 K' Q5 Q; w e! w( J
- length = Hcsr04GetLength();
$ t5 z) h/ D3 u0 A1 M$ x - printf("距离为:%.3f\n",length);% s+ M( z. P8 \) F+ f
-
3 C. V }' c* x2 i. G9 l -
6 p* r5 e' H3 B4 w - }
复制代码
& W1 c* u4 ^9 Y+ b- z4 R* V4 l, W2 w* C" U
实验结果: 3 Z1 w& [ B5 H4 I# V) \7 y; S
, f' M. x$ V+ n0 m1 m7 `- E
! ]. R) M. G* }. l# O) i好了,其实这个模块很简单,但是要是把他用的很好的话还是比较困难的,比如用超声波做一个四轴定高的程序,还是有一定的挑战性的。 写这篇博客的目的不仅仅是介绍这个模块的使用,其实这种使用介绍网上一搜一大把,我只是想纪录一下,我在做这个模块的时候遇到的一些其他的问题。 其中有一个小插曲,就是当吧写好的程序烧进去之后,运行时总是出现每次返回一个同样的比正常值小的多的数据,比如说0.034cm,这明显是一个错误的数据,但是刚开始的时候,不知道为什么 总是这样,多次复位从新上电总是这一个数据。让我很是苦恼。但是幸运的是,在这样的情况中间,他又会有时出现一两个正常的的数据,让你有点摸不着头脑。 上网查了一下才慢慢明白,这种现象叫做“余震”,网上关于余震的解释大致有三种: 1、探头的余震。即使是分体式的,发射头工作完后还会继续震一会,这是物理效应,也就是余震。这个余震信号也会向外传播。如果你的设计是发射完毕后立刻切换为接收状态(无盲区),那么这个余震波会通过壳体和周围的空气,直接到达接收头、干扰了检测(注:通常的测距设计里,发射头和接收头的距离很近,在这么短的距离里超声波的检测角度是很大的,可达180度)。
, ?( S/ ]7 r1 K: \" D 2、壳体的余震。就像敲钟一样,能量仍来自发射头。发射结束后,壳体的余震会直接传导到接收头,当然这个时间很短,但已形成了干扰。另外,在不同的环境温度下,壳体的硬度和外形会有所变化,其余震有时长、有时短、有时干扰大、有时干扰小,这是设计工业级产品时必须要考虑的问题。
D1 R' K+ g0 Q2 a; O! e1 B, p 3、电路串扰。超声波发射时的瞬间电流很大,例如某种工业级连续测距产品瞬间电流会有15A,通常的产品也能达到1A,瞬间这么大的电流会对电源有一定影响,并干扰接收电路。通过改善电源设计可以缓解这种情况,但在低成本设计中很难根除。所以每次发射完毕,接收电路还需要一段时间稳定工作状态。在此期间,其输出的信号很难使用。 9 H+ g. \" f3 _7 g( E, L- v$ X
消除上述现象的方法之一就是在检测的时候多次循环检测,取平均值,也就是加权平均滤波,一个简单的滤波处理。就是下面这一段: - int i = 0;
/ G u7 q2 @0 J; O& _; P8 q - float lengthTemp = 0;
+ N9 @" i# Q% A7 N# n0 w2 q% [1 R - float sum = 0;9 h U* X5 b5 d
- while(i!=5)
3 @! z$ c6 w) J( t; [# e' p - {
/ X, X8 V* M) Y0 e8 f - TRIG_Send = 1; //发送口高电平输出
0 M8 {0 ?2 [# i% n6 l+ M - Delay_Us(20);) k( g' A/ M d) S% i+ [ j3 p
- TRIG_Send = 0;
$ [3 H7 L& L: ?% S/ ^ - while(ECHO_Reci == 0); //等待接收口高电平输出9 r8 [2 ~: d8 e% w, {
- OpenTimerForHc(); //打开定时器
f' X5 C8 O& A2 g: x8 p+ |8 t, Q - i = i + 1;
' O- y5 b& l% M9 w! ]; N( v- H - while(ECHO_Reci == 1);1 i# @1 ?, d% B0 T2 V/ K3 z4 O
- CloseTimerForHc(); //关闭定时器
" V- A/ E7 K" I0 t - t = GetEchoTimer(); //获取时间,分辨率为1US! t4 c9 ?* D4 [8 t# L* G, k
- lengthTemp = ((float)t/58.0);//cm
0 Q# {8 `' l5 M+ Q# v4 \ - sum = lengthTemp + sum ;
- E; m4 I3 x3 k$ C3 L - 0 q& t$ O8 `5 r! X/ D
- }, C5 p7 T, `6 E+ C) m: K8 L- v
- lengthTemp = sum/5.0;: P+ H: d7 X5 G/ r, |
- return lengthTemp;
复制代码 3 C; X% E. R$ F) m8 e
3 D' E, t: ]9 W" b% k
加了这个之后,基本上就没有出现余震现象了。 还有一点就是测试程序前一定要检查引脚有没有接错,不管多有把握,也要看一遍,不然很容易出大事的,一个芯片也许就因为你的大意给GG了。切记,这个应该也算我们这个行业的基本素养吧。 9 W5 @9 x+ B ~6 H
|