简介:面向污水处理厂气体检测的电子鼻系统硬件设计方案,以STC89C51单片机为核心,搭配ADC0832扩展4路模拟量输入,完成硫化氢、氨气、甲烷、一氧化碳浓度采集与超限报警。适合单片机课程设计、电子竞赛或工程实训,帮助掌握多路模拟信号采集、LCD1602显示、声光报警和按键阈值设定等开发环节。
压缩包共63个文件,包括Keil C源码工程、Proteus仿真工程、Altium Designer原理图及PDF、LCD1602/ADC0832/主程序等8张流程图、元件清单Excel和hex烧录文件,压缩包1.72MB,目录清晰便于按模块学习。目前已有260人学习。
读者可先运行仿真观察4路ADC转换与液晶刷新流程,再依据原理图和物料清单搭建硬件;源程序按主循环和子模块划分,注释清晰,适合二次开发,也可作为课程设计或竞赛方案参考。
1. 四路模拟量报警器,先把硬件骨架钉在 51 上
标题里的“报警器”很容易让人想到红外对射、门磁干接点这类开关量方案,但这个项目走的是另一条路:它要监控的是连续变化的物理量,比如 4-20mA 变送器输出的温度、压力、液位、光照强度。51 单片机本身只认 0/1 数字电平,要处理模拟电压就必须在 MCU 和传感器之间塞一个模数转换器。ADC0832 是国民级 8 位双通道 ADC,通过 SPI 兼容时序和单片机通信,单颗芯片就能满足 4 路模拟量采集的需求,恰好补齐了 51 单片机没有片上 ADC 的短板。
整个系统的数据流很清晰:4 路传感器输出 0-5V 模拟电压,经过分压或运放调理后送入 ADC0832 的四个输入通道,单片机按地址切换通道、读数、比较阈值,一旦超出预设范围就驱动蜂鸣器或 LED 指示。配套的 Proteus 仿真图、原理图和源代码把整套流程拆成了可验证的步骤。适合正在做 51 单片机课程设计、或者想搞明白 ADC 采样和报警状态机之间怎么配合的人——新手能照着原理图焊板子,老手则能在仿真和轮询/中断两种处理模式里看出点值得推敲的东西。
2. ADC0832 的时序与读取,四路通道是怎么切出来的
2.1 为什么选 ADC0832,而不是 PCF8591 或 ADC0809
常见做法里,51 单片机配 ADC 有三类选择。ADC0809 是并行接口,读一次要拉 8 根数据线加 3 根地址线,占用引脚太多,且芯片早已不再量产,教学仿真里虽然能用,实际做板子却很难买到正品。PCF8591 只有 8 位精度,但它走 I2C 总线,时序处理相对繁琐,而且不支持差分输入,在工业模拟量采集场景里不如 ADC0832 灵活。ADC0832 是 SPI 兼容串行接口,只需要 CLK 和 DATA 两根线,加上片选 CS 一共三个 GPIO 就能完成全双工通信,支持双通道单端输入和差分输入两种模式。8 位分辨率在烟雾浓度、光强、温度区间监测这类应用中完全够用,误差约 0.4%(满量程 5V 时最小分辨率约 19.5mV),卡报警阈值 2.5V 时精度足够。
从硬件成本看,ADC0832 引脚兼容性能直接替换 ADC0831 到 ADC0838 系列,而且封装是 DIP-8,洞洞板和 PCB 都能轻松焊接。最关键的是,它的启动时序比 ADC0809 简单得多——片选拉低后直接送时钟,不需要对总线做地址锁存操作,代码量可以控制在 20 行以内。
2.2 SPI 时序拆解:启动位、通道选择位、数据读取
ADC0832 的串行接口本质上是一个 8 位移位寄存器。片选 CS 拉低后,每个 CLK 上升沿采样一位命令,下降沿输出一位数据。完整转换分三个阶段:
- 启动位(Start Bit): 第一个高电平脉冲,告诉芯片开始一次转换周期
- 通道选择位(SGL/ODD + SELECT): 接下来两个脉冲决定工作在单端还是差分模式,以及选哪个通道
- 数据读取(D7-D0 + D7-D0 重复): 之后 8 个时钟周期输出高字节在前的结果,再接着 8 个时钟输出低字节在前的重复值,用于校验
单端模式下,SGL/ODD 置 1。当 MULTI 选通顺序为 CHO 时,对应通道选择位组合如下:
| SGL/ODD | SELECT | 工作模式 | 输入通道 |
|---|---|---|---|
| 1 | 0 | 单端 | CH0 对 GND |
| 1 | 1 | 单端 | CH1 对 GND |
| 0 | 0 | 差分 | CH0+ 与 CH1- |
| 0 | 1 | 差分 | CH1+ 与 CH0- |
但注意,这是单颗 ADC0832 的原生通道数。标题要求 4 路模拟量,一种做法是直接外接两片 ADC0832,每片管两路;另一种是用一片 ADC0832 配合 4051 模拟多路开关扩展。实际上更常见的低成本方案选用带 4 个模拟输入的 ADC0834,它多了一个 ADD 引脚,通过地址位最多可寻址 4 个输入通道,且时序完全兼容 ADC0832。若坚持用 ADC0832,则有两种思路:通道选择位只寻址两路,另两路依靠 CD4051 的 A/B/C 译码来切换——但这样代码里就要同时维护两组切换逻辑,C 代码负责 CD4051 通道选择,同时等待 ADC0832 转换完成。个人建议直接换成 ADC0834,硬件连线少两根,代码里只是把读取函数从“2 通道”改成“4 通道”,时序本身没有任何变化。
2.3 51 单片机读取 ADC0832 的 C 语言代码
用 Keil 写一个通用的read_adc0832函数。以 STC89C52 为默认平台,接 12MHz 晶振。代码里有注释,可直接复制到 Keil 里编译:
#include <reg52.h> // ADC0832 引脚定义 sbit CS = P2^0; // 片选,低有效 sbit CLK = P2^1; // 时钟,空闲时低电平 sbit DATA = P2^2; // 数据线,双向口(读状态时输入,写命令时输出) /** * @brief 从 ADC0832 读取一次转换结果 * @param ch: 通道选择,0 对应 CH0,1 对应 CH1 * @return 8 位采样值(0~255) */ unsigned char adc0832_read(unsigned char ch) { unsigned char i, dat = 0; // 默认:单端模式,只操作当前选择的通道 CS = 0; // 片选拉低,启动一次转换 CLK = 0; DATA = 1; // 第一个脉冲前拉高数据线 CLK = 1; // 第一个上升沿:芯片检测到启动位 CLK = 0; // 发送通道选择位 // 第 1 个位:SGL/ODD = 1(单端输入) // 第二个位:ch = 0 选 CH0,ch = 1 选 CH1 if (ch == 0) { // 通道 0:发送 1 0 DATA = 1; CLK = 1; CLK = 0; // 第 2 个上升沿 DATA = 0; CLK = 1; CLK = 0; // 第 3 个上升沿 } else { // 通道 1:发送 1 1 DATA = 1; CLK = 1; CLK = 0; // 第 2 个上升沿 DATA = 1; CLK = 1; CLK = 0; // 第 3 个上升沿 } // 第 4 个下降沿后,芯片开始在 DATA 上输出转换结果 // 数据以高位在前(MSB first)顺序排列 DATA = 1; // 释放数据线,准备读 _nop_(); // 读取高 8 位:从 D7 到 D0 for (i = 0; i < 8; i++) { CLK = 1; _nop_(); dat |= DATA; // 读入当前位 dat <<= 1; // 左移,准备下一位 CLK = 0; _nop_(); } CS = 1; // 一次转换结束,片选拉高 // 注意:这里没有读取芯片输出的低 8 位校验重复值。 // 实际工程中,严谨的做法是再读 8 个时钟的 D7-D0 重复值, // 然后与高 8 位对比。两者一致才认为数据有效。 // 简化的教学代码往往只读高 8 位。 return dat; // 返回原始采样值(0~255) }2.3.1 这段代码的逻辑说明
片选拉低后,芯片其实在等待 CLK 上升沿接收命令。DATA = 1那行不是传数据,而是为了让数据线处于确定的高电平状态——因为 51 单片机的 P2 口是准双向口,读引脚之前必须先写 1。启动位之后的两个通道选择位,对应SGL/ODD和SELECT。这里我用了最直接的if (ch == 0) / else分支来发送组合,比用数组查表更直观。
高位先读的循环里有个细节:dat |= DATA在CLK = 1之后执行。这是为了等电平稳定——51 单片机在 12MHz 下运行,引脚电平变化后需要大概一个机器周期(1µs)稳定,而CLK = 1; _nop_();恰好提供了这个延时。如果改成CLK = 1后立即读,大概率会读到上一次残留的电平,造成偶发跳变。
2.3.2 参数和时序的调整空间
上面代码固定了 CPU 主频 12MHz。如果换用 11.0592MHz 晶振,_nop_()的时间会从 1.085µs 变成 1.19µs,但 ADC0832 的 CLK 最高可以跑到几百 kHz,这点变化完全在安全范围内。需要注意的反而是 CLK 空闲电平:有的例程在循环里用CLK = !CLK翻转,看起来没问题,但读数据时可能因为翻转和读顺序错位导致高低位颠倒。排错时先看逻辑分析仪抓的时序——CLK 空闲时是低电平,且启动位之后必须跟一个完整的下降沿,否则转换不触发。
2.3.3 四路扩展时的调用方法
如果按标题的方案真用了双片 ADC0832,调用逻辑就是分配两套引脚:
// 假设第二片 ADC0832 接到了 P2^3 (CS2)、P2^4 (CLK2)、P2^5 (DA2) unsigned char read_channel(unsigned char ch) { unsigned char val; if (ch < 2) { val = adc0832_read(ch); // 使用默认引脚 } else { // 切换 CS 到第二片 CS = 1; // 关闭第一片 CS2 = 0; // 打开第二片 val = adc0832_read_with_pins(ch - 2); // 第二个函数用 P2^3/P2^4/P2^5 CS2 = 1; } return val; }但这种写法很快会暴露出一个问题:adc0832_read函数内部写死了引脚。更好的办法是把引脚声明改成宏定义,或者干脆将函数参数改成“片选编号”。不过这只是代码整洁度问题,不影响功能。真正的坑在于两片 ADC0832 的转换周期不同步——如果各自独立上电,采样时刻会有微小的随机偏移。对报警器来说,4 路通道彼此独立,毫秒级的采样时间差完全不敏感,但如果将来要做 4 通道同时采样做差值运算,就必须用同一片 ADC0834 或者加采样保持器。
3. 报警阈值怎么设:从原始值到物理量的换算
3.1 8 位采样值不是电压
ADC0832 返回的是 0~255 的整数,对应 0~Vref 的电压。这里 Vref 不是 5V 芯片供电电压那么简单——如果 Vref 引脚外接了精密基准源,比如 TL431 输出的 2.5V,那么 255 只对应 2.5V。报警器设计第一件事,是把采样值换算成真实电压或真实物理量。
换算公式:V = (AdcValue / 255) * Vref。例如采样值 128,Vref 为 5V,则电压约 2.51V。如果想直接显示温度“35.2℃”,还需要查传感器数据手册里的传输特性曲线,比如 PT100 热电阻在 0℃ 时 100Ω,100℃ 时 138.5Ω,经过恒流源激励变成电压后,非线性误差需要软件查表修正——这里不展开,先记住报警器核心逻辑用原始值比较就够了。
3.2 阈值比较的三种策略
报警器不是简单的“超了就叫”,还需要考虑回差(滞回)和滤波。
第一种最原始:单阈值比较。if (adc_value > HIGH_LIMIT) alarm_on();。缺点是传感器噪声很容易造成临界点反复触发,蜂鸣器在阈值附近“哒哒哒”响个不停。
第二种:滞回比较(Hysteresis)。设置两个阈值,一个触发阈值HIGH_ON,一个复位阈值HIGH_OFF,且HIGH_OFF = HIGH_ON - 回差值。比如高温报警:超过 200(约 3.92V)启动报警,降到 190(约 3.73V)才解除。这样一来,即使输入信号在阈值附近抖动,输出也只有一个清晰的跳变沿。
第三种:平均值滤波 + 滞后。连续采样 N 次(比如 8 次)取平均,再和阈值比较。代价是响应变慢,但能滤掉大部分工频干扰和传感器噪声。这个方案我经常用,因为 ADC0832 速度本来也不快,做一次完整转换大概需要 100µs,8 次采样不到 1ms,对报警器实时性毫无影响。
下面代码实现第二种和第三种组合:
// 报警阈值通道定义(以原始 AD 值表示) // 假设 4 路通道:CH0 高温、CH1 高压、CH2 液位低、CH3 可燃气体浓度 #define CH0_HIGH_ON 200 // 高温报警触发值 #define CH0_HIGH_OFF 190 // 高温报警复位值 #define CH1_HIGH_ON 220 // 高压报警触发值 #define CH1_HIGH_OFF 200 // 高压报警复位值 #define CH2_LOW_ON 40 // 液位低报警触发值(低于此值报警) #define CH2_LOW_OFF 55 // 液位低报警复位值 #define CH3_HIGH_ON 180 // 可燃气体浓度报警触发值 #define CH3_HIGH_OFF 160 // 报警状态标志 bit alarm_flag[4] = {0, 0, 0, 0}; // 使用 bit 类型省内存,但数组不能直接用 bit,所以这里用 char 数组 // 修正:Keil C51 不支持 bit 类型数组,改用 unsigned char unsigned char alarm_state[4] = {0, 0, 0, 0}; // 0 = 正常,1 = 报警中 // 8 次采样求平均 unsigned char average_adc(unsigned char ch) { unsigned int sum = 0; unsigned char i; for (i = 0; i < 8; i++) { sum += adc0832_read(ch); // 假定已有 adc0832_read 函数 } return (unsigned char)(sum / 8); } // 主循环中的报警判断逻辑 // 阈值表:每路都是上上限、下限、起始阈值 // 这里以"高报"为例,低报逻辑用 if (value < LOW_ON) 即可 unsigned char threshold_table[4][2] = { {CH0_HIGH_ON, CH0_HIGH_OFF}, {CH1_HIGH_ON, CH1_HIGH_OFF}, {CH2_LOW_ON, CH2_LOW_OFF}, // 低报,参数顺序反过来 {CH3_HIGH_ON, CH3_HIGH_OFF} }; // 返回当前通道是否处于报警状态(已触发就返回 1,未触发则判断是否达到触发条件) unsigned char check_alarm(unsigned char ch, unsigned char value) { if (ch == 2) { // 液位低报警:低于 LOW_ON 触发,高于 LOW_OFF 解除 if (alarm_state[ch] == 0 && value < threshold_table[ch][0]) { alarm_state[ch] = 1; return 1; } if (alarm_state[ch] == 1 && value > threshold_table[ch][1]) { alarm_state[ch] = 0; return 0; } return alarm_state[ch]; } else { // 高报警 if (alarm_state[ch] == 0 && value > threshold_table[ch][0]) { alarm_state[ch] = 1; return 1; } if (alarm_state[ch] == 1 && value < threshold_table[ch][1]) { alarm_state[ch] = 0; return 0; } return alarm_state[ch]; } }3.2.1 代码逻辑说明
threshold_table是一个二维数组,第二列始终是复位阈值。对于高报通道,复位阈值小于触发阈值;对于低报通道(CH2),复位阈值大于触发阈值。check_alarm函数内部对 CH2 做了特判,这样其他三个通道走通用逻辑。alarm_state数组记录状态机,而不是每次比较都直接驱动蜂鸣器——这种写法支持后续增加“报警延时确认”功能:比如连续 3 次采样都超限才真正报警,过滤掉短暂干扰。
参数设置的依据是:CH0_HIGH_ON设 200,对应电压200/255 * 5V = 3.92V。如果传感器满量程对应 150℃,那就是 150℃ 的 78.4%,大约 117.6℃ 触发报警。想改报警点,只需修改这几行宏定义,不用触碰其他代码。
3.2.2 低报和高报的对称性设计
低报回差恰好和高报相反。想象液位传感器的输出:液位越低,输出电压越低。设定液位低于 40 时报警,但液位缓慢回升到 55 以上才解除。这个 15 的回差值,对应 0.29V 电压,约等于 5% 满量程。过小的回差会让加水过程里报警反复,过大回差会让实际控制点偏离设定值。经验值:回差取满量程的 2%~5% 比较稳妥。8 位分辨率下,满量程的 3% 约等于 7~8 个 LSB,上面代码里取 10~15 略偏大,但教学项目不容易出问题。
3.3 报警输出驱动:蜂鸣器、LED、继电器
单片机引脚只能输出约 20mA 电流,直接驱动蜂鸣器没问题,但继电器必须用三极管或 ULN2003 放大。常见驱动电路是 PNP 三极管 8550 接蜂鸣器到 5V,单片机引脚输出低电平驱动——这利用了灌电流模式,比高电平推挽输出更可靠。同时需要并在蜂鸣器两端一个续流二极管(1N4148),否则关断瞬间的反向电动势可能打坏 51 的引脚。仿真图里如果看到蜂鸣器直接悬空在 P1.0 上,那只是教学简化,制板绝对不能这么干。
LED 指示每路报警时,注意共阳极接法:LED 正极接 5V,负极经过限流电阻(330Ω~1kΩ)到单片机引脚,引脚拉低点亮。4 路报警对应 4 个 LED,加上蜂鸣器,一共 5 个驱动端口。
4. 原理图到物料清单,Proteus 仿真里都可能踩什么坑
4.1 最小系统电路搭建
51 单片机最小系统三要素:复位电路(10µF 电解电容 + 10kΩ 电阻接高电平复位)、晶振电路(12MHz 晶振 + 2 个 22pF 负载电容)、电源去耦(每个 IC 的 VCC 和 GND 之间并 0.1µF 陶瓷电容)。
ADC0832 的 Vref 默认接 VCC,但为了稳定性,建议在 Vref 和 GND 之间并联 10µF 电解电容和 0.1µF 陶瓷电容各一个。模拟输入通道 CH0~CH3 前各串一个 1kΩ 电阻做限流保护,防止传感器异常输出高压打坏 ADC 输入端。如果模拟信号来自 4-20mA 环路,需要并一个 250Ω 精密电阻转换成 1-5V。
在 Proteus 仿真原理图里,ADC0832 的型号选择要特别注意:“ADC0832” 在元件库中可能显示为 ADC0832,但它的引脚顺序和实际芯片有差异。做仿真前双击元件,确认电源引脚 VCC 和 GREF 存在。Proteus 某些版本的 ADC0832 模型没有完整的模拟输入引脚,会用IN0/IN1代替CH0/CH1,接电压源时直接标注直流电压即可。
4.2 物料清单的完整版
做一块双面板,核心物料清单如下:
| 类别 | 元件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|---|
| 主控 | 单片机 | STC89C52RC | 1 | DIP-40,带 8KB Flash |
| 模数转换 | ADC | ADC0832(或 ADC0834) | 2(或 1) | 标题方案用双片 |
| 显示 | LED | 红色 5mm | 4 | 每路报警指示 |
| 声音 | 蜂鸣器 | 有源 5V | 1 | 灌电流驱动 |
| 驱动 | 三极管 | 8550(PNP) | 1 | 蜂鸣器驱动 |
| 二极管 | 1N4148 | 1 | 续流 | |
| 晶振 | 12MHz | 1 | 配合 22pF 电容 x2 | |
| 电容 | 10µF/25V 电解 | 2 | 复位和电源滤波 | |
| 电容 | 0.1µF 陶瓷 | 5 | 每 IC 电源旁路 | |
| 电容 | 22pF 陶瓷 | 2 | 晶振负载 | |
| 电阻 | 10kΩ | 2 | 复位和上拉 | |
| 电阻 | 1kΩ 排阻 | 1 | 模拟输入限流 | |
| 电阻 | 330Ω | 4 | LED 限流 | |
| 接插件 | 排针/接线端子 | 若干 | 传感器接口 | |
| 传感器 | 电位器 10kΩ | 4 | 仿真时模拟传感器输出 |
4.2.1 关键元件选型理由
STC89C52RC 选它是因为下载方便——支持串口 ISP 下载,不用买专用编程器,USB-TTL 模块就能烧录。当然,AT89S52 也行,但需要 ISP 下载线。ADC0832 为什么建议备选 ADC0834:如果 PCB 已经按双片 ADC0832 布局,改为单颗 ADC0834 能省出两个引脚给按键或数码管,而且代码改动极小——只需在通道选择命令里增加一位地址。
蜂鸣器必须是有源蜂鸣器。无源蜂鸣器需要单片机输出 2-4kHz 方波才能响,有源蜂鸣器内置振荡电路,通电即响。判断方法:看器件侧面有没有标“H”或“有源”字样。LED 限流电阻取值 330Ω 在 5V 电源下电流约 12mA,足够亮且不超 51 引脚的最大灌电流 20mA。
4.3 Proteus 仿真图的搭建步骤
打开 Proteus 8 系列,按以下顺序连线:
- 放置 MCU:从元件库选
AT89C51或STC89C52。Proteus 自带 AT89C51 模型,STC 型号在仿真库里也有,但实际下载到单片机的是同一个内核,仿真用 AT89C51 完全等价 - 放置 ADC0832:元件关键字
ADC0832,确认能找到双排 8 脚或 14 脚封装。如果找不到,搜索ADC0834或ADC0838,但要注意引脚个数不同 - 放 4 个电位器:元件关键字
POT-HG或POT-LIN。默认阻值 10kΩ,输出端接 ADC 的 CH0-CH3 输入 - 放置虚拟仪器:左侧工具栏选 “Virtual Instruments” → “DSO” 示波器,或者直接放一个电压表看采样电压
- 连线时注意 ADC 的 CS、CLK、DATA 分别对应哪几个单片机引脚,要和源码里的宏定义一致
完成连线后,双击单片机载入 Hex 文件:右键 MCU → “Edit Properties” → “Program File” 选择 Keil 编译输出的.hex文件,然后点左下角播放按钮运行。如果仿真里 ADC 数据始终为 255 或 0,检查 DATA 引脚是否有上拉——Proteus 模型在数字量读取时对手动置 1 的依赖比真实芯片更强,可以尝试在 DATA 线上加一个 10kΩ 上拉电阻到 VCC。
4.3.1 仿真中调节电位器的方法
点中电位器,按键盘上下方向键或直接在属性窗口改 “Position” 值(0~100)。运行仿真时,电位器数值改变后 AD 输出应实时变化。如果数值瞬间跳变但过一会又跳回,可能是仿真步长问题:调低仿真速度(左下角滑块)或暂停后改值再继续。这个现象我在 Proteus 8.13 遇到过,通常是把 “Animation” 速度调节到 50% 以内才稳定。
4.4 源码在 Keil 里的工程配置
Keil 5 里新建工程后,务必确认两个设置:
- Target 选项卡:晶振频率填 12,Memory Model 选 Small
- Output 选项卡:勾选 “Create HEX File”,否则编译后没有 .hex 文件供仿真或烧录
reg52.h头文件是 Keil 自带的,声明了 P0~P3 和中断向量。如果用了_nop_()内建函数,需要包含intrins.h。有些教程里把_nop_()写成NOP(),那是宏定义不同,别混用。
5. 别只验证报警功能,把数据读出来看看
很多人做了这个项目,仿真能响铃,板子能跑,就认为完成了。但报警器本质上是一个数据采集系统,如果只验证“报警”“不报警”两个状态,ADC 线性度是否正常、阈值标定是否准确都无从知晓。最后一件事,是把采样值通过串口发到电脑上,用上位机调阈值、画曲线。
5.1 用串口输出采样值
51 单片机用 TI = 1 轮询方式发送单字节,配合printf重定向,几行代码就能在串口调试助手里看到 4 路电压值。
// 串口初始化:9600bps,方式 1,8 位数据位 void uart_init(void) { SCON = 0x50; // SM0=0, SM1=1, REN=1 TMOD |= 0x20; // 定时器 1 工作在方式 2(自动重装 8 位) TH1 = 0xFD; // 波特率 9600,晶振 11.0592MHz TL1 = 0xFD; TR1 = 1; // 启动定时器 1 } // 重定向 putchar,让 printf 走串口 char putchar(char c) { SBUF = c; while (!TI); TI = 0; return c; } void send_values(void) { unsigned char i; unsigned char adc_val[4]; for (i = 0; i < 4; i++) { adc_val[i] = average_adc(i); // 8 次采样平均 } printf("CH0:%03d CH1:%03d CH2:%03d CH3:%03d\n", adc_val[0], adc_val[1], adc_val[2], adc_val[3]); }参数说明:
- 串口初始化里
TH1 = 0xFD只适用于 11.0592MHz 晶振。如果用 12MHz,误差会变大——9600 波特率下可能会收到乱码。解决:改用 4800 或 2400,此时TH1对应 0xFA printf在 51 平台上默认使用putchar发送每个字符,重定向后所有格式化输出都能从串口出来%03d格式化让数值对齐,方便查看
5.2 阈值验证技巧:用坐标纸画传输曲线
串口输出配合电位器能从 0 调到 255,把数据记录下来,用 Excel 画散点图,斜率就是 ADC 的实际增益。理想曲线是从 (0,0) 到 (255,255) 的直线。如果发现输入 1.25V 时理论值应为 64,但实际读到 66,说明存在偏移误差。偏移误差可以通过软件校准:加一个零点校正系数——读一次接地通道的值,然后所有通道的采样值都减去这个零点偏移。
报警阈值标定的实际方法是:缓慢旋转电位器,记录串口输出的临界值。比如设定高温报警阈值 200,实际观察到 201 时报警、189 时解除,说明滞回区间和设计一致。如果发现应该报警但没报,先检查串口确认采样值是否真的超过阈值,再排查报警驱动电路——这能有效区分是逻辑问题还是硬件问题。
5.3 如何顺手把历史报警记录加上
主循环里每 500ms 检查一次 4 路状态,如果发生状态变化就通过串口输出带时间戳的报警消息。用 51 的定时器 0 做分钟计数,最多能记两个月内的报警时刻。关键代码只有几行:
// 定时器 0 中断里维护秒/分计数 unsigned int second_count = 0; unsigned char minute_count = 0; void timer0_isr(void) interrupt 1 { TH0 = 0x4C; // 50ms @12MHz TL0 = 0x00; second_count++; if (second_count >= 20) { // 20 * 50ms = 1s second_count = 0; minute_count++; } }主循环里对比alarm_state和上一次的状态,若有变化则printf("[%02dmin] CH%d alarm=%d\n", minute_count, ch, new_state);。这里的定时器重装值是 50ms 中断,20 次就是 1 秒。注意别在中断服务函数里做printf——占用时间太长,会丢失定时器溢出。先把变动记录到缓冲区,主循环里再发送。
这个技巧比分毫不差的实时性更有用:你要的不是当下这一刻的数据,而是报警器在什么时间点动作过。配合上面串口输出的实时曲线,这个 4 路模拟量报警器才真正算是一套可以交给现场的装置。
本文还有配套的精品资源,点击获取