简介:一份基于STC89C51/52单片机的坐姿矫正系统本科毕业设计文档,面向电子信息、自动化及相关专业学生,以及准备单片机课程设计和毕业设计的读者,可用于了解环境光检测、人体感应、测距报警与定时提醒如何整合成完整方案。压缩包内共1个doc文件,约295KB,内容涵盖摘要、总体方案设计、系统硬件设计、单片机最小电路、显示模块、光线监测和超声波测距等章节。已有793人学习或浏览,具备一定参考热度。文档围绕自动与手动两种模式展开,说明光敏电阻配合模数转换判断光线、人体感应模块检测是否有人、距离过近时蜂鸣器报警、人离开后延时关灯,以及LCD1602显示和按键设定学习时长等功能,可作为毕业设计写作、系统框架搭建与功能模块梳理的参考资料。
1. 从一份51单片机毕业设计说起:坐姿矫正系统真正要解决的是哪三个量
自习室里最常见的画面:人趴在桌上,脸离书不到二十厘米,头顶的灯还没开。这套基于 STC89C52 的坐姿矫正系统,抓的就是这三个量——人脸到桌面的距离、环境光线强弱、连续伏案时长。它用光敏电阻加 ADC0832 测光,用超声波模块测距,用定时器做 45 分钟久坐提醒,LCD1602 显示当前状态,蜂鸣器分级报警,再配一个人体感应模块做在座检测和台灯联动。整套硬件成本压得很低,代码量对 51 单片机毕业设计来说刚好可控,传感器种类却覆盖了模拟量采集、时序驱动、中断计数、人机交互四类典型考点,所以每年都有人选这个题目。这篇笔记按“选型—接线—代码—排错—标定”的顺序拆一遍,新手能照着焊,熟手能直接看参数边界。
2. 硬件选型不纠结:STC89C52、ADC0832与LCD1602的取舍边界
2.1 主控为什么停在STC89C52,而不是往上换STM32
这个题目最容易翻车的地方不是代码写不出来,而是选型阶段被“性能更强”带偏。STM32F103 确实资源多、主频高,但它带来的连锁成本是:3.3V 电平与 LCD1602、ADC0832、超声波模块的 5V 电平之间要做转换,下载要额外配调试器,工程要配时钟树和外设库。对一个以“把传感器链路跑通、把时序讲清楚”为目标的本科设计来说,这些工作量不产生任何答辩加分,反而挤压调试时间。
STC89C52 的优势很具体:DIP40 封装可以直接插在洞洞板或锁紧座上,5V 供电与常见模块电平一致,32 个 IO 口分到四个端口上,P0 做数据总线、P2 做控制线、P1 挂传感器、P3 留给按键和串口,分配起来不打架。8KB Flash 装这套程序绰绰有余,3 个定时器刚好一个做系统节拍、一个做超声波脉宽计数、一个留作串口波特率。代价也要说清楚:P0 口内部没有上拉,作为 LCD1602 的 8 位数据口时必须外接 10k 排阻,否则屏幕会出现时好时坏的乱码;芯片没有硬件 ADC,所以光敏电阻必须外挂 ADC0832。
我一般会先画一张引脚分配表再动手焊,表定不下来就不要碰烙铁。下面这张表是这套系统里比较稳的一种分法,后面所有代码都按它写:
| 功能 | 单片机引脚 | 备注 |
|---|---|---|
| LCD1602 RS / RW / E | P2.0 / P2.1 / P2.2 | RW 用 IO 控制,保留读忙能力 |
| LCD1602 D0~D7 | P0.0~P0.7 | P0 必须外接 10k 排阻上拉 |
| ADC0832 CS / CLK / DI / DO | P1.0 / P1.1 / P1.2 / P1.3 | DI 与 DO 也可共线,省一个 IO |
| 超声波 TRIG / ECHO | P1.4 / P1.5 | ECHO 输出 5V 电平,可直连 |
| 蜂鸣器 | P1.6 | 低电平导通,经三极管驱动 |
| 台灯继电器 | P1.7 | 高电平吸合,经三极管驱动 |
| 按键 加 / 减 / 模式 | P3.2 / P3.3 / P3.4 | 低电平有效,内部弱上拉 |
| 人体感应模块输出 | P3.5 | 高电平表示检测到人 |
这张表的价值在于把“哪些口能做中断、哪些口必须上拉、哪些口要留给下载和串口”提前锁死。P3.2 和 P3.3 同时是外部中断口,如果按键不够用,可以把模式键挪到 P3.6,别去占 P3.0 和 P3.1,那两个是串口下载和调试的口子,占了以后程序烧不进去会让人怀疑芯片坏了。
2.2 三个测量量对应三种传感链路,量程和分辨率要分开算
光线的测量链路是“光敏电阻分压—ADC0832—单片机”。光敏电阻选常见的 GL5528 一类,亮阻在十千欧量级,暗阻到兆欧量级,和一只 10k 电阻串联分压,中点接 ADC0832 的模拟输入。ADC0832 是 8 位、双通道、串行输出的模数转换器,参考电压直接取 5V,所以最小分辨率为 5V/256≈19.5mV。这个分辨率对“判断光线够不够”完全够用,但要注意它只有 8 位,别指望拿它做精确照度计,标定时只需要记住“多少 ADC 值对应我觉得该开灯”就够了。
距离的测量链路原文写的是超声波,摘要里又提到红外光电测距,这两种在实物上都能做,但边界不同。超声波模块常见量程 3cm 到 90cm,近距离有盲区,远距离误差会到 1~2cm,好处是不受光线影响、对桌面和人体这类大平面反射稳定。红外光电测距在 10~80cm 段线性更好,但输出是模拟电压,又得多占一路 ADC,而且深色衣服反射弱会误判。所以我一般建议以超声波为主测距离,红外只作为备选方案写进论文的方案论证章节,实物不一定要焊两套。
在座检测用人体感应模块,输出就是开关量,单片机只读高低电平。它的价值不在于精度,而在于解决“人走了灯还亮着”的浪费,以及“人没来但超声波测到椅子靠背”的误报。模块后面通常有一个电位器调延时,调到 5~10 秒比较合适,太短会让人稍微一动就重新计时,太长则人走了灯迟迟不灭。
| 测量量 | 器件 | 输出形式 | 单片机接口 | 典型判定阈值 |
|---|---|---|---|---|
| 环境光 | 光敏电阻 + ADC0832 | 8 位数字量 0~255 | P1.0~P1.3 软件时序 | ADC 值低于 80 判为偏暗 |
| 人脸距离 | 超声波模块 | 脉宽,1us 对应约 0.017cm | P1.4 触发、P1.5 捕获 | 小于 30cm 判为过近 |
| 是否在座 | 人体感应模块 | 高/低电平 | P3.5 普通 IO | 高电平持续 3 秒判为在座 |
| 伏案时长 | 单片机定时器 | 内部计数 | T0 中断 | 45 分钟触发休息提醒 |
阈值那一列不是拍脑袋写的,后文第 6 章会给标定方法。这里先记住一个原则:阈值必须写成宏定义,放在程序头部,不要散落在判断语句里,否则标定一次要改十几个地方,改漏一个就会出现“光线报警了但距离不报警”的怪现象。
2.3 显示与报警的驱动边界:LCD1602 占多少 IO,蜂鸣器要不要三极管
LCD1602 是 16 字乘 2 行的字符型液晶,内部字库带 192 个字符,显示数字、字母和几个自定义符号足够。它支持 8 位并口和 4 位并口两种接法,8 位并口占 RS、RW、E 加 8 根数据线共 11 个 IO,4 位并口只占 7 个。这套系统 IO 不紧张,用 8 位并口更省事,初始化序列也简单。第 3 脚 VEE 是对比度调节,接一只 10k 电位器到地,调得太高会出现整屏黑块,调得太低则字符淡到看不见,这是新手最常见的“屏幕坏了”误判。
蜂鸣器分有源和无源,原文用的是无源压电式,这意味着单片机必须输出方波才能响,频率一般落在 2~4kHz。无源的好处是可以用不同频率区分报警类型:姿势过近用 2kHz 快滴,光线偏暗用 1kHz 慢滴,久坐提醒用持续长鸣。代价是必须用定时器或软件延时产生方波,不能用一句BEEP = 0完事。
无论有源无源,都不建议把蜂鸣器直接挂在 IO 口上。51 单个 IO 的灌电流能力有限,蜂鸣器工作电流动辄二三十毫安,直连轻则声音发虚,重则把口线拉坏。常见做法是用一只 S8050 三极管做低边开关,基极串 1k~4.7k 电阻,单片机输出高电平时三极管导通、蜂鸣器响。台灯那一路如果用小继电器,线圈两端必须反并一只 1N4148 续流二极管,否则继电器断开的瞬间反电动势会把单片机打得复位,这个现象在后面避坑章节还会展开。
3. 从原理图到洞洞板:最小系统与四个外设模块的接线顺序
3.1 最小系统三件套:晶振、复位、电源去耦先焊对
最小系统决定整块板子能不能跑起来,顺序上必须先焊它、先上电测电压,再往外挂模块。STC89C52 的最小系统只有三部分:12MHz 晶振加两只 22pF 瓷片电容构成时钟电路,10uF 电解电容加 10k 电阻构成上电复位电路,电源引脚旁挂 0.1uF 去耦电容。EA 引脚必须接 VCC,否则单片机会从外部存储器取指,表现就是程序烧进去了但完全不跑。
晶振和电容要尽量靠近芯片的 XTAL1、XTAL2 引脚,走线越短越好。洞洞板上很多人把晶振焊在板子边缘,引两根长线过来,结果串口通信时好时坏,查半天以为是波特率算错,实际是时钟被干扰了。复位电路里的电解电容注意极性,负极接复位引脚、正极接 VCC 的那种接法适用于高电平复位,51 系列是高电平复位,按键并联在电容两端,按下时把复位脚拉高。
焊完这一步先不要插芯片,用万用表测三个点:VCC 与 GND 之间电阻不能接近零,复位脚上电后应该是接近 5V,EA 脚必须是 5V。三点都对再把芯片插上去,用 STC-ISP 一类工具烧一个闪灯程序验证最小系统。这一步偷懒,后面所有模块的问题都会混在一起,排错成本翻倍。
3.2 LCD1602 的 8 位并口、对比度电位器和背光限流
LCD1602 的 16 个引脚里,1 脚 GND、2 脚 VCC、3 脚 VEE 对比度、4 脚 RS、5 脚 RW、6 脚 E、7~14 脚 D0~D7、15 脚背光正、16 脚背光负。背光那一路最好串一只 100Ω 左右的限流电阻,直接接 5V 虽然大多数模块能亮,但长期跑电流偏大,模块发热会缩短寿命。
数据口接 P0 时,别忘了 10k 排阻上拉。P0 口内部是开漏结构,不加上拉输出高电平时是浮空的,LCD1602 读到的高电平不确定,屏幕就会出现“有的字符正常、有的字符变成黑块”的随机故障。这个故障最迷惑人的地方在于它时好时坏,用手碰一下板子可能又正常了,很容易被当成接触不良。
RW 脚建议接 IO 而不是直接接地。接地确实能简化程序,不用读忙标志,但代价是每次写命令写数据都得靠延时等内部操作完成,延时给短了就会丢命令。用 IO 控制 RW,就可以读忙标志位,程序更稳,也方便后面讲 LCD 驱动时把时序说清楚。初始化序列按 0x38(8 位两行 5x7 点阵)、0x0C(开显示关光标)、0x06(写一个字符后地址加一)、0x01(清屏)的顺序发,每条命令之间留 2ms 以上。
3.3 蜂鸣器、继电器与人体感应模块的接口电路怎么留余量
蜂鸣器驱动电路里,三极管基极电阻的取值可以按最坏情况算:单片机高电平输出按 4.5V 算,S8050 的 Vbe 约 0.7V,蜂鸣器电流按 30mA、三极管放大倍数按 100 算,基极电流需要 0.3mA,电阻上限约 12k。实际取 1k~4.7k 都行,取小一点让三极管进入饱和区,压降更小、声音更响。发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接 5V,这就是典型的低边开关。
继电器那一路同理,但要多一只续流二极管。线圈断电瞬间会产生几十伏的反电动势,方向是上负下正,反并一只 1N4148 给它一条泄放回路。如果台灯改用 MOS 管或可控硅,电路会简单一些,但对本科设计来说继电器更直观,吸合断开的声音本身就是一个状态反馈。
人体感应模块的输出电平有 3.3V 和 5V 两种版本,买的时候要确认是 5V 供电、输出高电平接近 5V 的型号,否则 3.3V 高电平进 51 的 IO 可能识别不到。模块的延时旋钮和灵敏度旋钮出厂位置不一定合适,装到壳子里之前先在外面调好:延时调到人离开后 5~10 秒断开,灵敏度调到坐在正常位置能稳定触发、稍微侧身不丢目标。
接线全部完成后,先不插单片机,用稳压电源给整板供电,测每个模块的 VCC 脚是不是 5V,测继电器线圈两端电阻是不是几十欧到一百欧量级,测光敏分压中点电压用手遮住光敏电阻时是否明显变化。这几步做完再上芯片,能省掉很多“到底是硬件还是软件”的争论。
4. 软件骨架落地:定时器、ADC0832时序与LCD1602驱动的可抄代码
4.1 定时器 T0 做 45 分钟倒计时,中断里只置标志
12MHz 晶振、12T 模式下机器周期是 1us,定时器方式 1 是 16 位计数,最大定时 65.536ms。取 50ms 中断一次,初值就是 65536-50000=15536,即十六进制 0x3CB0。50ms 中断 900 次是 45 秒,54000 次才是 45 分钟,所以计数变量要用unsigned int,不能用unsigned char。这一步算错的人不少,现象是设了 45 分钟,结果 45 秒就响,还以为是按键消抖把数值改坏了。
中断服务函数里只做三件事:重装初值、给 50ms 计数加一、到点置一个time_up标志。LCD 刷新、蜂鸣器方波、按键扫描全部放在主循环里做。中断里干太多活会拉长中断响应时间,超声波测距的脉宽计数就会丢精度,表现是距离读数偶尔跳一个大值。
#include <reg52.h> #define T0_50MS 15536 // 12MHz, 12T, 50ms #define CNT_45MIN 54000U // 50ms * 54000 = 2700s = 45min volatile unsigned int tick_50ms = 0; volatile bit time_up = 0; unsigned int study_min = 45; // 可通过按键调整的学习时长 void timer0_init(void) { TMOD &= 0xF0; // 保留T1配置 TMOD |= 0x01; // T0 方式1,16位 TH0 = T0_50MS >> 8; TL0 = T0_50MS & 0xFF; ET0 = 1; // 开T0中断 EA = 1; TR0 = 1; } void timer0_isr(void) interrupt 1 { TH0 = T0_50MS >> 8; // 必须重装,方式1不自动重装 TL0 = T0_50MS & 0xFF; tick_50ms++; if (tick_50ms >= (unsigned long)study_min * 1200) { tick_50ms = 0; time_up = 1; // 只置标志,不在这里刷屏 } }这段代码里study_min * 1200的 1200 是“每分钟有多少个 50ms”,45 分钟就是 54000。用unsigned long强转是为了防止编译器按 16 位算乘法溢出,51 的int是 16 位,45 乘 1200 已经到 54000,接近上限,换成长一点的时长就会翻车。倒计时显示时用study_min*60 - tick_50ms/20算出剩余秒数,再拆成分钟和秒送到 LCD。
按键调整学习时长放在主循环里做,加键一次加 5 分钟,减键一次减 5 分钟,范围限制在 5~120 分钟。每次改完把tick_50ms清零重新计时,避免改小时长后剩余时间算出一个负数。
4.2 ADC0832 读光敏:时序、通道选择和一次软件校验
ADC0832 是串行时序器件,CS 拉低开始一次转换,DI 线上先送一个起始位 1,再送单端/差分选择位(1 表示单端),再送通道号(0 或 1),之后 DI 释放,DO 线上按位输出转换结果。很多现成代码会在读数据前空打 8 个时钟,再读 8 位,这个做法在多数模块上能跑,但不同批次的芯片前导时钟数略有差异,如果读到的值长期固定在 0 或 255,可以先把空时钟改成 1 个试试。
#include <intrins.h> sbit ADC_CS = P1^0; sbit ADC_CLK = P1^1; sbit ADC_DI = P1^2; sbit ADC_DO = P1^3; unsigned char adc0832_read(unsigned char ch) { unsigned char i, dat = 0; ADC_CS = 0; // 片选有效 ADC_CLK = 0; ADC_DI = 1; // 起始位 ADC_CLK = 1; _nop_(); ADC_CLK = 0; ADC_DI = 1; // SGL/DIF=1,单端输入 ADC_CLK = 1; _nop_(); ADC_CLK = 0; ADC_DI = ch ? 1 : 0; // 通道0或1 ADC_CLK = 1; _nop_(); ADC_CLK = 0; ADC_DI = 1; // 释放DI,准备读DO for (i = 0; i < 8; i++) { // 转换期空时钟 ADC_CLK = 1; _nop_(); ADC_CLK = 0; } for (i = 0; i < 8; i++) { // MSB first 读8位 ADC_CLK = 1; _nop_(); dat <<= 1; if (ADC_DO) dat |= 0x01; ADC_CLK = 0; } ADC_CS = 1; return dat; }读到的 8 位值 0~255 对应 0~5V。实际使用时不直接用单次读数做判断,连续读三次取中值,能滤掉大部分尖峰干扰。光线判定还要加一点回差:低于 80 判为暗,高于 100 才判为恢复正常,中间留 20 的缓冲区,否则光线在阈值附近轻微晃动时蜂鸣器会滴滴答答响个不停,这是这套系统最容易被室友投诉的问题。
4.3 超声波测距:用 T1 计脉宽,超时直接返回无效值
超声波模块的用法是给 TRIG 一个大于 10us 的高电平,模块自动发 8 个 40kHz 脉冲,ECHO 拉高,收到回波后拉低,脉宽时间除以 58 就是厘米数(声速 340m/s,往返除以二,1us 约对应 0.017cm)。用 T1 方式 1 计数最准,因为软件while循环里++t的时间不是精确的 1us。T1 和 T0 共用 TMOD 寄存器,初始化时 T0 配低四位、T1 配高四位,两边都要写,不能用TMOD = 0x01一把梭,那样会把 T1 的配置冲掉。
sbit TRIG = P1^4; sbit ECHO = P1^5; unsigned int sonic_get_cm(void) { unsigned int cnt; unsigned int guard = 0; TRIG = 1; _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); // 约10us以上 TRIG = 0; while (ECHO == 0) { // 等回波起始 if (++guard > 3000) return 999; // 约3ms无响应,判无效 } TH1 = 0; TL1 = 0; TR1 = 1; guard = 0; while (ECHO == 1) { // 计脉宽 if (++guard > 25000) { TR1 = 0; return 999; } } TR1 = 0; cnt = (TH1 << 8) | TL1; return cnt / 58; // 12T,1us计数 }999是无效值标记,主循环里遇到 999 就不更新距离显示,也不做姿势判断,避免模块偶尔丢回波时蜂鸣器误响。3cm 以内是盲区,测出来可能是 0 或乱值,程序里把小于 3cm 的读数也归为无效。量程上限按 90cm 算,对应脉宽约 5.2ms,guard上限设 25000 足够覆盖。
4.4 LCD1602 驱动与主循环状态机:把四个判断拆成优先级
LCD1602 的驱动就三个函数:写命令、写数据、写字符串。写之前读忙标志当然更稳,但为了代码短,可以在每条命令后固定延时 2ms,这套系统刷新频率低,2ms 延时完全不影响响应。初始化必须在主循环之前完成,上电后先延时 40ms 再发第一条命令,等 LCD 内部复位完成,这一步省掉就会出现上电第一屏全是黑块、按一下复位又正常的情况。
#define LCD_DATA P0 sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; void lcd_write_cmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_E = 1; _nop_(); _nop_(); LCD_E = 0; delay_ms(2); } void lcd_write_dat(unsigned char dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; _nop_(); _nop_(); LCD_E = 0; delay_ms(2); } void lcd_init(void) { delay_ms(40); // 上电等待,不能省 lcd_write_cmd(0x38); // 8位、两行、5x7 lcd_write_cmd(0x0C); // 开显示、关光标 lcd_write_cmd(0x06); // 地址自增 lcd_write_cmd(0x01); // 清屏 } void lcd_show_str(unsigned char row, unsigned char col, unsigned char *s) { lcd_write_cmd((row ? 0xC0 : 0x80) + col); while (*s) lcd_write_dat(*s++); }主循环用一个简单的状态机把四类判断排优先级:久坐提醒最高,其次姿势过近,再次光线偏暗,最后才是正常显示。优先级高的报警会打断低级别的蜂鸣节奏,避免三个报警同时触发时蜂鸣器发出一串没有意义的声音。模式键在自动和手动之间切换,手动模式下台灯由按键直接开关,自动模式下由光线和在座状态共同决定。
void main(void) { unsigned char light, dist; unsigned char mode = 1; // 1自动,0手动 lcd_init(); timer0_init(); while (1) { light = adc0832_read(0); dist = sonic_get_cm(); key_scan(&mode); if (time_up) { alarm(ALARM_REST); // 久坐:长鸣 } else if (dist != 999 && dist < 30) { alarm(ALARM_POSTURE); // 姿势:快滴 } else if (light < 80) { alarm(ALARM_LIGHT); // 光线:慢滴 } else { alarm(ALARM_NONE); } if (mode) { RELAY = (PIR == 1 && (light < 80 || dist < 30)) ? 1 : 0; } lcd_refresh(light, dist, mode); } }这段主循环里alarm()用不同的计数分频产生不同频率的方波,ALARM_REST是持续低电平长鸣,ALARM_POSTURE是 2kHz 快滴,ALARM_LIGHT是 1kHz 慢滴。lcd_refresh()里做一件事就够:第一行显示光线 ADC 值和距离,第二行显示剩余分钟和当前模式,刷新周期控制在 200ms 一次,刷太快屏幕会闪。
5. 避坑与排查:五个让蜂鸣器“玄学乱响”的现场问题
现象一:半夜蜂鸣器自己响一声,白天怎么测都正常。原因多半在光敏分压中点悬空或走线太长,ADC0832 读到的值偶尔掉到阈值以下。光敏电阻和 10k 分压电阻的连线如果超过十厘米又没屏蔽,很容易捡到环境干扰。解决办法是把分压电路挪到 ADC0832 旁边,缩短模拟走线,软件上连续读三次取中值,并把光线阈值加 20 的回差。改完之后如果还偶发,就在分压中点对地并一只 0.1uF 电容,把高频毛刺滤掉。
现象二:LCD1602 上电全是黑方块,按一下复位又好了。这是初始化时序问题,不是屏幕坏了。LCD 内部复位需要时间,上电后立刻发命令它收不到,第一条 0x38 就丢了,后面的命令全部错位。解决办法是在lcd_init()最前面加 40ms 延时,每条命令后固定延时 2ms。如果还不行,检查第 3 脚对比度电位器是不是调得太高,把 VEE 电压调到 0.5V 左右字符最清楚。
现象三:距离读数偶尔跳到 999,或者站在 20cm 处显示 60cm。先看 ECHO 脉宽计数用的定时器有没有被 T0 中断打断。T1 计数期间如果 T0 中断进来重装初值,虽然不会直接改 T1,但中断响应会拉长while(ECHO==1)的退出时间,导致计数偏大。解决办法是测距期间临时关 T0 中断ET0=0,测完再开,或者把 T0 中断优先级设低、测距放在主循环里一次做完。另一个原因是 TRIG 高电平不足 10us,用_nop_()凑时间在不同编译器优化等级下长度会变,改成delay_us(15)更稳。
现象四:设了 45 分钟,结果 45 秒就响。这是计数变量类型和乘法溢出问题。50ms 中断 45 秒是 900 次,45 分钟是 54000 次,如果计数变量声明成unsigned char,到 255 就回绕,实际触发时间完全对不上。如果声明成unsigned int但乘法写成45 * 1200,编译器按 16 位算也不会溢出(54000 小于 65535),但时长改成 60 分钟就会溢出。统一用unsigned long强转,或者干脆把“每分钟 1200 次”写成宏,别在表达式里裸写数字。
现象五:继电器一吸合,单片机就复位或者 LCD 花屏。继电器线圈断电的反电动势通过电源线串回单片机,典型的电源跌落。解决办法是线圈两端反并 1N4148,继电器和单片机尽量分开供电,如果共用一个 5V 电源,在继电器电源脚旁边并一只 100uF 电解加 0.1uF 瓷片。还有一种情况是继电器吸合时拉低了大电流,USB 供电带不动,换成 5V/1A 以上的电源适配器就好了。
6. 阈值标定与整机验证:把“能响”调成“敢用”的两小时压测
阈值不能靠试,要靠测。距离阈值我用卷尺加一本厚书做简易标定台:把超声波模块固定在桌面边缘,反射面用硬纸板,分别放在 20cm、25cm、30cm、35cm、40cm 五个点,每个点读 20 次,去掉最大最小各三次,剩下的取平均。12MHz 晶振下理论值 30cm 对应脉宽约 1765us,实际读数会有正负 1~2cm 的偏差,所以阈值不要卡在 30 整,取 28cm 触发、32cm 恢复,留 4cm 回差。标定结果记成一张表贴在代码头部,换模块或换晶振时照着表重测一遍。
| 标定点 | 理论脉宽 | 实测均值 | 触发/恢复阈值 |
|---|---|---|---|
| 20cm | 1176us | 21.3cm | 不触发 |
| 25cm | 1470us | 26.1cm | 不触发 |
| 30cm | 1765us | 31.0cm | 28cm 触发 |
| 35cm | 2059us | 36.2cm | 32cm 恢复 |
| 40cm | 2353us | 41.4cm | 不触发 |
光线的标定更简单:用手机照度软件做粗参考,分别在 50lux、100lux、200lux、300lux 下读 ADC 值,记录四组数据。如果 100lux 对应 ADC 约 60、200lux 对应约 120,那阈值定在 80 就意味着大约 130lux 以下会提醒开灯,这个点接近教室后排的照度,比较合理。别把阈值定得太高,否则阴天白天也会一直报警,用户第一件事就是把电源拔了。
整机验证我一般会跑两小时压测,分三段:第一段正常坐姿 40 分钟,看会不会误报;第二段故意趴近到 20cm,看报警延迟是不是在 1 秒以内;第三段人离开座位 10 分钟,看台灯是不是在延时后自动关闭。三段跑完统计误报次数,超过 3 次就回头查传感器滤波和阈值回差,不要急着改硬件。压测时把串口打开,每 500ms 打印一次光线值、距离值、剩余时间,出问题时串口日志就是黑匣子,比盯着 LCD 猜快得多。
从那以后我每次做完这类系统,都会先把阈值表打印出来贴在壳子内侧,标定日期和晶振频率一起写上,换电池或换模块时强制重测一遍。这套 51 单片机坐姿矫正系统的代码量不大,真正花时间的是这些看起来不产生“功能”的标定和压测,但正是这部分决定了它是一堆会响的元件,还是一个能连续跑一晚上不误报的作品。希望帮到你。
本文还有配套的精品资源,点击获取