软件开发:I2C通信、定时中断与趋势预警算法
上篇回顾:我们把四个核心元件焊成了一台完整的监测仪。但硬件只是躯壳,真正让这台机器“会预判”的,是烧进单片机里的那几百行C代码。今天,我们打开Keil工程,看看软件是怎么让传感器开口说话、让蜂鸣器在该响的时候响的。
01 先搭一个稳稳当当的程序骨架
我们用的开发环境是Keil uVision4,语言是最朴素的C。代码不长,但必须层次分明:主循环负责顺序调度,中断负责严格定时,这是嵌入式开发的经典范式。
程序骨架长这样:
main() { 初始化LCD1602; 初始化定时器T0; // 设定2秒中断 全局中断使能; while(1) { // 每2秒执行一次 if(定时标志位 == 1) { 清除定时标志; 读取AM2320数据; 解析温度湿度; 执行趋势预警算法; 更新LCD显示; 控制蜂鸣器; } } }
一个关键设计是:主循环不自己做延时,所有周期同步于定时器T0。
为什么?因为趋势预警的核心是计算“湿度变化率”——变化率依赖一个精确的时间窗口。如果用delay()函数傻等2秒,CPU被占满不说,时间误差也会一点点累积,最后算出来的速率毫无意义。而定时器T0以晶振为基准,中断周期稳如钟摆,由此建立的2秒采样节拍,是整个系统的时间准星。

02 把传感器从睡梦中叫醒——AM2320的I2C通信
AM2320是个好传感器,但它有个“毛病”:会睡觉。
出厂默认的AM2320在完成一次测量后会自动进入休眠状态,以节省功耗。这对电池供电的设备是好事,但对我们这种实时监测系统来说,一旦睡过去,下一次读取就会失败——屏幕直接显示“!!”,通信异常。
我们的解决方案简单而有效:在每次读取数据前,主动发送I2C起始信号,并给它2毫秒的唤醒时间。
具体步骤如下:
-
发送起始信号:主机拉低SDA,再拉低SCL,完成I2C总线占用。
-
发送器件地址 0xB8:AM2320在I2C模式下的7位地址是0x5C,左移一位加读写位,即写地址0xB8。
-
等待2ms:这个短暂的延时,就是留给传感器从休眠中苏醒、并完成一次内部测量的时间。
-
发送读寄存器命令:向0x03寄存器写入0x00起始地址、读取4字节(湿度高字节、湿度低字节、温度高字节、温度低字节),之后附带2字节校验。
-
主机重新启动总线,从传感器接收6字节数据。
-
校验通过后,存入变量,释放总线。
这一套操作,代码层面完全是“软件模拟I2C”——STC89C52没有硬件I2C模块,我们通过精确翻转P1.0和P1.1的电平,配合微秒级延时,硬生生用程序实现了标准I2C时序。这锻炼了我们对通信协议底层时序的理解,也是整个项目中最“硬核”的一段代码。
03 数据解析:从原始字节到温湿度数值
拿到6个字节后,解析逻辑很直白:
湿度 = (第2字节 << 8 | 第3字节) / 10.0; // %RH 温度 = (第4字节 << 8 | 第5字节) / 10.0; // ℃
注意:高字节在前,低字节在后,组合成16位整数再除以10。这里有一个经常被忽略的细节——必须用浮点数或至少保留一位小数,否则0.1°C的分辨率就被抹掉了。我们在程序中实际使用整数运算,显示时把小数点固定在某一位,这样可以避免浮点运算带来的内存开销(STC89C52的RAM很宝贵)。
04 核心算法:三级状态机,让趋势被看见
这是整个项目的灵魂。我们定义了一个三级状态机,每2秒运行一次:
| 状态 | 触发条件 | 系统表现 |
|---|---|---|
| OK | 温度<24℃ 或 湿度≤55%,且湿度未快速上升 | LCD显示“OK” |
| VENTIL | 当前10分钟内湿度差值 ≥ 3%RH,且未触发MUGGY! | LCD显示“VENTIL”,建议开窗 |
| MUGGY! | 温度 ≥ 24℃ 且 湿度 > 55% | LCD显示“MUGGY!”,蜂鸣器响 |
4.1 湿度差值的计算
我们维护了一个长度为5的历史湿度数组(相当于10秒窗口,因为每2秒采一次)。每次新数据到来,计算“当前湿度”与“10分钟前记录的湿度”的差值。如果差值≥3%RH,则认为湿度正在快速攀升,触发VENTIL。
注意,“10分钟前”并不是严格记录10分钟前的值,而是在10分钟内取一个具有代表性的旧值——我们实际采用的是每10分钟采样周期起点处的湿度值。这样既能反映趋势,又避免了短时波动误报。(实际代码中,使用一个滑动窗口取每分钟的湿度进行差分平均,可进一步平滑噪音,但基本原理如此。)
4.2 状态的优先级与切换
状态判断的顺序严格遵循优先级:先检查MUGGY!,再检查VENTIL,否则为OK。这保证了一旦达到真正闷热,蜂鸣器必定响起,不会被通风建议覆盖。
状态的切换本身也经过消抖处理:连续两次采样都满足更高状态时,才向上跳转;而退出状态(如从MUGGY!降级到OK)则需要采样值稳定在低状态若干次,避免边界抖动导致反复报警。
正是这看似简单的“if-else”,实现了从“看到数值”到“理解趋势”的飞跃。在下一篇文章的实验部分,你会看到它在真实教室里是如何一步步触发VENTIL,然后才抵达MUGGY!的——这正是我们设计这套状态机的意义所在。
05 交互输出:LCD分层显示与蜂鸣器控制
有了状态,怎么呈现?
-
LCD第一行:固定显示T:XX℃ H:XX%,让人一目了然地知道物理量。
-
LCD第二行:动态显示系统状态码,按当前状态切换为Status:OK、Status:VENTIL或Status:MUGGY!。
这种“数据+状态”的分层显示策略,在代码层面表现为两段分开的显示更新函数:第一行每2秒更新一次数值,第二行仅在状态发生变化时才刷新,既减少了LCD的写入次数,又让重要状态信息即刻反映。
-
蜂鸣器:当system_state==MUGGY 时,P2.3引脚置高,有源蜂鸣器直接发声;状态退出MUGGY,引脚置低,鸣叫停止。
我们还在MUGGY!状态下加入了短暂间歇鸣响:响0.5秒、停0.5秒,这样既能持续提醒,又不至于让人烦躁到想破坏设备。这项细节是在实测后加上的——最初持续长响,被教室里的同学吐槽像火警,改成了间歇式后接受度高了很多。




06 烧录与调试中的小故事
程序第一次烧录进单片机时,LCD显示乱码。我们反复检查代码、修改时序,问题依旧。最后发现不是软件Bug——是单片机芯片在烧录座上没卡紧,供电时断时续,导致LCD初始化失败。重新插牢后,一切正常。
这个细节让我们明白:嵌入式开发是软硬一体的,当软件看起来没问题时,别忘记去捏一捏芯片、压一压排线。
07 小结与下一篇预告
至此,软件部分完整闭环:定时器建立了2秒采样节拍,I2C模拟唤醒了传感器的数据,状态机把湿度趋势转化成了通风建议或闷热警报,LCD和蜂鸣器将这一切传达给教室里的每一个人。
但实验室里的完美,到了真实课堂还灵不灵?下一篇,我们会抱着设备走进三种规模的教室,在真实的满员课堂上,悄悄地连续监测2小时,记录下每一次VENTIL和MUGGY!出现的时刻。
下一篇:《实验设计与现场实测——在真实课堂上“暗中观察”2小时》,我们看数据说话。
- 点赞
- 收藏
- 关注作者
评论(0)