ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于51单片机的视力保护仪:超声波测距与光敏检测实现

基于51单片机的视力保护仪:超声波测距与光敏检测实现 简介这是一套基于51单片机的视力保护仪毕业设计完整方案面向电子信息类学生与单片机爱好者涵盖超声波测距、光强检测、定时提醒等核心功能。系统以STC89C52RC为主控配合LCD1602显示、ADC0832采集光强、HC-SR04测量距离、DS1302记录使用时长可实现坐姿距离预警、环境光线提示、连续用眼30分钟休息提醒并扩展了声光报警与语音提示。资源共61个文件压缩包约4.69MB包含Keil工程与C语言源码、原理图与PCB设计、任务书、开题报告、参考论文、芯片手册及元件清单可支撑从仿真调试到实物制作的全过程。已有937人学习下载对于需要快速搭建毕业设计框架、理解传感器联动与单片机编程的读者有较高参考价值。1. 视距、光照、用眼时长51单片机视力保护仪到底在保护什么视力保护仪这个名字听起来像医疗设备拆开看就是一个多传感器合一的桌面小盒子。超声波传感器盯住眼睛到书本的距离光敏电阻感知环境亮度定时器累计连续用眼时长任一超标就驱动蜂鸣器和显示屏提醒坐直、开灯或休息。难点不在传感器本身而在于51单片机资源有限测距要精确捕获回波脉冲宽度光强读数要参与分档判断主循环还要响应按键和显示刷新不能因为一次超声波等待就卡死整机逻辑。这类项目用51单片机不是勉强够用而是刚好合适。STC89C52或AT89S52带两个16位定时器和三十多个IO口外部只需扩展一个8位ADC成本能压在二三十元以内。适合正在做课程设计的学生也适合想给家里书桌加一个防近视提醒的嵌入式爱好者。下面从硬件选型、软件状态机、阈值标定三条线展开。2. 从传感器选型到最小电路51单片机视力保护仪的硬件重心2.1 测距方案对比超声波、红外反射与激光的取舍首先确定测距传感器。视力保护仪要求量程在30cm左右测距方向基本固定更新周期几百毫秒就够所以可选项很明确。超声波HC-SR04是多数课程设计里的首选量程2cm到400cm成本低回波是高电平脉宽51单片机用定时器就能测量不需要额外ADC。它在斜面反射时读数会不稳定但保护仪的传感器正对使用者放置这个缺点影响不大。红外反射传感器GP2Y0A21输出模拟电压量程10cm到80cm短距离线性度比超声波好但要占用一个ADC通道成本翻两三倍。激光TOF精度高但51去模拟I2C读数据开发量不划算。还有一个常见误区是拿人体热释电传感器做测距它只能判断有没有人出现在扇形区域给不出具体距离保护仪无法据此区分低头和坐直。测距方案量程成本输出类型在视力保护仪里的结论超声波 HC-SR042-400cm5-8元高电平脉宽首选逻辑和倒车雷达测距同源红外反射 GP2Y0A2110-80cm15-25元模拟电压可用但占用ADC且价格偏高激光 TOF4cm-2m50元以上I2C高配版本才值得考虑人体热释电无距离值4-6元开关量不适用只能感知有人2.2 核心器件清单与选型理由主控推荐STC89C52RC内部不带ADC光敏检测需要外接ADC0804。如果换成STC12C5A60S2内部集成10位ADC可以省掉ADC0804主控价格差几块钱但代码兼容性稍差。学校课程设计若明确要求51单片机STC89C52更容易交代。显示模块用LCD1602四线接法能省四个IOOLED的性价比也不错但51没有硬件I2C软件模拟时序容易在读写地址上卡住。光敏检测我一般用5528光敏电阻配10kΩ电阻做分压网络。5528的暗阻接近1MΩ亮阻在10kΩ到20kΩ之间分压点电压随照度变化刚好落在ADC0804的0-5V量程里。这里不建议用电位器调到固定阈值不同房间的灯光类型、桌面反射都会让光敏读数差几十个AD值程序里应该做分档而不是做单点判断。蜂鸣器选有源蜂鸣器不需要生成频率单片机给出高电平或低电平就能响代码里少占一个定时器。按键用三个独立按键就够模式切换、加阈值、减阈值。矩阵键盘在这种只有三个功能键的场景里是过度设计接线和消抖都会变复杂。2.3 光敏分压计算与ADC0804接线光敏电阻和10kΩ电阻串联在5V和GND之间分压点接ADC0804的VIN。ADC0804是8位并行ADCVREF/2引脚悬空时参考电压为5V分辨率约19.5mV。房间照度300Lux时光敏电阻约10kΩ分压点电压2.5V对应AD值128照度降到100Lux时光敏电阻约30kΩ分压点电压1.25VAD值约64。这个换算关系不用写进程序但标定阈值时能帮你在代码和实测之间快速对应。接线时注意ADC0804的数据口直接接P0。51的P0口是开漏输出读外部数据前必须先把P0写0xFF打开上拉否则高电平读不进来。为了让电平更稳定可以在P0总线上外接4.7kΩ排阻到5V。ADC0804的并行读时序和51访问外部RAM十分相似RD、WR都可以接P3.6和P3.7INTR接P3.2转换完成时由高拉低。2.4 最小电路接线总览与初始化骨架下面这张接线表可以直接抄走默认使用STC89C52RC、LCD1602四线模式、ADC0804并行读取模块引脚连接说明HC-SR04Trig→P2.0Echo→P2.1Echo是高电平脉宽P2为准双向口可直读ADC0804DB0-DB7→P0WR→P3.6RD→P3.7INTR→P3.2CS直接接地LCD1602RS→P1.0E→P1.2DB4-DB7→P1.4-P1.7RW接地只写不读有源蜂鸣器正极→P2.2负极→GND驱动能力不够就加8550三极管独立按键P2.3/P2.4/P2.5按下接地内部或外部上拉注意超声波模块的VCC必须接5V接3.3V时Echo高电平电压不够可能出现读数偏小甚至一直为0。初始化骨架如下重点是Timer0工作在16位定时器模式用来捕获Echo脉宽#include reg52.h sbit TRIG P2^0; sbit ECHO P2^1; sbit BUZZ P2^2; sbit ADC_WR P3^6; sbit ADC_RD P3^7; sbit ADC_INTR P3^2; void timer0_init(void) { TMOD 0xF0; // 只修改Timer0配置 TMOD | 0x01; // Timer0, 16位定时器模式 TH0 0x00; // 计数初值清零 TL0 0x00; ET0 1; // 允许Timer0中断 EA 1; // 开总中断 }TMOD寄存器的高4位留给Timer1用先与后或的方式改低4位避免把Timer1配置冲掉。计数初值清零后TR0置1开始从0累加12MHz晶振下每1us计数一次16位计数器最大可计65535us而HC-SR04的Echo高电平最长不超过20ms测一次不会溢出。中断里暂时只做速率基准后面测距代码会直接查询TR0和TH0/TL0不进中断减少切换开销。3. 用定时器与状态机实现测距、光强与用眼时长判定3.1 主循环状态机为什么不能把所有逻辑都堆在while里视力保护仪里的三个关键动作测距离、读光强、计时看起来是顺序执行实际不能写成一长串函数调用。原因在于超声波测距函数里有一个等待Echo高电平结束的循环如果前方没有反射物最坏会等20ms。主循环里还有按键扫描和LCD刷新任何一个传感器函数把这20ms占死按键就会卡顿。我一般把整个系统做成一个简单状态机每500ms进入一次测量流程其余时间留给按键、显示和中断计时。状态切换用标志位而不是delay这个项目实时性要求不高但代码结构往这个方向改后续加传感器或换成其他主控时都能直接套用。typedef enum { ST_IDLE, ST_MEASURE, ST_JUDGE, ST_ALERT } sys_state_t; volatile unsigned char flag_500ms 0; void main(void) { sys_state_t state ST_IDLE; timer0_init(); lcd_init(); while (1) { if (flag_500ms) { flag_500ms 0; state ST_MEASURE; } switch (state) { case ST_MEASURE: dist_cm read_ultrasonic_cm(); light_val read_adc0804(); state ST_JUDGE; break; case ST_JUDGE: judge_conditions(dist_cm, light_val); state ST_ALERT; break; case ST_ALERT: update_alert(); state ST_IDLE; break; default: state ST_IDLE; break; } handle_key(); // 按键轮询短延时消抖 update_display(); // LCD刷新频率自己控制 } }关键点是flag_500ms在Timer0中断里置位主循环检测到置位后才开始测量。测量和判断状态不会阻塞太久从ST_MEASURE到ST_JUDGE之间没有插入delay整个测量流程在几十毫秒内完成。按键和显示放在switch外面意味着即使传感器读不到回波按键仍然响应这是状态机带来的最直接收益。3.2 超声波测距驱动脉宽捕获与参数换算HC-SR04的时序是标准三段式Trig先拉高至少10us模块内部发出8个40kHz脉冲然后Echo输出高电平高电平持续时间为超声波往返时间。距离等于高电平时间乘声速再除以2常温下用微秒数直接除以58得到厘米数这是一个工程近似式误差在1cm以内。unsigned int read_ultrasonic_cm(void) { unsigned int cnt; EA 0; // 关闭总中断避免回波捕获被打断 TRIG 1; delay_us(12); TRIG 0; while (!ECHO); // 等待回波高电平开始 TH0 0x00; TL0 0x00; TR0 1; // 开始计数 while (ECHO); // 等待高电平结束 TR0 0; cnt (TH0 8) | TL0; // 高电平持续时间单位us EA 1; return cnt / 58; // us转厘米 }3.2.1 回波超时保护上面代码假设Echo总会拉高并拉低。实际中如果传感器前面没有任何遮挡物Echo保持高电平的时间可能很长程序会卡在第二个while里面。稳妥的做法是加一个超时退出计数超过40000us就强制置0并退出把这次结果标记为无效距离交给上层做“未检测到目标”处理。对于视力保护仪无效距离可以直接当作正常距离不触发报警否则人会对着空中一直误报。3.3 光敏ADC0804读取与亮度分档ADC0804的时序是WR拉低启动转换INTR变低表示转换完成之后RD拉低把8位数据放到P0。视力保护仪的亮度变化很慢每个测量周期读一次即可不需要定时采样。要注意如果没有等待INTR就立刻RD读到的可能是上一次转换结果。unsigned char read_adc0804(void) { unsigned char val; ADC_WR 0; // 启动转换 ADC_WR 1; while (ADC_INTR); // 等待转换完成INTR由高变低 P0 0xFF; // P0开漏输出读前先上拉 ADC_RD 0; // 读使能 val P0; ADC_RD 1; return val; }P00xFF不可省略。如果外部在P0上挂了4.7kΩ排阻这行仍然建议保留相当于把总线释放。ADC值0对应0V255对应5V直接用数值分档不需要做浮点运算if (light_val 90) { light_level LOW_LIGHT; // 接近昏暗需要开灯 } else if (light_val 150) { light_level NORMAL_LIGHT; // 正常台灯环境 } else { light_level HIGH_LIGHT; // 白天或强光 }90和150不是固定值对应的照度取决于电阻搭配。分压电阻换成20kΩ后同样照度下分压点电压升高AD值整体上移。建议先把一组实测AD值通过串口打印到电脑观察白天和晚上台灯下的读数区间再把分档边界取在区间中位。3.4 用眼时长计时与休息提醒用眼时长不能靠主循环变量自加因为主循环每次执行耗时不同加上超声波等待时间累计误差会越来越大。正确做法是在Timer0中断里维护一个1ms计数累计到1000就增加一秒同时这个秒数只在工作状态下累加。volatile unsigned int work_sec 0; void timer0_isr(void) interrupt 1 { static unsigned int ms 0; ms; if (ms 1000) { ms 0; if (work_state STATE_WORKING) { work_sec; } } }work_state由judge函数更新距离阈值正常且光照正常时置为STATE_WORKING连续工作45分钟后置为STATE_REST蜂鸣器响LCD显示休息倒计时。45分钟可以通过按键调整我一般做成10到60分钟可调步进5分钟存到全局变量里。这个计时必须在状态机判定之后更新否则一次超声波测量阻塞20ms每秒会产生最多20ms的漂移。4. 视力保护仪的阈值标定、抗干扰与实测修正4.1 距离阈值标定根据传感器安装高度和坐姿范围超声波的读数只在传感器正对着反射物时才稳定所以安装位置决定了阈值。常见做法是传感器放在桌面靠使用者一侧朝正前方水平照射距离正常坐姿的胸口或书本约30到45cm。垂直朝下照射不是好主意桌面反射会比人体提前返回距离读数会稳定在一个固定伪值上。标定分三步。先让使用者保持标准坐姿记录LCD上稳定显示的距离值再身体前倾到已经太近的位置记录第二个值把报警阈值取在两个值中间偏标准坐姿一侧的位置。比如标准坐姿45cm前倾后30cm阈值取35cm。阈值定死为30cm只在摆拍场景里好看实际书桌高低不同、使用者身高不同必须现场修正。按键操作行为步进模式键长按进入距离阈值调整无加键单击阈值1cm1cm减键单击阈值-1cm1cm模式键短按保存并退出无保存后写进AT24C02或Flash避免每次开机重新标定。如果只是临时演示可以先用全局变量顶着但断电后阈值会丢答辩时容易穿帮。4.2 光敏阈值不能写死昼夜落差带来的误报光敏电阻的读数受测试环境影响很大阳光直射时可能超过220晚上只开显示器时可能不到30如果代码里写死一个阈值白天会误报光线不足晚上又会误报光线正常。我在代码里把亮度分成三档再用迟滞消除边界抖动。迟滞的意思是进入低亮度和退出低亮度使用两个不同阈值。光照读数从高到低降到90以下才报警从低到高则要升到100以上才取消报警。这5到10个AD值的回差能把光敏电阻在边界附近跳动导致的蜂鸣器反复开关压掉对应到照度约20Lux到40Lux的变化在普通LED台灯下已经足够明显。4.3 回波毛刺过滤连续采样与中值滤波超声波在桌面边缘、书本页角上会发生不规则反射偶尔一个距离值会跳高或跳低十几厘米。如果只用一次测量结果判定保护仪会间歇性误报。最简单的做法是连续采5次去掉最大值和最小值取中间三个数的平均值。unsigned int filter_distance(void) { unsigned char i, j; unsigned int buf[5], tmp; for (i 0; i 5; i) { buf[i] read_ultrasonic_cm(); delay_ms(30); // 让探头余波消散 } for (i 0; i 4; i) { for (j 0; j 4 - i; j) { if (buf[j] buf[j 1]) { tmp buf[j]; buf[j] buf[j 1]; buf[j 1] tmp; } } } return (buf[1] buf[2] buf[3]) / 3; }5次采样间隔30ms整个函数耗时约150ms加上超声波一次测距最多20ms总耗时在300ms以内对比500ms的测量周期还有余量。开始行的delay_ms(30)不是随便加的连续触发HC-SR04间隔太短时探头余波会叠加到下一次回波上距离读数整体偏大。排序后buf[0]是最小值buf[4]是最大值中间三个取平均比单纯把5个数取平均更抗脉冲噪声。4.4 实测流程与常见问题修正联调时建议按这个顺序走先不接超声波只让LCD显示ADC读数确认光敏电路输出正常再接上超声波用一本书挡住传感器看距离读数是否线性变化最后才接蜂鸣器做整体判定。问题分层排查避免出现报警不响时同时怀疑距离和光线两个链路。现象可能原因处理方式距离一直显示0Trig没拉到高电平或Echo接错用示波器测Echo确认有脉冲距离固定偏大2-4cm声速受温度影响加温度修正或减掉固定偏移量ADC值一直255或0P0未上拉或WR时序错误检查P0口排阻按读写时序调蜂鸣器不响驱动电流不足用8550三极管基极串1kΩ电阻4.4.1 Proteus仿真注意事项如果先在Proteus里仿真建议用信号源代替HC-SR04模型因为仿真模型里的回波参数难调而且Echo时序受仿真步长影响读数会整体偏大。用信号源生成一个高电平脉冲宽度对应目标距离反而更容易验证测距函数和状态机逻辑。实物联调时再把信号源换成真实模块。到这里这套系统的核心已经能跑了。再往前一步就是把提醒改成主动调节让保护仪不只是报警而是在光线偏暗时自动调亮台灯。5. 进阶PWM调光、EEPROM记录与计时精度小技巧5.1 用定时器产生PWM背光调节视力保护仪如果只做报警体验偏生硬。常见做法是让装置额外驱动一盏LED台灯光线不足时自动提高占空比亮度足够时降低。用51单片机产生PWM不需要硬件PWM模块Timer1中断里维护一个0到100的计数器比较占空比变量即可。原理和用51驱动舵机时生成20ms周期PWM一致只是这里周期更短、占空比无级可调。void timer1_isr(void) interrupt 3 { static unsigned char cnt 0; cnt; if (cnt 100) cnt 0; LED_PWM (cnt pwm_duty) ? 1 : 0; }pwm_duty是全局unsigned char变量取值范围0到100在光线分档函数里修改。光敏读数低于90时pwm_duty80中档时pwm_duty45亮度充足时pwm_duty20。这样保护仪就从提醒器变成了一个小型闭环调光系统逻辑类似温控风扇看着温度调占空比只是控制对象换成了光强。5.2 用AT24C02记录用眼数据课程设计答辩时老师通常会问数据断电会不会丢。把每天的累计用眼时长写进AT24C02下次开机显示昨日用眼时间是一个信息量足够又不太费IO的功能。AT24C02使用模拟I2CSCL和SDA各占一个IO。void eeprom_write_minutes(unsigned int minute) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址写操作 I2C_SendByte(0x10); // 数据地址 I2C_SendByte(minute 8); // 高位 I2C_SendByte(minute 0xFF); // 低位 I2C_Stop(); delay_ms(10); // 等待内部写周期约5ms }I2C_Start和I2C_SendByte是模拟时序SCL低电平期间改变SDA状态送数据SCL高电平期间SDA的下跳变代表Start、上跳变代表Stop。写完后必须延时10ms再读否则前一次写周期没结束读回来的数据是0xFF。读取时从地址0x10连续读两个字节再拼成unsigned int即可。5.3 硬定时器优于软件计数用时间戳代替秒累加最后一个值得记住的经验和排错无关却最影响长期稳定性用眼时长不要用work_sec这种累加值做判定。主循环里超声波的等待、LCD写入时序都会影响中断里的秒计数虽然误差只有几毫秒但一天下来可能慢几十秒。更好的做法是维护一个始终运行的free_run_ms它只在Timer0中断里自增永不复位。需要判定已连续工作45分钟时记录进入工作状态时的起始时间戳用当前时间戳减起始时间戳超过2700000就提醒。超声波测距关闭中断的20ms只会让free_run_ms慢20ms而不会改变时间戳之间的差值。unsigned int start_stamp free_run_ms; if (free_run_ms - start_stamp 45UL * 60 * 1000) { start_stamp free_run_ms; // 重新开始一轮 }把时间判定建立在差值上比在中断里做次数累加更稳。下次要给这套系统加自动关机或午休模式时直接把相同逻辑挂到free_run_ms时间戳上即可不用再为定时误差单独做校准。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进