十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

51单片机驱动HC-SR04超声波测距与LCD12864显示实现

51单片机驱动HC-SR04超声波测距与LCD12864显示实现 简介面向51单片机学习者和电子设计初学者这份压缩包提供超声波测距并通过LCD12864显示距离的完整C语言实例包含Proteus仿真工程与可直接烧录的HEX文件可直接用于课堂实验、课程设计或入门项目参考。包内共12个文件以Keil工程配置.uv2/.opt、C源程序、Proteus相关备份及编译中间文件为主压缩包仅32KB结构清晰文件覆盖源码、工程配置、仿真与烧录环节便于快速打开工程并对照学习。已有607人浏览学习属于轻量但非常实用的单片机外设应用案例。例程逻辑清晰代码注释简洁适合在Proteus中逐步调试并理解模块化编程思路。通过该资源可掌握HC-SR04类超声波模块的触发与回波计时原理、LCD12864的显示驱动方法以及51单片机C语言中定时器与中断的配合使用同时可借助Proteus仿真工程观察硬件连接、显示效果与程序运行流程减少实物调试成本。1. 超声波测距与 LCD12864 的组合为什么值得自己写一遍用 51 单片机驱动超声波传感器做测距显示端不用数码管而用 LCD12864这个组合在课程设计和实际仪表面板里都很常见。原因有三一是 HC-SR04 这类超声波模块只要一根触发引脚和一根回波引脚时序逻辑清晰非常适合当作 C 语言状态机与定时器捕获的入门案例二是 LCD12864 能直接显示汉字和两行以上的测量结果比 1602 的信息密度高能把距离、温度、状态提示同时放在一屏上三是在 Proteus 里做仿真时超声波模块的触发和回波逻辑可以用信号源或模型快速验证不用等真实硬件焊完才调试。这篇文章会从硬件连接、51 的 C 语言驱动、Proteus 仿真搭建到误差修正和阈值报警扩展一步步把整个方案讲透。适合已经写过 LED 流水灯和数码管驱动、想往传感器读取与液晶显示方向进阶的读者也适合需要交单片机课程设计的在校学生直接照着改。2. 硬件连接与 Proteus 仿真先理清超声波模块和 LCD12864 的引脚时序2.1 HC-SR04 的时序本质不是测电压是测脉冲宽度超声波测距模块 HC-SR04 对外只有四个引脚VCC、GND、Trig、Echo。单片机要做的事非常少先给 Trig 一个不低于 10us 的高电平脉冲模块内部就会自动发出 8 个 40kHz 的超声波脉冲同时把 Echo 引脚拉高。Echo 高电平持续的时间就是超声波从发射到碰到障碍物再返回的总时间。单片机只需要用定时器精确测量这个高电平宽度再乘上声速并除以 2就能得到距离。关键点在于Echo 高电平的测量精度直接决定了测距精度。假如声速按 340m/s 计算1us 的时间误差对应约 0.17mm 的距离误差看起来很小但对定时器而言1us 以内的误差来自定时器溢出和中断响应延迟。51 单片机晶振 12MHz 时机器周期 1us用定时器工作在方式 116 位能计满 65535us对应距离约 11.1 米足够覆盖 HC-SR04 的实际量程。代码里通常不用中断而是在检测到 Echo 上升沿后启动定时器下降沿时停止并读取计数值这样能避免中断进出栈带来的时间抖动。2.2 LCD12864 并行接口与 ST7920 时序LCD12864 在 Proteus 里常见的是带字库的 ST7920 控制器。和 LCD1602 类似它有两种接口模式8 位并口和串口。并口需要 8 根数据线 D0-D7加上 RS寄存器选择、RW读写、E使能还有 PSB 引脚拉高或拉低选择并口/串口。Proteus 仿真时建议直接用并口因为并联数据线的电平状态在仿真里非常直观排错方便。ST7920 的关键时序参数有几个必须注意使能 E 的高电平脉冲宽度至少 450nsRS/RW 建立时间至少 100ns数据建立时间至少 80ns。51 单片机的机器周期通常是 1us 或 1.085us只要在写数据前加几个空指令就能满足建立时间。真正难的是初始化时序ST7920 上电后需要等待 40ms 以上再依次写入功能设置指令0x30、显示开关指令0x0C、清屏指令0x01。如果初始化顺序不对12864 会一直无显示或者花屏这是新手最常遇到的问题。2.3 Proteus 原理图连接与仿真源配置在 Proteus 8 Professional 中新建工程放置 51 单片机AT89C51 即可、LCD12864、HC-SR04 模块。如果元件库里没有 HC-SR04可以用两个 PULSE 电压源模拟回波信号一个 PULSE 源给 Trig 提供外部触发检测另一个用单次脉冲模拟 Echo 回波。实际操作中更省事的方法是直接把 Echo 接一个按键按一次按键给一个高电平脉冲用于测试程序是否进入测量流程。但这样无法验证测距算法的连续性所以正式仿真时我一般会用一个受控脉冲源周期和脉宽可以手动调模拟不同距离的回波。连接关系如下表这是并行方式的标准接法也是后续代码引脚定义的依据信号LCD12864/模块引脚51 单片机引脚数据线D0-D7P1.0-P1.7寄存器选择RSP2.0读写选择RWP2.1使能EP2.2模式选择PSB接 VCC并口超声波触发TrigP3.5超声波回波EchoP3.4Proteus 中晶振设置为 12MHz双击单片机设置 Clock Frequency 为 12MHz。注意 AT89C51 在 Proteus 里默认没有内部时钟必须设置 External Clock 或要在原理图上放置晶振电路否则程序跑不起来。我通常直接在单片机属性里填 12MHz仿真速度更快。3. 51 单片机 C 语言实现HC-SR04 测距与 LCD12864 显示的完整代码3.1 工程文件组织与全局定义按功能把代码分成两个文件main.c 负责主流程和测距逻辑lcd12864.c 负责液晶驱动。头文件里定义引脚接口和常量。Keil 工程里选择 AT89C51 型号勾选 Hex 输出才能在 Proteus 里加载。下面给出可编译的完整代码为了减少篇幅把 LCD 驱动函数集中在一起。// main.c #include REG51.h #include lcd12864.h sbit Trig P3^5; sbit Echo P3^4; #define SOUND_SPEED 340 // 声速 m/s可做温度补偿 // 50us 粗略延时 void delay_50us(unsigned int t) { unsigned int i; while(t--) { for(i 0; i 57; i); // 12MHz 下约 50us } } // 触发超声波模块 void trigger_pulse(void) { Trig 1; delay_50us(1); // 10us 以上即可 Trig 0; } // 测量回波高电平时间返回单位为 us unsigned int measure_echo_time(void) { unsigned int time_us 0; while(Echo 0); // 等待上升沿 TR0 1; // 启动定时器 0 while(Echo 1); // 等待下降沿 TR0 0; // 停止定时器 time_us (TH0 8) | TL0; TH0 0; TL0 0; return time_us; } // 计算距离单位 cm unsigned int calc_distance(unsigned int echo_us) { unsigned long distance; distance (unsigned long)echo_us * SOUND_SPEED / 100 / 2; return (unsigned int)distance; } void main(void) { unsigned int us; unsigned int cm; char buf[16]; TMOD 0x01; // 定时器0工作方式116位 TH0 0; TL0 0; lcd12864_init(); lcd12864_show_string(1, 0, 超声波测距); while(1) { trigger_pulse(); // 只等待 Echo 上升沿若模块无回波会死等因此要加超时判断 while(Echo 0); us measure_echo_time(); cm calc_distance(us); lcd12864_show_string(2, 0, 距离: ); // 把 cm 转成字符串后拼接单位 sprintf(buf, %d cm , cm); lcd12864_show_string(2, 4, buf); delay_50us(400); // 约 20ms避免频繁刷新闪烁 } }这段代码里TMOD 0x01把定时器 0 设置为 16 位定时模式TR0是启动控制位。measure_echo_time函数里先等待 Echo 为高上升沿到来后立即启动定时器下降沿到达时关闭读取TH0和TL0拼接成 16 位计数值。注意 12MHz 晶振下每个计数单位是 1us所以读到的值直接就是回波时间微秒数。距离公式里的/100是把米换算成厘米/2是因为超声波来回走了两倍距离。这段代码有一个隐患如果探头前没有障碍物Echo 会一直保持低电平while(Echo 0)会死等程序卡死。真实硬件中 HC-SR04 在超过量程时会把 Echo 拉低但若要增加鲁棒性建议在等待上升沿时加入超时判断比如用定时器计数超过 30000约对应 5 米就放弃本次测量。后文会专门讲这个坑。3.2 LCD12864 驱动程序初始化、写命令、写数据// lcd12864.c #include REG51.h #include lcd12864.h sbit RS P2^0; sbit RW P2^1; sbit E P2^2; #define LCD_DATA P1 void lcd_delay(unsigned int t) { unsigned int i; while(t--) for(i 0; i 120; i); } void lcd_check_busy(void) { unsigned char status; RS 0; RW 1; do { E 1; status LCD_DATA; E 0; } while(status 0x80); // 判断 BF 位 } void lcd_write_cmd(unsigned char cmd) { lcd_check_busy(); RS 0; RW 0; LCD_DATA cmd; E 1; lcd_delay(1); E 0; } void lcd_write_data(unsigned char dat) { lcd_check_busy(); RS 1; RW 0; LCD_DATA dat; E 1; lcd_delay(1); E 0; } void lcd12864_init(void) { lcd_delay(100); // 等待内部上电复位 lcd_write_cmd(0x30); // 基本指令集8位数据 lcd_delay(10); lcd_write_cmd(0x30); lcd_delay(10); lcd_write_cmd(0x0C); // 显示开光标关 lcd_delay(10); lcd_write_cmd(0x01); // 清屏 lcd_delay(20); } void lcd12864_show_string(unsigned char row, unsigned char col, unsigned char *str) { unsigned char i; // 基本指令集下 DDRAM 地址第一行 0x80第二行 0x90第三行 0x88第四行 0x98 switch(row) { case 0: lcd_write_cmd(0x80 | col); break; case 1: lcd_write_cmd(0x90 | col); break; case 2: lcd_write_cmd(0x88 | col); break; case 3: lcd_write_cmd(0x98 | col); break; } while(*str) { lcd_write_data(*str); } }这里lcd_check_busy是读忙位函数RS 置 0、RW 置 1读回状态字节最高位为 1 表示 LCD 内部正在处理指令需要等待。要注意的是第一次调用lcd_write_cmd内部会调用lcd_check_busy而 LCD 刚上电时可能尚未初始化所以lcd12864_init里前几个命令之间的延时参数要故意放大前面的lcd_delay(100)用于等待电源稳定。lcd12864_show_string的 row 参数我设计为 0 到 3对应 LCD 的四行col 是汉字列地址每个汉字占两个字节所以传入的 col 需要按实际显示位置调整。例如在第二行第 0 列显示汉字col 取 0在第二行第 4 列显示 ASCII 数字是因为一个汉字占两个 ASCII 字符的位置。3.3 主程序中的定时器初始化与测量流程优化在上面的 main.c 里定时器 0 只用于测量高电平时间但你可能会问主循环里while(Echo 0)如果一直等不到上升沿岂不是把整个系统卡住确实所以更可靠的做法是让定时器同时承担超时计数的职责。可以把定时器初始化后先清零然后进入等待 Echo 的循环中每几十微秒检查一次 TH0/TL0 是否超过上限或者利用定时器中断产生一个 1ms 的系统时基在主循环里用标志位判断超时。推荐第二种方法把定时器中断打开unsigned char timeout_flag 0; unsigned char tick_1ms 0; void Timer0_ISR(void) interrupt 1 { TH0 0xFC; // 重装初值12MHz 下 1ms 定时 TL0 0x18; if(tick_1ms 50) { // 50ms 无回波则超时 timeout_flag 1; tick_1ms 0; } }然后在等待 Echo 上升沿之前先把timeout_flag清零在等待过程中不断检查该标志若超时则跳过本次测量。这种做法把定时器从单纯的闸门计时变成了兼具看门狗作用的计数器代码稳健性会提升一个档次。真实的 HC-SR04 最大量程约 4.5 米回波时间约 26ms所以超时阈值设为 50ms 已经足够。4. 距离计算标定与误差修正从声速补偿到 LCD 刷新策略4.1 为什么 340m/s 不够精确查表法与温度传感器补偿前面代码里把声速固定为 340m/s常温 15℃ 时误差不大但温度每上升 1℃声速增加约 0.6m/s在高温车间或寒冷户外环境测距误差会超过 2%。工程上通常采用两种方法如果系统里已经有一路 ADC 采集模拟温度可以用公式v 331.45 0.6 * TT 为摄氏温度实时计算声速如果不想增加传感器可以做一个「查表 线性插值」的固定补偿表。超声波测距的误差在 Proteus 仿真中更敏感因为仿真里的时钟是理想稳定的误差主要来自定时器溢出和代码中其他中断的干扰。用 340m/s 计算时距离整数厘米的显示结果可能不断在前后两个值之间跳动。比如真实距离 50cm回波时间 2941us计算得到 50.0cm但由于定时器分辨率 1us 的限制测量到的回波时间偶尔是 2942us计算结果就变成 50.1cm 并显示为 50cm所以整数显示反而平稳。若要显示一位小数必须对多次测量取平均。我通常连续测 5 次去掉最大值和最小值再取平均能让结果稳定在 ±1mm 以内。4.2 LCD12864 显示刷新优化的三个必调参数LCD12864 内部没有显存每次写数据都要经过控制器传输指令执行时间以毫秒计算。若主循环每 1ms 刷新一次距离会看到屏幕不断闪烁而且抢占了测距的扫描时间。实际调试中我习惯调三个参数清屏时机不要在显示数字前清屏只更新数字所在的局部区域。上面代码里sprintf(buf, %d cm , cm)后跟了两个空格就是用来覆盖上一轮的残影避免每次清全屏。测量周期超声波模块两次触发之间建议间隔大于 60ms因为声波在空气中传播有固定速度发射脉冲后需要等待回波完全衰减。用 20ms 循环会偶尔测到上一次的余波导致数据乱跳。通常把测量周期设在 100ms 到 200ms 之间人眼看起来既流畅又不跳数。显示位置12864 一屏可以显示 4 行 8 个汉字数字用 ASCII 字符显示时每个数字占 8 个像素宽所以第二行显示距离字符串时要注意总量不能超过 16 个英文字符。建议把结果放在固定行和固定列配合%03d格式化为三位数这样小于 100cm 时也能右对齐视觉上更整齐。4.3 传感器盲区与测量量程的取舍HC-SR04 的盲区通常在 2cm 左右也就是说探头前方小于 2cm 的障碍物根本测不到。这是超声波传感器的固有特性发射的脉冲还没结束回波就已经回来了Echo 引脚只会一直保持高电平无法读出有效时间。仿真中如果按真实的 HC-SR04 模型同样会遇到这个问题。程序里应该对测量结果做一次范围判断if(cm 400) { cm 999; } // 超量程显示 999 if(cm 2) { cm 0; } // 盲区显示 0这里把超量程和盲区都标记为无效值显示端可以区别处理。有人喜欢在盲区显示 0有人显示 0.0这取决于你的产品定义。我的建议是显示 「--」 或空白避免让用户误以为距离真的是 0。在 Proteus 里做仿真时如果回波脉冲宽度设置成超过 30ms就能稳定观察到 999 的超量程处理逻辑如果设置得很窄比如只有 10us则对应测量距离 1.7mm会被盲区判断拦下来此时可以验证边界条件。4.4 定时器重装与中断冲突的坑如果用了定时器 0 的中断做超时管理那么测距时使用TR0去控制定时器启停就会和中断发生冲突每 1ms 进入一次中断你把TH0改为 0xFC 重装值但测距读取的(TH0 8) | TL0会读到中断重装后的计数值导致结果全错。要避免这个问题最好把超时管理和高电平计时分开成两个定时器或者用定时器 1 做高电平计时、定时器 0 做系统时基。51 单片机有定时器 0 和定时器 1建议这个项目里定时器 1 专门测回波定时器 0 留给超时或者干脆用软件计数。防止后续扩展时踩坑我在第 5 章的报警扩展里会采用这种分工方式。5. 进阶应用带距离阈值的超声波报警系统与验证技巧5.1 在现有代码上扩展蜂鸣器报警和阈值设置做课程设计或小型安防装置时通常不仅需要显示距离还要在障碍物进入警戒范围时报警。扩展方法是在主循环里加上阈值比较逻辑阈值可以用两个按键调整也可以通过宏定义固定。由于 LCD12864 还有两行空余正好可以用来显示当前报警阈值和状态。// alarm.c sbit Buzzer P3^7; sbit KeyUp P3^6; sbit KeyDown P3^5; unsigned int alarm_threshold 30; // 默认 30cm void check_keys(void) { if(!KeyUp) { delay_ms(20); if(!KeyUp) { if(alarm_threshold 400) alarm_threshold 5; while(!KeyUp); // 等待松开 } } if(!KeyDown) { delay_ms(20); if(!KeyDown) { if(alarm_threshold 5) alarm_threshold - 5; while(!KeyDown); } } } void update_alarm_output(unsigned int cm) { if(cm alarm_threshold cm 2) { Buzzer 0; // 低电平驱动蜂鸣器需配合三极管电路 } else { Buzzer 1; } }这里按键消抖用了最简单的延时判断适合仿真环境。真实硬件中如果按键连接在 P3.6 和 P3.5需要确认它们没有和其他外设复用。报警阈值变量是无符号整型加减步长 5cm并把范围限制在盲区 5cm 到量程 400cm 之间。主循环里每次测距后调用update_alarm_output(cm)即可。要注意蜂鸣器的驱动电流很大Proteus 里可以直接驱动但真实电路必须用 NPN 三极管或 ULN2003否则单片机会被拉复位。5.2 用 Proteus 虚拟信号模拟回波的验证方法在 Proteus 里验证测距算法时建议在 Echo 引脚上接一个电压源设置成「单次脉冲或周期脉冲」。最实用的做法是放置一个DPULSE源把初始状态设为低电平脉冲宽度设为2 * 距离 / 声速 * 1000000单位是秒。例如要模拟 50cm 的距离回波时间应为 50 * 2 / 34000 ≈ 2941us也就是 0.002941 秒。把脉冲宽度填成 2.941ms 并设置周期为 200ms程序读到的距离就会稳定显示在 50cm 左右。通过改变脉冲宽度可以验证不同距离下的显示和报警动作。注意Proteus 中的DPULSE源如果直接和 51 单片机引脚相连必须确保它的高电平电压为 5V且输出阻抗不能太大否则引脚检测不到高电平。另外超声波模块模型虽然方便但它只模拟物理行为不会提供内部回波 Pulse 信号的关断时间设置如果你用的是 Proteus 自带的 HC-SR04 模型最好到模型属性里确认它支持的触发脉宽下限否则可能出现 Trig 给了但 Echo 没有反应的情况。5.3 验证 LCD 显示时序的观察技巧仿真运行时把 LCD12864 的 Enable 引脚接到虚拟示波器通道上能看到每个指令周期都会有高电平脉冲。如果显示乱码或花屏优先检查初始化序列里 0x30 命令前的延时是否足够。Proteus 的 LCD12864 模型对时序要求比真实屏宽松所以仿真通过并不代表真实硬件能过反过来如果你已经烧录到真实硬件发现显示不刷新可以试着把lcd_delay函数的循环次数加倍因为 ST7920 在真实硬件上执行清屏指令耗时接近 2ms而仿真模型往往瞬间完成。调整时不要盲目减小延时参数优先保证稳定。5.4 小数点显示与字符串处理的边界情况显示带一位小数的距离时需要自己控制整数和小数拆分unsigned char int_part, dec_part; int_part cm / 10; dec_part cm % 10; sprintf(buf, %d.%d cm, int_part, dec_part);但这种做法要求前面的计算直接把距离结果放大 10 倍再存例如用 毫米 为最小单位。建议把所有变量统一成「毫米」显示时再拼字符串。例如distance_mm echo_us * 340 / 10000因为 340m/s 等于 340mm/ms而 echo_us 是微秒换算后echo_us * 340 / 1000得到毫米。注意整数溢出echo_us最大 30000乘 340 是 10200000超出 16 位必须用unsigned long临时变量最后再截断。这个细节对以毫米为单位显示结果特别重要。本文还有配套的精品资源点击获取
返回列表