STC89C52坐姿矫正系统:光强测距与定时提醒
发布时间:2026/11/2 12:28:54 锦皓数字建站

简介《基于51单片机坐姿矫正系统设计》是一份面向电子信息、自动化及嵌入式方向学生与毕业设计参考者的完整设计文档聚焦学习坐姿监测、环境光线判断与用眼休息提醒等实际问题。压缩包内含1个doc文件约295KB以毕业设计论文为主体覆盖总体方案、硬件电路、单片机最小系统及传感器模块等章节便于按目录梳理设计思路。文档围绕STC89C51/52主控展开整合光敏电阻与模数转换、人体感应、红外/超声波测距、LCD1602显示和蜂鸣器提示说明自动/手动模式、靠近报警、离开延时关灯及定时提醒的实现逻辑。已有793人学习适合需要参考系统框图、模块选型与功能论证的读者用于课题开题、方案对比和论文撰写。1. 从一块 STC89C52 开发板说起这套坐姿矫正系统到底解决什么问题晚上九点半台灯下写作业的孩子整个人趴在桌上脸离本子不到十厘米屋里的灯光其实早就暗下去了——这三件事叠在一起就是这套基于 51 单片机的坐姿矫正系统要盯住的东西。它用 STC89C52 做主控光敏电阻加 ADC0832 测环境亮度超声波模块量人离桌面的距离再配一个 45 分钟倒计时任何一项越界就让无源蜂鸣器叫起来同时在 LCD1602 上把当前状态和剩余时间显示出来。作为一份 51 单片机毕业设计它整套硬件成本不高全是直插器件万用板上就能焊完适合刚学完 C51、想找一个能摸得着的项目把 IO 口、定时器、AD 转换、时序驱动串一遍的人。你要的是能摆在书桌上、而不是只在仿真软件里跑的东西这套方案的门槛刚刚好。2. 总体方案与硬件选型三组器件对比到一张引脚分配表动手焊之前最费时间的不是写代码是选型。这个系统要同时处理四路输入光线、距离、人体、按键和两路输出显示、声音方案一旦定错后面所有代码都要重写。所以得先把三个关键位置定下来主控用什么、显示用什么、测距用什么。2.1 主控、显示、传感三条选型线主控这块STC89C52 的核心优势不是性能是5V 直驱 资料密度。这个系统里所有外设——LCD1602、HC-SR04 超声波、ADC0832、PIR 模块——都是 5V 电平器件接 51 不用任何电平转换一根杜邦线插上去就能通信。换成 3.3V 的 STM32光是电平匹配就得加一片 TXS0108 或者一堆分压电阻对一份要在两周内出板的毕业设计来说这些都属于额外的风险。方案型号优势落地代价方案一STC89C525V 直驱、8KB Flash、Keil C51 生态成熟、资料极多无硬件 I2C/SPI时序全靠软件模拟方案二AVR 系列单周期指令、效率高寄存器配置量大入门阶段容易卡在外设初始化方案三STM32F103RBT6外设丰富、主频高需要仿真器下载、库函数配置繁琐本系统用不上这么多资源方案二、方案三不是不行是杀鸡用牛刀。这个系统满打满算需要 20 个左右 IO、一个 16 位定时器、一个外部中断STC89C52 的 32 个 IO 口和 3 个定时器完全够用还留有余量。显示部分对比了数码管、点阵和 LCD1602。数码管亮度高、占用 IO 少但它只能显示数字你没法在上面写Light Low这种提示点阵能显示汉字可驱动复杂度和功耗都上去了LCD1602 内置 192 个字符字模通过 8 位数据口加 RS/RW/EN 三根控制线就能控制字符位置可以直接按行列寻址代码写起来最省心。测距这块红外光电传感器输出的是模拟电压受环境光干扰大量程也近超声波模块的量程在 3cm 到 90cm近距离精度不错远距离有 1cm 到 2cm 的误差——但对于人是不是趴桌上了这个判断来说30cm 到 40cm 的阈值区间根本用不到远距离量程超声波完全够。这点误差不影响功能反而省了标定模拟量的麻烦。提示VDC 摘要里提到过 AD0809 和数码管显示那是最早的一版方案。AD0809 是 8 通道 8 位并行 ADC要占用 8 根数据线加 3 根控制线接线量比 ADC0832 大一圈。最终定的版本用 ADC0832 串行 AD 加 LCD1602引脚省下来给按键和超声波用。2.2 系统框图与 IO 口分配信号流是这样的光敏电阻和固定电阻串起来分压分压点接到 ADC0832 的 CH0单片机通过 CS/CLK/DIO 三根线读出 0 到 255 的数字量超声波模块的 Trig 由单片机发一个 10 微秒以上的高电平Echo 回一个高电平高电平宽度就是声波往返时间PIR 人体感应模块输出开关量人离开后模块自身会延时几秒再拉低延时时间靠模块上的电位器调。IO 分配我建议一开始就写成表后面写代码直接照着定义能省掉大量这根线到底接的哪个口的返工。引脚连接对象方向说明P0.0–P0.7LCD1602 D0–D7双向8 位数据口P0 无内部上拉需外接 10k 排阻P1.0LCD1602 RS输出高电平选数据寄存器低电平选指令寄存器P1.1LCD1602 RW输出高电平读低电平写P1.2LCD1602 EN输出下降沿锁存数据P1.3ADC0832 CS输出片选低有效P1.4ADC0832 CLK输出串行时钟P1.5ADC0832 DI/DO双向分时复用写命令时输出读数据时输入P1.6超声波 Trig输出触发测距P3.2超声波 Echo输入接 INT0也可纯轮询P2.0–P2.3四个独立按键输入设置 / 加 / 减 / 确认P2.4蜂鸣器驱动输出经三极管放大后接无源蜂鸣器P3.3PIR 人体感应输入高电平表示检测到人这套分配留了 P3.0/P3.1 给串口后面想加把每天报警次数发给上位机的功能时不用改板子。2.3 最小系统与供电晶振、复位、上拉电阻最小系统就三块电源、晶振、复位。晶振用 12MHz配两只 22pF 瓷片电容接地这个频率下机器周期正好是 1 微秒定时器算初值最省事——1 毫秒就是 1000 个机器周期。复位电路是 10uF 电解电容加 10k 电阻上电瞬间电容相当于短路RST 保持高电平完成复位。EA 引脚必须接 VCC否则单片机会去读外部存储器程序根本跑不起来这个坑我见过不止一个人踩。P0 口是开漏输出没有内部上拉接 LCD1602 数据线时必须外挂 10k 排阻不然写进去的数据是乱的。供电用 USB 5V 或者 AMS1117-5.0 稳压输入端加 100uF 电解电容滤低频纹波输出端并一个 0.1uF 瓷片电容滤高频这两个电容别省超声波一发波电源上的毛刺能让 LCD 花屏。引脚定义我习惯单独放一个头文件主程序和各个驱动模块都 include 它改板子的时候只改这一处/* board_cfg.h —— 全系统唯一的引脚定义处换板只改这里 */ #ifndef __BOARD_CFG_H__ #define __BOARD_CFG_H__ #include reg52.h /* LCD1602 */ #define LCD_DATA P0 sbit LCD_RS P1^0; sbit LCD_RW P1^1; sbit LCD_EN P1^2; /* ADC0832 光强采集 */ sbit ADC_CS P1^3; sbit ADC_CLK P1^4; sbit ADC_DIO P1^5; /* HC-SR04 超声波 */ sbit US_TRIG P1^6; sbit US_ECHO P3^2; /* 蜂鸣器PNP 三极管驱动低电平导通 */ sbit BEEP P2^4; /* 按键低电平有效 */ sbit KEY_SET P2^0; sbit KEY_ADD P2^1; sbit KEY_SUB P2^2; sbit KEY_OK P2^3; /* PIR 人体感应 */ sbit PIR_IN P3^3; #endif把引脚集中定义的好处很直接调试阶段如果发现 P1.5 那根线焊虚了改成 P1.7 只动一行代码不用在整个工程里搜索替换。我后来做别的板子也都保留这个习惯血泪经验。3. 核心模块拆解光线采集、超声测距与蜂鸣器驱动的三段代码硬件定了接下来是三个必须自己写时序的模块。LCD1602 有现成驱动可以抄按键扫描是体力活真正需要理解时序图的是 ADC0832、超声波和蜂鸣器这三个——它们的共同点是单片机必须严格按芯片手册的时序拉高拉低早一个时钟周期、晚一个时钟周期读回来的就是垃圾数据。3.1 ADC0832 读光敏电阻一次 AD 转换的完整时序光敏电阻的阻值随光照增强而减小和一只 10k 固定电阻串联分压分压点的电压就在 0 到 5V 之间变化。ADC0832 把这个模拟电压转成 8 位数字量0 表示全黑255 表示很亮。注意这个方向光越强光敏电阻阻值越小分压点电压越低还是越高取决于光敏电阻接在上端还是下端。我一般把光敏电阻接 VCC 侧、固定电阻接地这样光照越强分压点电压越高读数越大逻辑上顺一点。ADC0832 的时序有几个容易翻车的细节。CS 拉低之后要发一个起始位、一个单端/差分选择位、一个通道选择位这三个位都是在 CLK 的上升沿被芯片采样。发完之后要把 DIO 线释放成输入状态因为 51 的准双向口写 1 就是输入所以直接写ADC_DIO 1就行不用切方向寄存器。读数据时芯片在 CLK 的下降沿输出下一位MSB 在前读满 8 位就拉高 CS 结束。/* adc0832.c —— 读 CH0 上的光敏电阻分压返回 0~255 */ #include board_cfg.h unsigned char adc0832_read(unsigned char ch) { unsigned char i; unsigned char dat 0; ADC_CS 0; /* 片选拉低一次转换开始 */ ADC_CLK 0; /* 第 1 位起始位固定为 1 */ ADC_DIO 1; ADC_CLK 1; ADC_CLK 0; /* 第 2 位SGL/DIF 1 表示单端输入0 为差分 */ ADC_DIO 1; ADC_CLK 1; ADC_CLK 0; /* 第 3 位ODD/SIGN 选通道bit0 0 选 CH0 1 选 CH1 */ ADC_DIO (ch 0x01); ADC_CLK 1; ADC_CLK 0; /* 关键51 准双向口写 1 即为输入DIO 现在由芯片驱动 */ ADC_DIO 1; for (i 0; i 8; i) { ADC_CLK 1; /* 上升沿芯片准备下一位 */ ADC_CLK 0; /* 下降沿后 DO 上出现有效数据 */ dat 1; if (ADC_DIO) { dat | 0x01; } } ADC_CS 1; /* 结束本次转换释放总线 */ return dat; }参数说明ch取值范围 0 或 1对应芯片的两个单端通道。函数返回 0 到 255数值越大表示越亮。如果光敏电阻和固定电阻接反了返回值方向会反过来这时候不用改硬件在调用处做个255 - adc0832_read(0)就行。有个细节值得单独说ADC0832 允许把转换结果读两遍第一遍 MSB 在前、第二遍 LSB 在前两次对比可以校验数据。在电源纹波大的板子上这个校验能过滤掉不少毛刺读数。我这版代码没加因为 10k 分压加 0.1uF 滤波电容之后读数已经很稳加了反而拖慢采样。阈值判定很朴素读回来的值低于 90大约对应 1.7V就认为光线不足蜂鸣器短叫一声LCD 第二行显示 Light Low。这个 90 不是算出来的是拿照度计对着书桌实测标定的不同房间的灯不一样得现场调。3.2 超声波测距40kHz 脉冲到距离值的换算HC-SR04 的工作流程是Trig 给一个大于 10 微秒的高电平模块内部发 8 个 40kHz 的方波然后把 Echo 拉高等到收到回波再拉低。Echo 的高电平持续时间就是声波从发射到返回的时间除以 2 得到单程时间乘以声速得到距离。在 12MHz 晶振下机器周期正好 1 微秒用定时器 0 计数算出来的计数值直接就是微秒数。声速按 340m/s 计算1 厘米的往返时间是 2×0.01/340 秒 ≈ 58.8 微秒所以距离(cm) 计数值 / 58。/* ultrasonic.c —— 返回距离单位 cm返回 0 表示超时无效 */ #include board_cfg.h unsigned int ultrasonic_cm(void) { unsigned int t 0; unsigned int timeout 0; US_TRIG 0; US_TRIG 1; delay_us(12); /* 手册要求 10us留点余量 */ US_TRIG 0; /* 等回波上升沿加超时防止模块没插好时死等 */ while (US_ECHO 0) { if (timeout 60000) return 0; } TH0 0; TL0 0; TR0 1; timeout 0; while (US_ECHO 1) { if (timeout 60000) { TR0 0; return 0; } } TR0 0; t (TH0 8) | TL0; return t / 58; /* 12MHz 下计数值即微秒数除以 58 得 cm */ }这段代码里最重要的不是换算公式是那两个timeout。很多同学的超声波程序在拔掉模块或者接线松动时会直接卡死因为while (US_ECHO 0);没有出口。加上超时之后函数最坏情况 60 毫秒返回 0主循环还能照常刷新显示这就是程序卡死和显示异常的区别。另外定时器 0 在这里被临时借用来计回波宽度测完之后要TR0 0停掉否则会和 1 毫秒系统节拍抢同一个定时器。我的做法是系统节拍用定时器 0超声波测量时先关掉ET0中断、清 TH0/TL0、测完再恢复。如果你不想这么折腾也可以用定时器 1 专门给超声波用代码更干净。量程方面HC-SR04 标称 2cm 到 400cm但这个系统只关心 20cm 到 60cm 这个区间。超过 60cm 的读数我直接丢弃返回 0因为那多半是没人在座或者是回波打到别的地方了。3.3 无源蜂鸣器定时器方波与音调控制有源蜂鸣器给电就响无源蜂鸣器得给它方波方波频率决定音调。这个设计用的是无源压电式好处是能发出不同频率的声音——坐姿报警用高频短促的滴滴定时到点用低频长音人耳一耳朵就能分清是哪种提醒。驱动电路上单片机 IO 口的灌电流能力有限直接接蜂鸣器声音很小甚至不响所以中间加一只 PNP 三极管。基极串 1k 电阻限流发射极接 VCC集电极接蜂鸣器再到地。这样单片机输出低电平时三极管导通蜂鸣器得电输出高电平时截止。注意这个极性写反了就是只要上电就一直响。/* beep.c —— 用软件延时产生方波freq_khz 决定音调 */ #include board_cfg.h void beep_ms(unsigned int freq_hz, unsigned int ms) { unsigned int i; unsigned int half_us 500000UL / freq_hz; /* 半周期单位 us */ unsigned int cycles (unsigned long)ms * freq_hz / 1000; for (i 0; i cycles; i) { BEEP 0; /* PNP 导通蜂鸣器得电 */ delay_us(half_us); BEEP 1; /* PNP 截止 */ delay_us(half_us); } BEEP 1; /* 收尾时确保关断别让它一直响 */ }参数说明freq_hz常用 2000尖锐用于坐姿报警和 800低沉用于定时提醒ms是持续毫秒数。half_us用500000UL / freq_hz算是因为 1 秒 1,000,000 微秒半周期就是 500,000 除以频率。delay_us需要你自己实现一个基于_nop_()的微秒延时12MHz 下一个_nop_()大约 1 微秒循环里再补几条指令凑准。软件延时的缺点是占 CPU响 200 毫秒期间主循环什么都干不了。如果你要一边响一边刷屏就把方波生成挪到定时器中断里中断里翻转一次 IO主循环只管开定时器和关定时器。这套代码我留着软件延时版本是因为它足够简单新手能一眼看懂而且 200 毫秒的阻塞对显示刷新影响很小。4. 软件架构1ms 节拍下的主循环、倒计时与 LCD1602 驱动层三个驱动模块写完之后剩下的问题是怎么把它们组织起来。这个系统有四件事要同时做每 10 毫秒采一次光和距离、每秒更新一次倒计时、随时响应按键、持续刷新 LCD。如果全写成顺序执行的阻塞代码按一次键屏幕就会闪一下超声波测距的时候倒计时还会走慢。所以软件结构上采用定时器节拍 主循环轮询的前后台模型中断只负责置标志位所有耗时操作放主循环。4.1 主循环与前后台结构定时器 0 配成工作方式 116 位定时12MHz 下每 1000 个机器周期进一次中断也就是 1 毫秒。中断服务程序里维护三个计数器1 毫秒用来给延时函数提供时基10 毫秒置一次采样标志1000 毫秒置一次秒标志并递减倒计时。中断里绝对不要写 LCD、不要调超声波这两件事都会阻塞几十毫秒节拍全乱。/* main.c —— 前后台结构中断置标志主循环干活 */ #include board_cfg.h #define STUDY_SEC (45 * 60) /* 默认 45 分钟 */ #define DIST_WARN_CM 35 /* 小于 35cm 判为趴桌 */ #define LIGHT_MIN 90 /* ADC 低于此值判为光线不足 */ volatile unsigned int remain_sec 0; volatile bit flag_10ms 0; volatile bit flag_1s 0; void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; /* 定时器 0方式 116 位定时 */ TH0 0xFC; /* 12MHz 下 1ms65536-1000 64536 0xFC18 */ TL0 0x18; ET0 1; EA 1; TR0 1; } void timer0_isr(void) interrupt 1 { static unsigned char c10 0, c100 0; TH0 0xFC; TL0 0x18; /* 必须重装方式 1 不自动重载 */ if (c10 10) { c10 0; flag_10ms 1; } if (c100 100) { c100 0; flag_1s 1; if (remain_sec) remain_sec--; /* 到 0 就停在 0不往回绕 */ } } void main(void) { unsigned char light, dist; lcd_init(); timer0_init(); lcd_show_string(0, 0, Posture Monitor); while (1) { if (flag_10ms) { flag_10ms 0; light adc0832_read(0); dist ultrasonic_cm(); if (dist 0 dist DIST_WARN_CM) { beep_ms(2000, 150); lcd_show_string(1, 0, Too Close! ); } else if (light LIGHT_MIN) { beep_ms(800, 100); lcd_show_string(1, 0, Light Low! ); } else { lcd_show_string(1, 0, Posture OK ); } } if (flag_1s) { flag_1s 0; lcd_show_time(remain_sec); /* 第二行的右半边显示 MM:SS */ } key_scan(); /* 非阻塞靠 10ms 标志做消抖 */ } }这里有两个设计判断值得说清楚。第一采样周期选 10 毫秒而不是 1 毫秒是因为超声波一次测量本身就要几毫秒到几十毫秒采太快没有意义反而挤占了刷屏和按键的时间。第二remain_sec声明成volatile因为它在中断里被修改、在主循环里被读取不加这个关键字编译器可能把它优化到寄存器里主循环永远读到旧值——这个坑很隐蔽现象是倒计时走到某个数就不动了。4.2 45 分钟倒计时与按键设置倒计时逻辑本身只有一行if (remain_sec) remain_sec--;真正麻烦的是按键设置。四个键的功能是SET 进入设置模式、ADD 加一分钟、SUB 减一分钟、OK 确认并启动。设置模式下倒计时暂停LCD 上光标闪烁提示当前在设置状态。按键消抖不用delay_ms(10)那种阻塞写法而是在key_scan()里用一个静态变量记录上次电平配合 10 毫秒的采样标志做状态机判断。这样按一次键只触发一次也不会让主循环卡住。/* key.c —— 基于 10ms 节拍的非阻塞消抖 */ #include board_cfg.h unsigned char key_scan(void) { static unsigned char last 0x0F; unsigned char now, changed, key 0; now 0; if (KEY_SET 0) now | 0x01; if (KEY_ADD 0) now | 0x02; if (KEY_SUB 0) now | 0x04; if (KEY_OK 0) now | 0x08; changed now (~last); /* 只在由松到按的边沿触发一次 */ last now; if (changed 0x01) key 1; /* SET */ if (changed 0x02) key 2; /* ADD */ if (changed 0x04) key 3; /* SUB */ if (changed 0x08) key 4; /* OK */ return key; }changed now (~last)这一行是整个按键逻辑的核心只有上一个是松开的、现在是按下的这一刻才返回键值按住不放不会连续触发。要改成长按连续加也很简单把changed换成now再加一个计时器就行。设置范围限制在 1 到 90 分钟超出就钳位不然用户一路按 ADD 能按到几万分钟显示区也会溢出。4.3 LCD1602 驱动忙标志判断与字符写入LCD1602 的驱动有两个流派一种是每次写指令后delay_ms(2)死等另一种是读忙标志位 BF。前者简单但慢刷一整行要几十毫秒后者快但要求 RW 线接好、P0 口能正确读回数据。我选后者因为这个系统要频繁刷新第二行的状态提示。写指令的流程是先读 BF 等它变 0然后 RS0、RW0、数据口送指令、EN 拉高再拉低产生下降沿。写数据的流程一样只是 RS1。EN 的高电平宽度要求大于 450 纳秒12MHz 下几条指令就够了但为了保险我还是加了 2 微秒延时。/* lcd1602.c —— 带忙标志判断的字符写入 */ #include board_cfg.h void lcd_wait_busy(void) { LCD_DATA 0xFF; /* P0 准双向口写 1 释放为输入 */ LCD_RS 0; LCD_RW 1; LCD_EN 1; while (LCD_DATA 0x80); /* BF 是数据口最高位1 表示内部忙 */ LCD_EN 0; } void lcd_write_cmd(unsigned char cmd) { lcd_wait_busy(); LCD_RS 0; LCD_RW 0; LCD_DATA cmd; LCD_EN 1; delay_us(2); LCD_EN 0; } void lcd_write_data(unsigned char dat) { lcd_wait_busy(); LCD_RS 1; LCD_RW 0; LCD_DATA dat; LCD_EN 1; delay_us(2); LCD_EN 0; } void lcd_init(void) { delay_ms(50); /* 上电等内部复位完成这一步不能省 */ lcd_write_cmd(0x38); /* 8 位数据口两行显示5x7 点阵 */ delay_ms(2); lcd_write_cmd(0x38); lcd_write_cmd(0x0C); /* 开显示关光标 */ lcd_write_cmd(0x06); /* 写入后地址自动加 1 */ lcd_write_cmd(0x01); /* 清屏 */ delay_ms(2); } void lcd_show_string(unsigned char row, unsigned char col, char *str) { unsigned char addr (row 0) ? 0x80 : 0xC0; addr col; lcd_write_cmd(addr); while (*str) { lcd_write_data(*str); } }0x80和0xC0是两行的起始地址第一行 DDRAM 从 0x00 开始第二行从 0x40 开始写指令时加 0x80 表示设置地址。lcd_init里那个delay_ms(50)是硬性要求LCD1602 内部有复位电路上电后 40 毫秒内不接受任何指令这段时间发的命令全部丢失现象就是开机满屏黑块。5. 避坑与排查五个把板子焊好却跑不起来的现场记录硬件焊完、程序烧进去第一次上电大概率是不工作的。这套系统里最容易出问题的不是复杂的时序而是几个看起来没问题的地方。下面五条都是实际调试时遇到的按现象、原因、解决三步写。5.1 硬件侧的三条现象一LCD1602 上电满屏黑方块程序怎么改都不显示字符。原因有两个层次。浅层是初始化时序不对上电后没等够 40 毫秒就发指令深层是对比度引脚 VEE 悬空或者直接接地液晶偏压不对字符其实写进去了但看不见。解决办法是先确认 VEE 接了一个 10k 电位器的中间抽头调到字符最清晰的位置再检查lcd_init()开头有没有delay_ms(50)。这两个都排除之后还不行量一下 P0 口有没有 10k 上拉排阻——P0 是开漏输出没有上拉就推不出高电平读忙标志永远读到 0程序会一直卡在while里。现象二蜂鸣器上电就一直响或者该响的时候只听到嗒一声。一直响多半是三极管极性搞反了。PNP 管的基极低电平导通如果代码里按 NPN 的逻辑写高电平导通上电时 IO 默认是高电平三极管反而导通。只听到嗒一声说明用的是无源蜂鸣器但代码只拉低了一次电平没有持续翻转——无源蜂鸣器必须给方波给直流只会响一下。解决确认三极管型号和接法用万用表量基极电压无源蜂鸣器改用beep_ms()这种带频率参数的函数。现象三超声波读数在 0 和 200 多之间乱跳LCD 上距离一会儿Too Close一会儿正常。先排除接线Trig 和 Echo 接反是最常见的接反之后 Echo 上永远等不到上升沿函数靠超时返回 0主循环把 0 当作无效跳过看起来就是数据乱跳。其次看电源超声波发射瞬间电流能到 15 毫安以上如果和单片机共用一条细杜邦线电压会瞬间跌落导致读数错误解决是在模块 VCC 和 GND 之间就近并一个 100uF 电容。第三是角度超声波波束角大约 15 度斜着打桌面会收到多次回波读数偏小把模块垂直朝上装就行。5.2 软件侧的两条现象四倒计时走到某个数就不动了按键还能响应屏幕也在刷新。这是volatile缺失的典型症状。remain_sec在中断里递减、在主循环里判断如果不加volatile编译器优化后主循环可能只从寄存器读一次值之后永远用那个缓存值。另一种可能是remain_sec被定义成unsigned char45 分钟 2700 秒早就溢出了。解决声明成volatile unsigned int并且在中断里递减前先判断非零避免从 0 减到 65535。现象五加了超声波之后LCD 刷屏明显变慢按键反应也变迟钝。原因是把ultrasonic_cm()放进了定时器中断或者放进了 1 毫秒的采样分支。一次测距最坏要 25 毫秒对应 4 米量程这段时间 CPU 完全被占住LCD 的忙等待和按键扫描全部推迟。解决测距放在主循环的 10 毫秒分支里并且在函数内部加超时退出如果一定要更快的响应把测距周期降到 50 毫秒一次人趴桌这个动作不可能在 50 毫秒内完成又恢复。6. 二次开发从会响到能用几个能落地的改造点这套代码跑通之后最想改的通常是阈值——出厂写死的 35cm 和 90 在每个人家里都不合适。所以第一件事是把所有可调参数抽到一个头文件里配合按键做成运行时可改改完写进 STC 内部的 EEPROM用 IAP 方式掉电不丢。参数默认值可调范围影响DIST_WARN_CM3520–60越小越宽松趴到很近才报警LIGHT_MIN9040–160越大越敏感稍暗就提示STUDY_SEC270060–5400单次学习时长BEEP_GAP_MS30001000–10000两次报警最小间隔防止一直叫第二件事是加串口上报。P3.0/P3.1 现在空着接一个 USB 转 TTL 模块每秒往上报一行L85,D42,T1240上位机拿串口助手就能存成 CSV跑一周之后你就知道孩子到底在哪个时间段最容易趴桌。串口初始化用定时器 1 方式 2波特率 9600TH1 0xFD这几行代码加起来不超过 30 行。第三件事是把超声波换成激光测距模块。HC-SR04 的 1 到 2 厘米误差在趴桌判定里其实够用但它怕两种情况桌面有反光物体、旁边有人走动。VL53L0X 这类 ToF 模块精度到毫米级I2C 接口缺点是 3.3V 供电得加电平转换。如果你只是想验证思路超声波完全够要做成能送人的成品值得换。第四件事加 PIR 人体感应做省电。现在这套系统只要上电就一直采样、一直刷屏没人在的时候纯属浪费。P3.3 接 PIR 模块的输出检测到人再启动采样和倒计时人走之后延时 30 秒进休眠用PCON | 0x01进空闲模式外部中断唤醒。这一改整套东西才真正像个产品而不是作业。改造完之后一定要做的验证是老化测试连续通电 8 小时每半小时记录一次 LCD 有没有花屏、倒计时有没有走偏、蜂鸣器有没有误触发。我第一次做完这套系统觉得功能都对了就直接交差结果第三天发现连续工作 5 小时后倒计时会慢 2 分钟——原因是中断里做了浮点运算float在 51 上是软件模拟的一次除法几百个机器周期节拍被拖垮了。从那以后我每次写定时器中断都强制走一遍中断里只准置标志、不准做运算这条线宁可主循环多轮询几次。希望这些踩过的坑能帮你少走点弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。