简介:底板程序V1.1是一套面向STC15系列单片机与ADS1232高精度ADC的嵌入式工程包,专为需要实现24位数据采集与串行通信(RS-232/RS-485)的开发者设计,适用于称重、应变测量、工业仪表等低噪声模拟信号处理场景。包内共70个文件,以C源文件、头文件、目标文件及列表文件为主,另含烧录文件、工程文件、启动文件和编译记录;所有文件合计仅236KB,结构紧凑,便于查看各模块编译细节。当前已有163人浏览学习,适合正在调试ADS1232驱动或搭建相关数据采集系统的电子工程师参考。工程代码按功能拆分为ADC驱动、RS485通信、按键、显示等独立模块,主程序中给出了整体调度逻辑,程序更改记录文件保留了版本迭代说明,配合各模块的列表反汇编输出,可清晰还原ADC初始化、通道选择、数据读取与串口发送的完整流程;寄存器定义与配置头文件汇总了外设参数,方便快速移植到同系列项目中,从而缩短数据采集系统的开发周期。
1. ADS1232 与 STC15F32S2 的这套底板程序,为什么值得拆开看
如果你正在做称重仪表、拉压力采集或者高精度工业变送器,大概率会遇到这样一个组合:一颗 ADS1232 负责把传感器毫伏级信号放大并转成 24 位数字量,一颗 STC15F32S2 负责读数据、算标定、显示和对外通信。这套底板程序 V1.1 就是围绕这两个芯片展开的,工程名 RS485_Ctrl 直接点明了它的重点:把高精度采样结果通过 RS485 总线稳定地送出去。程序包里既有 ad1232.c 这样的底层驱动,也有 e2prom.c 负责校准参数掉电保存,还有 key.c、disled.c 这类人机交互模块,完整度接近一个商品级仪表的雏形。对想快速跑通 ADS1232 采集链路的人而言,这份代码比零散的数据手册示例更值钱,因为它把“能用”和“好用”之间的那段距离填上了不少。
2. ADS1232 驱动实现:从 ad1232.c 看 24 位 ADC 的时序与采样配置
2.1 模块划分:底板程序的“洋葱结构”
程序包里的文件不是随手堆在一起的。main.c 做初始化与主循环调度,ad1232.c 专管 ADS1232 的读写时序,rs485.c 和 uart1.c 负责串口与 RS485 收发,e2prom.c 封装 STC15F32S2 内部 EEPROM 的 IAP 操作,key.c 扫描按键,disled.c 驱动数码管或 LED,volforce.c 负责把 ADC 原始码值换算成电压或力值,kout.c 很可能处理按键输出的业务逻辑,timer.c 提供系统节拍。这种分层方式和在 STM32 上做 HAL 驱动如出一辙,只是把对象换成了 51 单片机。好处是调传感器时不用翻通信代码,改协议时不用动 ADC 初始化。
对 STC15F32S2 来说,主频可以跑到 24MHz 甚至更高,执行速度上并不拖后腿。真正限制吞吐量的反而是 ADS1232 本身的输出速率,默认 10SPS 或者 80SPS。所以工程里用轮询读取完全够用,不需要为这个低速 ADC 专门挂中断。理解了这一点,再看 ad1232.c 里的延时和等待逻辑,就会明白那些时间参数是顺着硬件特性来的。
2.2 ADS1232 接线与初始化:用引脚控制代替 SPI 时序
ADS1232 是 24 位 Δ-Σ 型 ADC,内部有 PGA 可编程放大器,支持两路差分输入,增益和输出速率靠外围引脚电平选择。它的串行接口类似 SPI,但又不是标准 SPI:只有一根 SCLK 和一根 DOUT,没有片选,数据输出的第一位由 DOUT 的下降沿标志。典型接法是 DOUT 接 P1.0,SCLK 接 P1.1,PDWN 接 P1.2,MODE 接 P1.3。PDWN 拉低时芯片进入掉电模式,拉高后开始转换;MODE 接高电平选择 10SPS,接低电平选择 80SPS。10SPS 下内部数字滤波器对工频干扰抑制更好,适合称重这类低速高精度场景。
初始化代码一般长这样:
// ad1232.h 中定义引脚 sbit ADS_DOUT = P1^0; sbit ADS_SCLK = P1^1; sbit ADS_PDWN = P1^2; sbit ADS_MODE = P1^3; // 1:10SPS 0:80SPS void ADS1232_Init(void) { ADS_SCLK = 0; ADS_PDWN = 0; // 先进入掉电模式 delay_ms(10); ADS_PDWN = 1; // 唤醒,开始转换 ADS_MODE = 1; // 选 10SPS,噪声更低 delay_ms(200); // 等待内部模拟滤波器和参考源稳定 while(ADS_DOUT == 1); // 等待第一个转换周期完成 }这里的 200ms 延时不是随手写的。ADS1232 上电后,内部 Δ-Σ 调制器、数字滤波器和参考电压需要时间建立,如果立即读取,前几帧数据往往是乱跑的数。把 MODE 引脚在初始化时固定为高或低,之后不要再动态切换,否则每次切换都要重新等待一个完整的转换周期。需要更高采样率时再把 MODE 拉低,并把后面的 while 循环改成带超时判断,避免程序死在等待上。
2.3 读取 24 位数据与符号扩展
读取函数的核心是一个 24 次的移位循环。ADS1232 在 SCLK 上升沿把数据送到 DOUT,单片机应该在 SCLK 为高电平时采样 DOUT。下面这段代码是常见的实现方式:
long ADS1232_Read(void) { unsigned char i; long value = 0; while(ADS_DOUT == 1); // 等待 DOUT 拉低,表示数据准备好 for(i = 0; i < 24; i++) { ADS_SCLK = 1; value = value << 1; if(ADS_DOUT == 1) value |= 1; ADS_SCLK = 0; } // 第 25 个脉冲使 DOUT 变高,释放数据线 ADS_SCLK = 1; ADS_SCLK = 0; // 24 位有符号扩展,否则负数会变成正数 if(value & 0x800000) value |= 0xFF000000; return value; }循环结束后,ADS1232 会要求给出第 25 个 SCLK 脉冲,DOUT 才会变高,否则下一次读取时无法用下降沿判断数据是否就绪。另一件容易被忽略的是符号扩展。ADS1232 输出的 24 位二进制补码,最高位是符号位;如果直接把读到的值赋给 32 位 long 变量,负值会变成超过 0x7FFFFF 的大正数。手动用if(value & 0x800000)把高 8 位补 0xFF,才能得到正确的负向力值。
连续多次读取做算术平均,是降低数字噪声最简单的手段。在 10SPS 采样率下,取 8 次平均可以把峰峰抖动从几十个 LSB 压到十个 LSB 以内。如果做的是动态测量,平均次数太多会滞后响应,这时可以改成滑动平均窗口,只保留最近 4 次结果。
2.4 增益与参考电压的选型依据
ADS1232 的模拟输入不是随便接的。它内部有固定增益,常见配置是 64 倍或 128 倍,具体由芯片版本和引脚决定。配称重传感器时,灵敏度 2mV/V、激励电压 5V 的桥路满量程输出只有 10mV,用 128 倍增益后接近 1.28V,刚好匹配 ADS1232 的差分输入范围。如果传感器输出太大,PGA 会饱和,读出来的满码直接卡在 0x7FFFFF 或 0x800000。判断方法很简单:在空载时读一次零位,加满量程砝码再读一次,两次差值换算成电压应该落在 ADS1232 允许的输入范围内。超出范围就改用 64 倍增益,或者降低传感器激励电压。
3. RS485 通信链路:uart1.c 与 rs485.c 是怎么把数据送出去的
3.1 串口初始化:定时器 2 作波特率发生器
STC15F32S2 的 UART1 可以工作于模式 1,也就是 8 位可变波特率格式。波特率发生器用定时器 2 最方便,因为定时器 1 可能还要用来做其他计时。工程里同时出现 timer.c 和 uart1.c,说明这两部分功能是分开的。在 11.0592MHz 晶振下,把定时器 2 配成 1T 模式,9600bps 的重装值是 0xFFE8。初始化代码通常是这样:
void UART1_Init(void) { S1CON = 0x50; // 0101 0000: 模式1,8位可变波特率,REN=1 CLK_DIV &= 0xCF; // 外设时钟不分频 T2L = 0xE8; // 9600bps @ 11.0592MHz T2H = 0xFF; AUXR |= 0x14; // T2R=1,T2x12=1(1T模式) IE2 |= 0x01; // 使能 UART1 接收中断 }重装值的计算方法是:65536 - Fosc / (4 × Baud)。代入 11.0592MHz 和 9600,得到 65248,也就是 0xFFE8。注意这里必须是 1T 模式,如果误用了 12T 模式,实际波特率会变成 800bps,上位机收到的全是乱码。STC15F32S2 的最高波特率可以做得比较高,但 RS485 总线上如果挂的设备多、线缆长,9600 反而是最不容易出错的选择。别为了一点点速度提升去跑 115200,工业现场的长线电容会把上升沿磨圆。
3.2 RS485 方向切换与发送时序
RS485 是半双工,需要一个 IO 控制 MAX485 的 DE/RE 引脚。DE 为高时芯片处于发送状态,DE 为低时处于接收状态。问题在于方向切换不是瞬间完成的,单片机从写 SBUF 到 TX 引脚真正出现起始位,中间有一段不确定的延迟;如果 DE 刚拉高就写第一个字节,接收方很容易丢掉帧头。我在项目里习惯这样写发送函数:
sbit RS485_DE = P3^7; void RS485_SendData(unsigned char *buf, unsigned char len) { unsigned char i; RS485_DE = 1; // 进入发送模式 delay_ms(1); // 等总线收发器稳定 for(i = 0; i < len; i++) { SBUF = buf[i]; while(TI == 0); TI = 0; } while(1) if(TI == 1) break; // 等最后一位彻底移出 TI = 0; RS485_DE = 0; // 回到接收模式 }这个函数里多做了一个“再等一次 TI”的动作,因为最后写入 SBUF 的字节可能在跳出循环时还没有完全发送结束。立刻把 DE 拉低会截断停止位,对端可能报错。delay_ms(1)是保守做法,如果你的板上光耦延迟很小,可以改成延时 500us 或 50 个空指令。多次调试后你会发现,RS485 通信不稳定,十次里有三四次丢包,多半不是校验算法问题,而是方向切换的边沿没处理好。
3.3 接收中断与数据帧解析
接收侧数据是碎片化进入的,需要一个静态缓冲区暂存。串口中断里只负责把字节塞进数组,解析动作放在主循环,这样能避免在中断里做复杂判断。常见做法是:
unsigned char rx_buf[8]; unsigned char rx_cnt = 0; unsigned char rx_flag = 0; void UART1_Isr(void) interrupt 4 { if(RI) { RI = 0; rx_buf[rx_cnt++] = SBUF; if(rx_cnt >= 8) rx_cnt = 0; // 防止越界 } }主循环里判断 rx_buf[0] 和 rx_buf[1] 是否为固定的帧头,再对最后一个字节做累加和校验。帧长度越短,出错概率越低。我一般会用 5 到 6 字节的帧,例如:帧头 0xAA、命令字、数据高字节、数据低字节、校验和。这种协议在 9600 波特率下整个帧传输时间不到 7ms,非常适合仪表类轮询。
4. 从“底板”到系统:EEPROM、按键与状态指示的协同工作
4.1 校准参数存储:e2prom.c 里的 IAP 操作
任何称重系统都需要校准,零点和增益存入 STC15F32S2 的内部 EEPROM。使用内部 IAP 时,先要去查对应型号的头文件,因为 STC15F32S2 和 STC15F2K60S2 的 IAP 寄存器地址略有差别。写入前需要设置使能控制寄存器,触发一次操作后再关闭,避免意外写入。读写的核心代码:
void EEPROM_WriteByte(unsigned int addr, unsigned char dat) { IAP_CONTR = 0x80; // 使能 IAP IAP_CMD = 0x02; // 字节编程 IAP_ADDRL = addr & 0xFF; IAP_ADDRH = (addr >> 8) & 0xFF; IAP_DATA = dat; IAP_TRIG = 0x5A; // 触发 IAP_TRIG = 0xA5; IAP_CONTR = 0x00; // 关闭 IAP } unsigned char EEPROM_ReadByte(unsigned int addr) { unsigned char dat; IAP_CONTR = 0x80; IAP_CMD = 0x01; // 读 IAP_ADDRL = addr & 0xFF; IAP_ADDRH = (addr >> 8) & 0xFF; IAP_TRIG = 0x5A; IAP_TRIG = 0xA5; dat = IAP_DATA; IAP_CONTR = 0x00; return dat; }写校准值前强烈建议先擦除整个扇区,或者对同一个地址先写后读回校验。如果中途掉电,EEPROM 内容可能处于不确定状态,设备带着错误增益跑起来很难发现。好的习惯是保存结构体时附带两个字节 CRC,启动时先校验再决定是用默认参数还是 EEPROM 参数。这套底板程序里 e2prom.c 和 volforce.c 配合,其实已经隐含了“读参数、算物理量”的流程。
4.2 按键与指示灯在产线调试中的价值
key.c 和 disled.c 这两个模块看似简单,但在量产和现场调试时特别有用。比如长按按键进入“零点校准”,再按一次进入“满量程校准”,同时数码管显示当前采集码值,整个过程完全不需要电脑。下面是一个典型的处理框架:
if(Key_Scan() == KEY_SHORT_PRESS) { if(display_mode == 0) display_mode = 1; // 切换显示原始码值 else display_mode = 0; } else if(Key_Scan() == KEY_LONG_PRESS) { calibration_step++; // 进入校准流程 EEPROM_WriteByte(CAL_ADDR, calibration_step); }LED 还承担着通信状态指示的功能。RS485 每发送一帧数据就翻转一次某个 LED 引脚,调试时不用接示波器,看灯闪不闪就能判断主从设备有没有在交互。产线工人不需要理解协议,只看指示灯就能知道通讯正常与否,这是很多成熟仪表通用的设计。
4.3 常见故障排查表
把多个项目里遇到过的状况汇总成下面这张表,可以按顺序排查:
| 现象 | 可能原因 | 检查点 |
|---|---|---|
| ADC 读回全 0 | DOUT 一直为高 | PDWN 电平,SCLK 是否存在多余脉冲 |
| ADC 读回数值跳变 | 模拟电源纹波过大 | ADS1232 的 AVDD 用独立 LDO,加 100nF 去耦和 10uF 储能电容 |
| 串口乱码 | 波特率误差 | 定时器 2 是否为 1T 模式,重装值对不对 |
| RS485 首帧丢失 | 方向切换太快 | 增大 DE 拉高到写入 SBUF 的延时 |
| EEPROM 校准值失效 | 写入未做校验 | 保存后回读,加入 CRC 或 double copy |
ADC 跳变的问题往往不是程序 bug。ADS1232 的参考电压源在芯片内部,如果 AVDD 和 DVDD 共用一个纹波很大的开关电源输出,采样结果的末几位会像水波一样来回摆。把模拟电源和数字电源隔开,地线走星型接地,比在代码里多滤几十次更有效。另外 SCLK 引脚最好接一个 100Ω 的串联电阻,减小振铃,防止快速上升沿耦合到 DOUT 上导致误采样。
5. 移植与验证:用一条命令打通采集到回传的链路
5.1 验证整个系统的单片测试法
我拿到新底板第一件事,是写一段最精简的“单次采集应答”代码。上位机发一个字节 0x01,设备读一次 ADC,把 24 位原始值拆成三个字节,加上帧头帧尾和校验回传。代码片段:
if(rx_buf[0] == 0x01) { long adc = ADS1232_Read(); unsigned char out[5]; out[0] = 0xAA; out[1] = (unsigned char)(adc >> 16); out[2] = (unsigned char)(adc >> 8); out[3] = (unsigned char)(adc & 0xFF); out[4] = out[1] ^ out[2] ^ out[3]; // 简单校验 RS485_SendData(out, 5); }这段代码同时验证了 ADS1232 读取、串口中断、RS485 方向切换和校验逻辑。如果上位机收到的值在传感器空载和加载时按预期变化,就说明整条采集链路没问题。之后再去调 EEPROM 和按键,能节省大量定位时间。需要注意的是,adc >> 16时如果 adc 是负数,算术右移会把符号位带进来,拆包时只取低字节,所以不受影响。
5.2 换到其他芯片时的最小改动清单
把这份代码从 STC15F32S2 移植到 STC15W408AS 或 STC8G 系列,核心驱动不用大改,改三个地方就够:
- 引脚定义:ad1232.h 里的 sbit 管脚重新映射。
- 波特率初值:根据新系统晶振频率重算定时器 2 的重装值。
- EEPROM 寄存器名:STC8 系列的 IAP 触发时序和地址寄存器名称不同,需要查对应头文件。
移植后不能直接用,要做一个“回环测试”:把 RS485 的 A/B 输出短接到主机端,发送 0x55 看返回值是否一致。这一步能快速暴露波特率误差和方向切换问题。最后在 ADS1232_Read 函数开头加一个带超时的等待计数器,DOUT 长时间不拉低就返回一个错误标志,避免程序卡死在while(ADS_DOUT == 1)里;超时值设为 200ms,刚好大于 10SPS 的转换周期。这样即使传感器断线也有机会触发报警,而不是整机死锁。
本文还有配套的精品资源,点击获取