首先,先来看一下这个模块的基本功能和原理。 HC-SR04超声波测距模块可提供2cm-400cm的非接触式距离感测功能,测距精度可达高到3mm;模块包括超声波发射器、接收器与控制电路。像智能小车的测距以及转向,或是一些项目中,常常会用到。智能小车测距可以及时发现前方的障碍物,使智能小车可以及时转向,避开障碍物。 注意是5v输入,但是我用stm32 的3.3v输入也是没有问题的。 ( I6 Y* q. t7 O1 \0 Z7 L
2 @ k! t! X# ]$ T2 ^
二.工作原理 1.给超声波模块接入电源和地。
& U4 g6 w) W2 m8 j0 P$ Y8 s; [ 2.给脉冲触发引脚(trig)输入一个长为20us的高电平方波 3.输入方波后,模块会自动发射8个40KHz的声波,与此同时回波引脚(echo)端的电平会由0变为1;(此时应该启动定时器计时)8 Y% b& n3 w4 F# C+ U8 M. i
4.当超声波返回被模块接收到时,回波引 脚端的电平会由1变为0;(此时应该停止定时器计数),定时器记下的这个时间即为超声波由发射到返回的总时长。, B( d4 _/ E: F& A/ n( d
5.根据声音在空气中的速度为344米/秒,即可计算出所测的距离。 要学习和应用传感器,学会看懂传感器的时序图是很关键的,所以我们来看一下HC-SR04的时序触发图。 * J: \( O \: \8 }; ?' j! i! W; b
我们来分析一下这个时序图,先由触发信号启动HC-RS04测距模块,也就是说,主机要先发送至少10us的高电平,触发HC-RS04,模块内部发出信号是传感器自动回应的,我们不用去管它。输出回响信号是我们需要关注的。信号输出的高电平就是超声波发出到重新返回接收所用的时间。用定时器,可以把这段时间记录下来,算出距离,别忘了结果要除于2,因为总时间是发送和接收的时间总和。 下面是亲测可用的驱动程序。 芯片型号为stm32f103zet6,超声波测距后通过串口打印到电脑上面。 驱动和测距; - //超声波测距
: T8 D& J! n f1 i" t
! K( x$ M, G$ \- n( d8 t" o- #include "hcsr04.h"' E$ R9 F* c5 a9 Q3 b1 V4 L
-
) K1 N1 D# L2 g' T8 L& T1 [ - #define HCSR04_PORT GPIOB
: w& h( ?1 A5 O" h! f" p - #define HCSR04_CLK RCC_APB2Periph_GPIOB% x, U$ D0 x {/ P4 J; y0 j
- #define HCSR04_TRIG GPIO_Pin_5
+ S4 c+ t6 H9 S3 q4 I6 k - #define HCSR04_ECHO GPIO_Pin_6
: z% r7 t! A' E1 x& l% g1 c
, v/ |% g, m4 w( U! f# M; s" E' H- #define TRIG_Send PBout(5)
& d' b0 B; Y6 R - #define ECHO_Reci PBin(6). X) |7 q( E3 W5 |# f, B& d
' ]6 ^1 O4 \* @( N) K8 b- u16 msHcCount = 0;//ms计数3 y8 C1 Y( {6 f( F" c, l$ K
+ q. p; ~' W2 o- void Hcsr04Init()
- K; h) Z$ Z# u/ _; | @9 \# ~ - { : Z# S$ N1 a8 K. _/ W# Z d
- TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //生成用于定时器设置的结构体
. w. @) E, L* o$ j0 E2 `! | - GPIO_InitTypeDef GPIO_InitStructure;" F8 V$ D6 V/ Y9 B# e, y; Z
- RCC_APB2PeriphClockCmd(HCSR04_CLK, ENABLE);
- R( v/ w! p, S# d+ a4 E# l" d -
$ t5 E1 w! g r; Y9 d, ` - //IO初始化2 I# l) L4 H- M' y
- GPIO_InitStructure.GPIO_Pin =HCSR04_TRIG; //发送电平引脚
9 Y$ P4 l$ B: o! E8 G1 `9 h" s) y6 I - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
& z' @# E' h! ? - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
* \* [( q ]- h7 u - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure);1 v1 D4 w, A4 `! i$ d
- GPIO_ResetBits(HCSR04_PORT,HCSR04_TRIG);9 N) f R$ H& V& d; f
- 4 P- X# Z" g% x6 K
- GPIO_InitStructure.GPIO_Pin = HCSR04_ECHO; //返回电平引脚
3 ]8 O8 L- h7 t. [ V3 X - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
5 z6 {9 U! ~, T$ I$ q% b - GPIO_Init(HCSR04_PORT, &GPIO_InitStructure); ( r' N- h7 S$ \0 B' H7 L2 L. Y
- GPIO_ResetBits(HCSR04_PORT,HCSR04_ECHO);
* ~9 k$ _1 m" I - # g/ w% l6 i4 n$ [. k- J% t/ r6 K$ s
- //定时器初始化 使用基本定时器TIM6( d& r3 s p# T$ Y( {6 L+ |, l
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //使能对应RCC时钟% ]1 V# H. L, E3 P
- //配置定时器基础结构体
/ B/ J$ |" |) i+ q. s - TIM_DeInit(TIM2);1 J: U- ] f2 j; g3 Q( ~
- TIM_TimeBaseStructure.TIM_Period = (1000-1); //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到1000为1ms" z' X! w R% r9 D
- TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //设置用来作为TIMx时钟频率除数的预分频值 1M的计数频率 1US计数
& \9 ~7 a# ]" H) F - TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//不分频
^7 s" |& Q ?! p. q# k$ Y - TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
0 } [& v$ W6 l' u9 f6 Y9 H - TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
" [8 Z! h/ M9 ]2 o- i - - j: v6 B8 ?- T k5 A& p1 i
- TIM_ClearFlag(TIM6, TIM_FLAG_Update); //清除更新中断,免得一打开中断立即产生中断& L7 J% a9 K b9 }
- TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); //打开定时器更新中断7 a6 v8 V( e# V, j
- hcsr04_NVIC();6 [+ g2 k4 }/ H
- TIM_Cmd(TIM6,DISABLE); , w6 M# m/ J# L: v4 I1 S; N
- }2 J$ }* X$ c1 {. m, l0 W9 Z
- 8 z6 N$ `9 C+ M' t& A/ [- A- _
4 n% E# v" P, @( O) e2 q; ?5 l6 {- //tips:static函数的作用域仅限于定义它的源文件内,所以不需要在头文件里声明( e' l7 Q- `+ ^
- static void OpenTimerForHc() //打开定时器8 K- q& @, W9 [
- {5 m' x; C6 E2 v* Q" o
- TIM_SetCounter(TIM6,0);//清除计数
) Z8 j6 C# L5 t0 J - msHcCount = 0;
1 A6 R8 L9 ~* u% k( s' R - TIM_Cmd(TIM6, ENABLE); //使能TIMx外设5 z0 _% i( B( w) w( j0 L
- }& u# I+ K' P" }6 s$ W' D
- & F+ D# q: x! Q( N7 q9 q+ Q
- static void CloseTimerForHc() //关闭定时器; _( k4 J- N2 f& T
- {
, v/ k# B& e% T8 g - TIM_Cmd(TIM6, DISABLE); //使能TIMx外设" v( \& ^$ f% Y
- }. _: F e" V; N8 @* b4 s, l: }) |5 h
-
2 K# v0 V, v: q, j! v - * W* z2 p- n* {3 w! L, D
- //NVIC配置
( g/ h3 Y! h1 a. {8 v - void hcsr04_NVIC()3 h( Q4 W; a1 s+ K% n8 M
- {
6 T1 {- B8 G! i - NVIC_InitTypeDef NVIC_InitStructure;2 N/ s# V; O1 [
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
2 b8 t! Q2 |8 y% M1 k5 A - : s. h6 N6 R7 C" o3 v- P
- NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn; //选择串口1中断
) N# c0 c. _. Y. z- J0 X% p - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占式中断优先级设置为1) D* h* o1 `+ w# K4 ~
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应式中断优先级设置为17 X; ]9 g/ S$ d& `8 H3 t* Z0 ]" Q
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断
) ^+ Q9 L$ L, a. C( i( S - NVIC_Init(&NVIC_InitStructure);! D9 ?& }: s+ c4 B' i/ j" B
- }
# N* ]' b$ `; s" K; B5 G7 p
3 y# Y& C+ [, ^, l; A- & E$ H% i H9 X
- //定时器6中断服务程序
; Z6 d. o2 f- J/ O$ T0 R! H - void TIM6_IRQHandler(void) //TIM3中断
X# G# ^% M% N9 x- v- ]0 ?: ^+ `; Q) s - {
1 T: a4 \: e$ y; Z - if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否# P) S- R' N; ~6 T& c2 r
- {+ e6 Y, y: m# p. k5 [1 s, ]
- TIM_ClearITPendingBit(TIM6, TIM_IT_Update ); //清除TIMx更新中断标志 , r4 [8 M1 B* G6 _8 u1 {" N$ U
- msHcCount++;7 K& @ N& }8 F% Z/ C; i+ w1 a
- }
Y' M7 N: q. G n& V, j0 C - }
( o( N) u" Y, \) H/ {" O+ A2 ^ - # H& ~/ w- _* P4 G v5 j. _
: {* Z6 d o+ O1 v8 O' ?- //获取定时器时间
- m+ r4 T9 {6 _: ~9 v; X. x& z - u32 GetEchoTimer(void)
1 C, M: M: s% X( O8 N/ d. X$ P7 q - {9 I' X! H* N4 z8 i8 _$ ]
- u32 t = 0;, ]! ]0 p5 k% K9 [' L
- t = msHcCount*1000;//得到MS
) p) S1 s! j0 i' p; { b9 X+ t - t += TIM_GetCounter(TIM6);//得到US$ g9 R% e' u3 W) q( B
- TIM6->CNT = 0; //将TIM2计数寄存器的计数值清零0 B _2 W8 A+ u
- Delay_Ms(50);' C: g- B) ?# }, b8 i: M
- return t;$ f- c- O, V% D, x5 b2 W
- }& u/ a. |, T' o* r7 y
- ; M) J7 Q* _( g" J8 o: T( Y) a3 [
- ( w! R* T7 z- {4 T
- //一次获取超声波测距数据 两次测距之间需要相隔一段时间,隔断回响信号
0 U. _5 e% x, x8 e - //为了消除余震的影响,取五次数据的平均值进行加权滤波。& i5 D* f3 k; e% s9 Z4 |
- float Hcsr04GetLength(void )
2 e; `. ~* k2 S0 h& J' d' t3 j- C - {
3 Y5 q; @4 j s5 N8 ]$ B( q - u32 t = 0;
# U: J& e" l, ]1 \ - int i = 0;
, a! R4 q% K! @8 u7 s# J - float lengthTemp = 0;
4 z& Q. e- \' r( y6 F/ s% n - float sum = 0;3 G" k! H9 B5 y f$ k, Z
- while(i!=5)! A1 Q% v$ f( q* |
- {
& t% J& ]+ F8 g I! T6 e' J - TRIG_Send = 1; //发送口高电平输出
6 j+ T3 u+ d2 c3 t - Delay_Us(20);1 N) n; u3 V" J( o" p2 a1 w+ L
- TRIG_Send = 0;) C( c) b# W& { l; _1 z7 }6 p
- while(ECHO_Reci == 0); //等待接收口高电平输出
( t) W5 d4 o9 l3 F - OpenTimerForHc(); //打开定时器
, K. j1 r0 Z% C" [ - i = i + 1;& [+ e* p% W$ @
- while(ECHO_Reci == 1);
% j2 x/ d$ I6 N n! } - CloseTimerForHc(); //关闭定时器
7 Q5 h% x, r% }: k, k: W5 e - t = GetEchoTimer(); //获取时间,分辨率为1US
, U! x+ A7 m$ L7 \ - lengthTemp = ((float)t/58.0);//cm6 f$ k# S9 D% {4 P+ h& M
- sum = lengthTemp + sum ;
/ U- f% r! L6 a" a: s& L1 ? -
! T2 ]) r) V- r5 m& C6 G$ o2 G4 Y - }9 G) e& o, U: i- v, u
- lengthTemp = sum/5.0;
/ a$ g3 W- }% y1 L; P! ~ - return lengthTemp;* u" g2 `* l4 C& r, H2 @
- }
8 _. m3 C1 \% B
+ q7 q/ a2 L0 c7 A, L" F: t- 6 D% w& o$ x) \5 t0 L1 U
- /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::' C) Q8 t' W2 W% J2 g$ Q; X
- ** 函数名称: Delay_Ms_Ms
8 P+ N, L7 d( T/ V9 h% t - ** 功能描述: 延时1MS (可通过仿真来判断他的准确度) & I9 s' C. m- ]4 u
- ** 参数描述:time (ms) 注意time<65535
! J. @7 f" K( T - :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
1 E% j6 l2 Y$ H ]6 I - void Delay_Ms(uint16_t time) //延时函数
! y% k1 C) t' B - {
7 P( m7 @( X B* \* n; |4 v - uint16_t i,j;
" w! O, a* W9 _- z6 |: ^ - for(i=0;i<time;i++)) z$ B; ^+ Z& s( k6 [
- for(j=0;j<10260;j++);' U4 g) R9 v; Z0 y
- }
; a3 k4 n3 f, `/ I0 \& ] - /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
2 ~ N/ ~4 f/ X W- J - ** 函数名称: Delay_Ms_Us
; C: _: c) b! Q- x/ ] - ** 功能描述: 延时1us (可通过仿真来判断他的准确度)
1 K. s, T, v! A7 W8 U% @; c$ b! N - ** 参数描述:time (us) 注意time<65535 ! J/ }; b1 v0 M- H4 A! G! h
- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
( D/ n! v8 X" z8 ` c2 y - void Delay_Us(uint16_t time) //延时函数; c- P% M* {# V4 u9 `& W; G
- {
5 o2 l2 Q8 C$ T - uint16_t i,j;) K5 W5 d `2 P( ?3 G5 p
- for(i=0;i<time;i++)
# }! j8 U9 Z6 F! M/ R) u% b; P - for(j=0;j<9;j++);; N4 V. j& N9 e3 F: ]: e7 z, T0 s
- }
复制代码 : x4 s! N" O4 r9 i# l& N- I* j
但是关于USART的函数我就不往上写了,这个简单的串口打印大家应该都会写。下面简单贴一下我的主函数吧。 - /*0 W1 H2 X/ c3 D! J Q. e
- 教训:实验前一定要检查引脚连接是否正确,万不可搞错,不然又要烧坏芯片!!!!
! N- l0 v7 s# @ - 3 V2 U) Y! v1 h/ V3 e: S" G
- */8 I9 o1 |( X" L# S- B7 o p, M: a
8 [( }' G% f, t O- I3 P+ }- #include "hcsr04.h"$ |% F* w. ?2 g }
- #include "chao_usart.h"
9 S: x7 j+ _$ r- E ] - 5 k0 O( Z. L) \' P
- int main()- E0 b6 f3 T3 o i8 ~; D
- { j" T1 I- _3 v% q/ B
-
) B* N8 z) Q5 ~! I; Q - float length;
, {# C* i$ C J -
9 {" P0 I, D- v1 p/ r - GPIO_cfg();
( l' I( d1 U' S( o6 x- d+ Z - NVIC_cfg();
+ _% S# n c0 e' H9 l - USART_cfg(); - |( n1 F6 n. \; w/ C
- printf("串口初始化成功!\n");
- Q" \ a! U9 P Z$ G# B9 l -
9 t* ^! V! t( b/ I9 C, n5 Y - Hcsr04Init(); 5 Z4 Y) t( q* M% ~! V( \( J6 F7 b
- printf("超声波初始化成功!\n");//测试程序是否卡在下面两句上面
* W1 `$ y( t0 A+ g3 Z# c, r* I
: \0 f. F1 O9 H* y- }- length = Hcsr04GetLength();$ a C" g) T$ F0 g
- printf("距离为:%.3f\n",length);
* M; Y1 I$ y$ o+ k - ! o. i7 v/ @. }5 u
-
8 L" A- ~7 u# x! L4 r5 A5 D$ h! R - }
复制代码
& f2 D$ _' h: {6 X; k$ a* f, Z) b- |1 Z
实验结果:
( Q! x% c- h0 o I- x1 C; S* g, Z3 m
9 v1 I" k9 m1 I9 b: Y0 m5 r. V* p9 j6 m7 C! h
好了,其实这个模块很简单,但是要是把他用的很好的话还是比较困难的,比如用超声波做一个四轴定高的程序,还是有一定的挑战性的。 写这篇博客的目的不仅仅是介绍这个模块的使用,其实这种使用介绍网上一搜一大把,我只是想纪录一下,我在做这个模块的时候遇到的一些其他的问题。 其中有一个小插曲,就是当吧写好的程序烧进去之后,运行时总是出现每次返回一个同样的比正常值小的多的数据,比如说0.034cm,这明显是一个错误的数据,但是刚开始的时候,不知道为什么 总是这样,多次复位从新上电总是这一个数据。让我很是苦恼。但是幸运的是,在这样的情况中间,他又会有时出现一两个正常的的数据,让你有点摸不着头脑。 上网查了一下才慢慢明白,这种现象叫做“余震”,网上关于余震的解释大致有三种: 1、探头的余震。即使是分体式的,发射头工作完后还会继续震一会,这是物理效应,也就是余震。这个余震信号也会向外传播。如果你的设计是发射完毕后立刻切换为接收状态(无盲区),那么这个余震波会通过壳体和周围的空气,直接到达接收头、干扰了检测(注:通常的测距设计里,发射头和接收头的距离很近,在这么短的距离里超声波的检测角度是很大的,可达180度)。
* e: h4 j- |3 k; H+ h+ t! i9 Y3 z) g 2、壳体的余震。就像敲钟一样,能量仍来自发射头。发射结束后,壳体的余震会直接传导到接收头,当然这个时间很短,但已形成了干扰。另外,在不同的环境温度下,壳体的硬度和外形会有所变化,其余震有时长、有时短、有时干扰大、有时干扰小,这是设计工业级产品时必须要考虑的问题。0 e" U( ?7 @# v, d T- W
3、电路串扰。超声波发射时的瞬间电流很大,例如某种工业级连续测距产品瞬间电流会有15A,通常的产品也能达到1A,瞬间这么大的电流会对电源有一定影响,并干扰接收电路。通过改善电源设计可以缓解这种情况,但在低成本设计中很难根除。所以每次发射完毕,接收电路还需要一段时间稳定工作状态。在此期间,其输出的信号很难使用。 1 h, ^' x- F/ N- O# P' J
消除上述现象的方法之一就是在检测的时候多次循环检测,取平均值,也就是加权平均滤波,一个简单的滤波处理。就是下面这一段: - int i = 0;" w0 u9 T) ^9 k' B' e$ u
- float lengthTemp = 0;
; ], N$ |; S: U% {% [ - float sum = 0;( W o+ d4 n M6 A/ q
- while(i!=5). V4 @9 d) u/ v6 u8 H' D5 O
- {
1 E- y- \! b7 V( y: ^, q; ] - TRIG_Send = 1; //发送口高电平输出. h3 [$ A$ N' y% d
- Delay_Us(20);* X' R# m7 _, z" ?+ X2 t
- TRIG_Send = 0;8 a6 A2 Z: k1 ]& b5 ]0 X
- while(ECHO_Reci == 0); //等待接收口高电平输出6 H. B: t& P- ?% c3 g& O8 `
- OpenTimerForHc(); //打开定时器. n V$ M4 p2 b" H; o% \
- i = i + 1;' G9 Z& j% o" |# u; O7 A) U
- while(ECHO_Reci == 1);- L0 H9 s& V0 }, m5 ]% e* h
- CloseTimerForHc(); //关闭定时器2 N* U, T r. k2 I0 H; [# r
- t = GetEchoTimer(); //获取时间,分辨率为1US
; w1 {5 J5 { m# R: a1 Q8 e' m - lengthTemp = ((float)t/58.0);//cm
& u% z4 T2 D( n# a6 \! d5 ?/ o1 m - sum = lengthTemp + sum ;3 [' B& H4 V( \ r$ l1 S( `
- 3 |& L, \4 I: i" T2 c- s
- }# L' r+ ~- m2 v3 F8 N& T
- lengthTemp = sum/5.0;% X0 [8 K: `& I
- return lengthTemp;
复制代码
( z; g: D$ U/ J/ n: O
: H9 b% ]7 ]9 Y; ~3 N# t! g加了这个之后,基本上就没有出现余震现象了。 还有一点就是测试程序前一定要检查引脚有没有接错,不管多有把握,也要看一遍,不然很容易出大事的,一个芯片也许就因为你的大意给GG了。切记,这个应该也算我们这个行业的基本素养吧。 ( t( Y! n$ q, Y; c+ Z! B2 v1 E
|