首先,先来看一下这个模块的基本功能和原理。 HC-SR04超声波测距模块可提供2cm-400cm的非接触式距离感测功能,测距精度可达高到3mm;模块包括超声波发射器、接收器与控制电路。像智能小车的测距以及转向,或是一些项目中,常常会用到。智能小车测距可以及时发现前方的障碍物,使智能小车可以及时转向,避开障碍物。 注意是5v输入,但是我用stm32 的3.3v输入也是没有问题的。 0 C8 _$ b% @) q, ^1 W* T4 j( R
5 F: j" F8 r$ D) _# `8 w9 M
二.工作原理 1.给超声波模块接入电源和地。( w0 N, U5 i( J( s/ A% P3 A" f- b
2.给脉冲触发引脚(trig)输入一个长为20us的高电平方波 3.输入方波后,模块会自动发射8个40KHz的声波,与此同时回波引脚(echo)端的电平会由0变为1;(此时应该启动定时器计时)
) y+ G7 L! H; H+ ` E" i 4.当超声波返回被模块接收到时,回波引 脚端的电平会由1变为0;(此时应该停止定时器计数),定时器记下的这个时间即为超声波由发射到返回的总时长。
) J5 ]+ E, P: t 5.根据声音在空气中的速度为344米/秒,即可计算出所测的距离。 要学习和应用传感器,学会看懂传感器的时序图是很关键的,所以我们来看一下HC-SR04的时序触发图。 @+ j" _% M; J. P( d+ t5 r
我们来分析一下这个时序图,先由触发信号启动HC-RS04测距模块,也就是说,主机要先发送至少10us的高电平,触发HC-RS04,模块内部发出信号是传感器自动回应的,我们不用去管它。输出回响信号是我们需要关注的。信号输出的高电平就是超声波发出到重新返回接收所用的时间。用定时器,可以把这段时间记录下来,算出距离,别忘了结果要除于2,因为总时间是发送和接收的时间总和。 下面是亲测可用的驱动程序。 芯片型号为stm32f103zet6,超声波测距后通过串口打印到电脑上面。 驱动和测距; - //超声波测距
' S, d, {8 v* i+ a' s2 j
+ o% ]* h, _ w x& ?( y) g- a- #include "hcsr04.h"* j$ |* W, T& W* I" z
- . k* a! F$ K" _. f3 `, a9 B) O
- #define HCSR04_PORT GPIOB
0 A3 g3 o2 Y4 J, q# | - #define HCSR04_CLK RCC_APB2Periph_GPIOB
. i" H$ s5 r; S" C - #define HCSR04_TRIG GPIO_Pin_5
" t4 B( n, V1 F, N% b* S - #define HCSR04_ECHO GPIO_Pin_6
- } H5 Z, E3 M5 J0 D9 G2 i
; ]. }7 M" T/ q# `: I& }# U2 ]- #define TRIG_Send PBout(5)
3 |! H' Z6 R4 b. c' m$ N1 x - #define ECHO_Reci PBin(6)
, O& u( v! h6 ^# H - 0 z: @& S i$ I8 q6 U; Z* T, R
- u16 msHcCount = 0;//ms计数& Z) [5 a" B0 E( f/ c! C
- + C5 J4 I# ~6 y* {, `* a& ~8 }
- void Hcsr04Init()2 `; u& i/ m) O; [* {6 A) o
- { 7 o4 W! Q2 J A/ V) A" D
- TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //生成用于定时器设置的结构体
0 ~0 [; _) h" H! [1 h3 b9 P; X, }+ y - GPIO_InitTypeDef GPIO_InitStructure;! M7 n0 x! S. {0 K$ C. l' \9 p
- RCC_APB2PeriphClockCmd(HCSR04_CLK, ENABLE);5 `% N0 B$ }/ `4 W
- " @7 e4 y5 Z. M
- //IO初始化
4 ~; P! [% b7 N6 e" L - GPIO_InitStructure.GPIO_Pin =HCSR04_TRIG; //发送电平引脚
) d/ q$ C& j3 Z7 r: K* ~) f - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- {; I: g0 g( t4 Z% e- Q( W - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
E5 {/ Y# a# w3 Z& [2 H$ o - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure);4 g' g: o' `) d- R. e
- GPIO_ResetBits(HCSR04_PORT,HCSR04_TRIG);
& ?* r% z$ x4 Y) e5 k - $ D# v- z. E: }8 B
- GPIO_InitStructure.GPIO_Pin = HCSR04_ECHO; //返回电平引脚
) L/ ~/ g# O0 N9 H ~, t5 e8 Y$ a - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
0 q/ r5 u7 o& J3 j; x6 U2 ~0 c4 d; W - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure);
/ k" l# U3 R$ P. t - GPIO_ResetBits(HCSR04_PORT,HCSR04_ECHO); }0 a% J9 p- L0 {8 _( q
-
+ u: z1 Z6 j/ @! y$ L# P - //定时器初始化 使用基本定时器TIM6
5 z4 H+ s' U4 K( |/ d; n& B0 B7 D9 L - RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //使能对应RCC时钟
' E$ C: ~/ P# a1 w* s - //配置定时器基础结构体' y p9 u# X b; g8 V D5 Q
- TIM_DeInit(TIM2);- ?& Q/ h3 m% n& K8 z$ b( n; ^
- TIM_TimeBaseStructure.TIM_Period = (1000-1); //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到1000为1ms
' i! L% O) N6 B$ ~3 B - TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //设置用来作为TIMx时钟频率除数的预分频值 1M的计数频率 1US计数
' L& y9 R, |, k$ N - TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//不分频
- B: h3 d% P7 M& `+ ~9 W - TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式; D3 @- o5 Q) c2 L( F7 S1 |
- TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位 " k) O/ }8 Z) V! j* F& n
- 2 l* H; L$ z6 i) r# f7 S# V( r& G* q: _
- TIM_ClearFlag(TIM6, TIM_FLAG_Update); //清除更新中断,免得一打开中断立即产生中断3 A' M" X, P; [$ c" e: d' r3 a* d
- TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); //打开定时器更新中断1 L) s `; }7 `* }4 v" \ |5 @3 k
- hcsr04_NVIC();
6 `$ B: p2 W! @$ `2 { - TIM_Cmd(TIM6,DISABLE);
e4 o& T; U# P5 P" }+ q+ _ - }# C" @4 @- G' W: R% f0 O; s0 V& J T
( S' ~0 D/ T$ H9 G4 Q; R
6 f, q) B6 f3 \2 n2 d- //tips:static函数的作用域仅限于定义它的源文件内,所以不需要在头文件里声明
/ i) F( W8 M7 o5 G0 F; L2 u8 O; P - static void OpenTimerForHc() //打开定时器* u# \% X! L& T9 {: P8 N! q2 H' v
- {
4 L2 w' Q7 v! }8 W& { - TIM_SetCounter(TIM6,0);//清除计数
& K$ {" r/ l& W, N m5 [ - msHcCount = 0;. E0 s5 D$ g. T. c% E0 i! K
- TIM_Cmd(TIM6, ENABLE); //使能TIMx外设- n3 C9 H% E0 E# X, d' j$ H
- }
% K8 u/ P* Q2 D# ?' l# _& B - 7 o% @! C9 m9 x P' D m
- static void CloseTimerForHc() //关闭定时器
6 K0 Y, m5 {9 ^6 [, n - {
/ t, \0 ^4 v/ G: J# v% u6 r - TIM_Cmd(TIM6, DISABLE); //使能TIMx外设
, @2 `7 ?3 t5 a; h$ b0 { - }5 `, t1 b8 j# |# e/ d* _$ H+ X
-
$ }0 k. m; K% a9 Y5 I8 Q& O! s -
/ a, H- \4 y7 A - //NVIC配置
8 P1 h# b% R: l - void hcsr04_NVIC()0 `1 O9 l# N/ X
- {5 z. \2 f6 x% ~8 N0 |* j9 X' L
- NVIC_InitTypeDef NVIC_InitStructure;7 M6 p( k+ D1 C) Z; w* @3 `
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
3 A ?) _! t2 }' j! e3 I! v8 O; n -
/ e' g1 B- e9 H s4 L+ H9 } - NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn; //选择串口1中断
/ b! q* o6 k% Z9 C, l - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占式中断优先级设置为1
: @( p# D& |2 u& V% Y# j) h3 _ S% { - NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应式中断优先级设置为1
0 @. U7 e0 a# q - NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断
* v9 B, c+ F3 r0 c, Y3 K" x, K2 z2 r - NVIC_Init(&NVIC_InitStructure);
0 q& ]/ L# f9 @- {6 L0 m - }
5 [, P6 e* N: N' E
I( ^ @! K6 }/ k2 y* l
1 d2 t3 B( i" [ k4 Y* x' E- //定时器6中断服务程序2 r* `$ o" w5 M/ H
- void TIM6_IRQHandler(void) //TIM3中断
2 y% F1 \& Z% G2 z - {
- l( c# M& m( ~5 @! S1 z+ f! B - if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否7 Z; G! y0 W, e
- {
2 N! _# M6 C" s. E& S5 d" R" _$ H - TIM_ClearITPendingBit(TIM6, TIM_IT_Update ); //清除TIMx更新中断标志
3 `( A- h9 e* b# f - msHcCount++;
$ T6 j: q! k# r/ n- a+ r; X" b" _ - }
8 n# k! r% q4 q5 A" A/ F9 P1 Z! D - }
) k3 s$ F( S7 s - 6 V2 Q/ |2 d& h+ ^' _: |. O7 ~
- ? [% | `& ?/ a# Q. j
- //获取定时器时间
4 J6 s$ v7 h. n' |7 D0 n - u32 GetEchoTimer(void)' C/ T. J' K. v; Y- _
- {
. Q" j$ n9 z& e! I( k& @ j - u32 t = 0;
$ z) F5 G9 @7 ]- ~# Y# b# w - t = msHcCount*1000;//得到MS; V G+ G) A0 h' S
- t += TIM_GetCounter(TIM6);//得到US0 o, j% d8 ?7 R& d
- TIM6->CNT = 0; //将TIM2计数寄存器的计数值清零
( b, _! d" N9 P - Delay_Ms(50);9 r! Z9 q1 C+ d; _" Z) z
- return t;
; Z# P6 D, Y7 }# D5 u - }4 f; @, p& j0 ~. Y- \
-
G( w$ j, e7 @" d# E5 k" D - & u0 w5 d5 I6 \$ X& j3 V
- //一次获取超声波测距数据 两次测距之间需要相隔一段时间,隔断回响信号6 a6 P9 {5 Q: E5 j/ C* b) {( v
- //为了消除余震的影响,取五次数据的平均值进行加权滤波。
# L7 k0 r. S4 x1 J. {5 _ - float Hcsr04GetLength(void )4 w- f, ?* G( p. u) F
- {
$ B5 C0 \# C1 e. `1 F$ d- N7 s - u32 t = 0;& O7 S+ |6 F( `2 o, }# ?: o: L
- int i = 0;$ T! B0 a! z; ~
- float lengthTemp = 0;
7 b! P2 A5 u' H3 z* L2 E - float sum = 0;
9 _3 b' f5 V; T- W; } - while(i!=5). f% f o- D2 a1 F7 G( g) ^, q
- {+ K$ g2 J) h, c( {
- TRIG_Send = 1; //发送口高电平输出1 A5 z+ G; W: Y" W
- Delay_Us(20);
9 ], C! P1 E+ e" J7 t6 F - TRIG_Send = 0;2 N& u* G Z8 K2 ^& x
- while(ECHO_Reci == 0); //等待接收口高电平输出- }2 x I% t: j* J
- OpenTimerForHc(); //打开定时器: ~" f8 b& @5 ]% c5 R1 N
- i = i + 1;- Q; c: j% r. a; M3 N
- while(ECHO_Reci == 1);
. F( g- q9 D% U8 l7 S* ?6 V! V - CloseTimerForHc(); //关闭定时器$ a: O1 w% i4 L) s8 g+ m( c2 j
- t = GetEchoTimer(); //获取时间,分辨率为1US! a- H8 C& K: i; U
- lengthTemp = ((float)t/58.0);//cm
6 c# U* u+ j! P( V - sum = lengthTemp + sum ;
# _% d* Q+ ^2 z- i -
4 M1 p# c$ A" u5 u6 @- @5 R: e0 c4 A - }3 Q3 w ]6 E, {" q. M2 p
- lengthTemp = sum/5.0;
! ^* k2 Q/ D% r - return lengthTemp;$ u# P/ \: X# F( T1 a! u
- }
/ y4 \( D6 ~' w) }$ `- W3 d7 F: j
% y5 {# G, M8 r+ j8 y
; A0 H) ?9 E, [) I! v9 `- /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
- e! w- E) K& [% @- c% w( z7 F - ** 函数名称: Delay_Ms_Ms
a* ~/ s) M# D" j - ** 功能描述: 延时1MS (可通过仿真来判断他的准确度)
" P% {$ W9 _7 q6 o7 w o - ** 参数描述:time (ms) 注意time<65535 K; ^* L1 ?1 e& r
- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/4 p7 j7 H5 G! M" S$ B3 N
- void Delay_Ms(uint16_t time) //延时函数
5 X+ c2 r4 o2 l. L' | - {
: A, F0 X" f/ f. g - uint16_t i,j;
7 b ?7 s; u0 J' j - for(i=0;i<time;i++): h( {' l$ P2 \( B$ }4 L
- for(j=0;j<10260;j++);2 b; ^) {: e0 o
- }
% _) |% c9 z0 X0 n6 { - /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
. s( x( c! }9 t; O( w - ** 函数名称: Delay_Ms_Us
: V6 V U; [3 E( j, z1 t" x - ** 功能描述: 延时1us (可通过仿真来判断他的准确度)8 u! K" O+ |6 k# Q, }
- ** 参数描述:time (us) 注意time<65535
2 ?6 M9 U$ Q/ B* U; o! m/ Q! y' b$ O E4 V - :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/4 k. C* i; f5 r8 e* s, {
- void Delay_Us(uint16_t time) //延时函数# N% }0 T4 Y9 _4 q# }# i% U
- { 4 I7 m( i; R( y9 w8 U
- uint16_t i,j;. l9 P4 I7 t g6 o4 v, z2 ^' V. u1 V% q
- for(i=0;i<time;i++)
F: o9 K( Y0 r% b( {/ e3 ? } - for(j=0;j<9;j++);
' L- a, n# L: q% T4 I - }
复制代码 6 D+ F- J$ D7 Y. T) a
但是关于USART的函数我就不往上写了,这个简单的串口打印大家应该都会写。下面简单贴一下我的主函数吧。 - /*6 v7 L* e" }% A! f* e* R
- 教训:实验前一定要检查引脚连接是否正确,万不可搞错,不然又要烧坏芯片!!!!% @: g& {6 ^& E) ?9 u8 [
5 W F. D$ E% H: ~- */6 _& ^- y- w' G0 C2 n
% j& `4 m M0 [; N& }- #include "hcsr04.h"
$ H: Q& H* H. V& A - #include "chao_usart.h"
* q, Z. A3 ]& ] - * v2 Z% Z, C5 J8 k# j( M
- int main()- ?/ w, k" [7 Z. T' {9 d
- {0 ?6 ?. q% Y# [4 E7 z- W0 r+ {, ~
-
9 g' i' P% I# I5 F% O: P - float length;2 f0 [' h$ C+ V) x% {, ?( u8 @
-
. Z1 O1 Q# z x4 Q2 }7 ~ - GPIO_cfg();
: T' C1 e% g$ ^% y& i - NVIC_cfg();7 A8 E8 |6 i2 F: ^/ j9 V5 N+ o
- USART_cfg(); % n1 Z" T4 W5 Q e! ]$ [( w
- printf("串口初始化成功!\n");
, I5 Q, H4 k) N& l( _ - 6 E1 d5 m( w* v4 w7 E' f* k5 ^
- Hcsr04Init(); ) ^. g/ @& n" T4 x* h' e: u! [
- printf("超声波初始化成功!\n");//测试程序是否卡在下面两句上面2 k8 q# ]% v; F$ S! L
- $ X6 ^8 Y' Q% G8 P. h% }- f
- length = Hcsr04GetLength();0 ]1 A3 J h4 [$ g5 C' x( ]
- printf("距离为:%.3f\n",length);
. N# Z1 a) q# W - 5 P6 a* y# j3 g) x# t+ C5 R
- " a1 `6 ?3 p1 o. z
- }
复制代码 ( t: t; z9 w. r. u+ Y+ z5 [
8 w5 N' j& C% T- O, y% j
实验结果: ! y- H. G! u2 t
* d# K6 T k: A3 ^5 H# [) Z6 S ~" H
好了,其实这个模块很简单,但是要是把他用的很好的话还是比较困难的,比如用超声波做一个四轴定高的程序,还是有一定的挑战性的。 写这篇博客的目的不仅仅是介绍这个模块的使用,其实这种使用介绍网上一搜一大把,我只是想纪录一下,我在做这个模块的时候遇到的一些其他的问题。 其中有一个小插曲,就是当吧写好的程序烧进去之后,运行时总是出现每次返回一个同样的比正常值小的多的数据,比如说0.034cm,这明显是一个错误的数据,但是刚开始的时候,不知道为什么 总是这样,多次复位从新上电总是这一个数据。让我很是苦恼。但是幸运的是,在这样的情况中间,他又会有时出现一两个正常的的数据,让你有点摸不着头脑。 上网查了一下才慢慢明白,这种现象叫做“余震”,网上关于余震的解释大致有三种: 1、探头的余震。即使是分体式的,发射头工作完后还会继续震一会,这是物理效应,也就是余震。这个余震信号也会向外传播。如果你的设计是发射完毕后立刻切换为接收状态(无盲区),那么这个余震波会通过壳体和周围的空气,直接到达接收头、干扰了检测(注:通常的测距设计里,发射头和接收头的距离很近,在这么短的距离里超声波的检测角度是很大的,可达180度)。1 o+ ~: F2 i+ `5 b6 n7 |
2、壳体的余震。就像敲钟一样,能量仍来自发射头。发射结束后,壳体的余震会直接传导到接收头,当然这个时间很短,但已形成了干扰。另外,在不同的环境温度下,壳体的硬度和外形会有所变化,其余震有时长、有时短、有时干扰大、有时干扰小,这是设计工业级产品时必须要考虑的问题。2 ~* a6 I6 f2 k3 l2 c/ j+ k$ K
3、电路串扰。超声波发射时的瞬间电流很大,例如某种工业级连续测距产品瞬间电流会有15A,通常的产品也能达到1A,瞬间这么大的电流会对电源有一定影响,并干扰接收电路。通过改善电源设计可以缓解这种情况,但在低成本设计中很难根除。所以每次发射完毕,接收电路还需要一段时间稳定工作状态。在此期间,其输出的信号很难使用。
* Z) b( A8 h# X% Z) J3 O消除上述现象的方法之一就是在检测的时候多次循环检测,取平均值,也就是加权平均滤波,一个简单的滤波处理。就是下面这一段: - int i = 0;/ ]1 D* A4 b9 Q: c4 O
- float lengthTemp = 0;/ e! _& }2 M/ X" e+ P! @; e$ t0 l: |
- float sum = 0;
$ x$ L! X7 N. S) ^ - while(i!=5) n; W& Y3 p5 P' M; s
- {# o7 Z+ Y) K. T- ?
- TRIG_Send = 1; //发送口高电平输出% E+ K2 y; L3 g1 t+ i7 Y5 J
- Delay_Us(20);/ @& p2 A, J1 N* s& M4 m
- TRIG_Send = 0;0 H; o1 [+ {. j0 J6 g- @
- while(ECHO_Reci == 0); //等待接收口高电平输出
. A) o0 x) T* W: l - OpenTimerForHc(); //打开定时器
$ V5 d9 U' h6 T/ M) g$ P; s - i = i + 1;
\: P% s2 ^3 b7 x - while(ECHO_Reci == 1);. `" {* G% z" c% S5 b, ^# r) G: z
- CloseTimerForHc(); //关闭定时器
" m! Z: `" D: _4 f - t = GetEchoTimer(); //获取时间,分辨率为1US
, x' A8 I* P6 u+ R) c - lengthTemp = ((float)t/58.0);//cm" Y5 D- O# C8 t, R
- sum = lengthTemp + sum ;4 r" K- r+ z2 Q" m% E
- ' [. v9 d' e! O& t- Q, T* d
- }
0 c/ D4 g, C" t" U. m+ E1 I, o - lengthTemp = sum/5.0;( R! s3 C8 H0 v5 x% l
- return lengthTemp;
复制代码
0 v# }9 @+ i# V4 X0 y$ L+ t7 P4 e. g. P+ n8 t
加了这个之后,基本上就没有出现余震现象了。 还有一点就是测试程序前一定要检查引脚有没有接错,不管多有把握,也要看一遍,不然很容易出大事的,一个芯片也许就因为你的大意给GG了。切记,这个应该也算我们这个行业的基本素养吧。
; U+ r" _6 s8 Z4 K |