简介:面向高精度电压监测与嵌入式测量开发,这份工程包给出STC15系列单片机利用内置SPI与TI 24位Σ-Δ型ADS1256通信的完整实现,适合电子测量、仪器仪表方向的开发者与学习者参考。资源共20个文件,约9.96MB,包含3个h头文件、2个c源文件、Keil工程配置(uvproj/uvopt)、可烧录hex及obj、lst、m51等编译中间文件,并附ADS1256英文手册与STC15F2K60S2数据手册,便于对照寄存器与硬件参数排查问题。内容围绕8路通道采样、单端与拆分输入、7.8MHz晶振配置展开,采样速率可控制在2.5~10sps,实际电压监测误差可减小到0.00001V以内。已有1408人学习下载,可作为高精度监测仪器项目中SPI时序调试、初始化流程与数据处理思路的参考底稿。
1. 一块 24 位 ADC 挂在 8 位单片机上,难点从来不在 SPI
前阵子帮人看一块称重板,主控用的是 STC15,采集端是 ADS1256。对方一开始图省事,用 GPIO 软件模拟 SPI,跑了半个月,读数始终在最后几位跳得厉害,30kSPS 连一半都跑不到。问题不在 ADS1256,也不在称重传感器,而是软件模拟的时序被定时器和串口中断撕得七零八落,时钟高低电平宽度全在抖。
后来改用 STC15W4K 的内部硬件 SPI,四根线接上,代码缩到两百行出头,DRDY 一低就去取数,反而不抖了。这套组合适合的场景其实很明确:主控成本卡死、算力也就那样,但前端非要微伏级分辨率——电子秤、热电偶测温、应变桥、电池内阻测试,都属于这一类。它不追求采样率,追求的是把那 24 位有效位真正兑现出来。
2. 硬件与引脚先对齐:STC15 内部 SPI 与 ADS1256 的时序契约
在写代码之前,有几件事必须先确定下来,否则后面调试会非常痛苦。ADS1256 的 SPI 不是那种“接上就能通”的接口,它有时序上限、有电源域要求,还有一条容易被忽略的 CS 整帧拉低规则。
2.1 电源域和四根线的接法
先看供电。ADS1256 的模拟电源 AVDD 一般给 5V,数字电源 DVDD 手册允许 2.7V 到 5.25V。STC15W4K 系列本身也是宽压,2.5V 到 5.5V 都能跑。这两者的电源关系必须想清楚:
- STC15 跑 5V,那 ADS1256 的 DVDD 也给 5V,两边电平自然匹配,最省事;
- STC15 跑 3.3V,DVDD 也给 3.3V,同样没问题;
- 千万不要出现 5V 的 STC15 去推 3.3V 供电的 DVDD,MISO 上那 5V 会直接灌进 ADS1256 的输入保护二极管,短期能通,长期是玄学故障。
推荐第一种。DVDD 用 5V 时,ADS1256 的输入高电平门限是 0.7×DVDD = 3.5V,STC15 的 5V 输出足够;同时 STC15 的输入高门限约 0.7×VCC,ADS1256 输出的 5V 也够。
接线我一般按下面这张表来:
| ADS1256 引脚 | 方向 | STC15 引脚 | 说明 |
|---|---|---|---|
| CS | 输入 | P3.4 | 普通 IO 做片选,低有效 |
| SCLK | 输入 | P1.5 | 硬件 SPI 时钟输出 |
| DIN | 输入 | P1.3 | MOSI,主机输出 |
| DOUT | 输出 | P1.4 | MISO,主机输入 |
| DRDY | 输出 | P3.2 / INT0 | 数据就绪,低有效,可接外部中断 |
| RESET | 输入 | P3.6 | 低电平复位 |
| PDWN | 输入 | P3.7 | 低电平掉电,正常工作时拉高 |
还有两点容易漏。第一,ADS1256 的 XTAL1/XTAL2 要接 7.68MHz 晶振,这个时钟决定了它的采样速率精度和内部滤波器的陷波点,用内部 RC 或者随便给个 8MHz 都会让 50Hz 抑制跑偏。第二,CS 虽然只有一个从机时可以常拉低,但我建议还是用 IO 控制,因为 ADS1256 对“一帧命令”这个概念很敏感,CS 在中途抬起会导致命令解析错位。
2.2 SPCTL/SPSTAT/SPDAT:三个寄存器定生死
STC15 的 SPI 外设非常精简,就三个寄存器:控制寄存器 SPCTL、状态寄存器 SPSTAT、数据寄存器 SPDAT。另外还有 AUXR1 里的两位用来切换 SPI 引脚组,因为在 STC15W4K 上 SPI 可以映射到 P1.2~P1.5、P2.4~P2.7 或者 P4.0~P4.3 三组。
/* 直接给地址,省得依赖具体型号的头文件 */ sfr SPSTAT = 0xCD; sfr SPCTL = 0xCE; sfr SPDAT = 0xCF; sfr AUXR1 = 0xA2; sbit ADS_CS = P3^4; sbit ADS_DRDY = P3^5; /* 若接 INT0 则用 P3^2 */ sbit ADS_RST = P3^6; sbit ADS_PDWN = P3^7;SPCTL 的位定义要背下来,因为这几位配错一个,现象就是“完全读不到数据”,没有任何提示:
void spi_init(void) { AUXR1 &= ~0x0C; /* 引脚组选择:00 -> SS=P1.2 MOSI=P1.3 MISO=P1.4 SCLK=P1.5 */ SPCTL = 0xD4; /* SSIG=1 忽略SS | SPEN=1 使能 | DORD=0 高位先发 MSTR=1 主机 | CPOL=0 空闲低 | CPHA=1 第二边沿采样 SPR1:SPR0 = 00 -> 4 分频 */ SPCTL |= 0x02; /* 改成 10 -> 16 分频,24MHz 主频下 SCLK = 1.5MHz */ SPSTAT = 0xC0; /* SPIF 和 WCOL 都是写 1 清零 */ }几个参数单独说明一下。SSIG 必须置 1,否则 SS 引脚(P1.2)一旦被外部拉低,SPI 会强制切到从机模式,这是很多人“程序跑着跑着 SPI 就不动了”的真正原因。MSTR 置 1 是主机模式。CPOL=0、CPHA=1 对应 SPI 模式 1,正好是 ADS1256 要求的:SCLK 空闲为低,数据在下降沿移出、上升沿采样。DORD=0 表示 MSB 先行,ADS1256 的 24 位数据就是高位在前。
SPSTAT 的两个标志位也值得说。SPIF 是传输完成标志,WCOL 是写冲突标志——如果你在上一字节还没发完就往 SPDAT 里写新数据,WCOL 会置位,数据直接丢弃。这两个位都是写 1 清零,写 0 无效,这一点和很多 MCU 相反,血泪经验。
2.3 速率上限:为什么 24MHz 主频只能跑 /16 分频
ADS1256 手册给了一条硬约束:SCLK 周期不能小于 4 个主时钟周期。7.68MHz 晶振下就是 520ns,换算成频率大约 1.92MHz。这条线不能越。
STC15 的分频档位是主频除以 4、8、16、32,部分型号还能到 64 和 128。假设主频 24MHz:
| SPR1:SPR0 | 分频 | SCLK 频率 | 能不能用 |
|---|---|---|---|
| 00 | /4 | 6MHz | 超了,读数会错位 |
| 01 | /8 | 3MHz | 超了 |
| 10 | /16 | 1.5MHz | 可以,留了余量 |
| 11 | /32 | 750kHz | 可以,偏保守 |
所以 24MHz 主频下只能用 /16 或更慢。有人会问,1.5MHz 会不会太慢?算一下就知道了:读一次 24 位数据是 4 个字节(1 个命令 + 3 个数据),32 个 SCLK,在 1.5MHz 下约 21µs,加上字节之间轮询 SPIF 的开销,实测 30µs 左右。ADS1256 最高 30kSPS 对应 33µs 一个周期,刚好卡在边上,所以真要跑满 30kSPS,得靠中断 + 精简代码,这点后面会讲。
提示:STM32 之类带 FIFO 和 DMA 的 SPI 可以做到字节间零间隙,STC15 没有,每个字节都要靠软件轮询 SPIF。这反而降低了平均时钟密度,对手册的时序约束是好事。
3. 驱动骨架:从字节收发到寄存器读写
硬件对齐之后,驱动其实是分层的:最底下是一个字节收发,中间是命令帧封装,最上面是寄存器读写和初始化。层次分清楚,出问题时才能一层一层往下压。
3.1 ADS1256 的命令集与寄存器地图
ADS1256 的寄存器不多,常用的就四个,记住这几个够用了:
| 地址 | 名称 | 关键位 | 常用值 |
|---|---|---|---|
| 0x00 | STATUS | 高 4 位是 ID;bit2 ACAL 自动校准;bit1 BUFEN 输入缓冲 | 0x04 |
| 0x01 | MUX | 高 4 位 PSEL 正端;低 4 位 NSEL 负端 | 0x00 差分 / 0x08 单端 |
| 0x02 | ADCON | bit7~6 时钟输出;bit5~4 传感器电流源;bit2~0 PGA | 0x00(PGA=1) |
| 0x03 | DRATE | 数据速率 | 0x82 = 100SPS |
命令层面,真正用到的也就几条:
| 命令 | 编码 | 用途 |
|---|---|---|
| WAKEUP | 0x00 | 从待机唤醒 |
| RDATA | 0x01 | 读一次转换结果 |
| RDATAC | 0x02 | 进入连续读模式 |
| SDATAC | 0x03 | 退出连续读模式 |
| RREG | 0x10 | reg | 读寄存器 |
| WREG | 0x50 | reg | 写寄存器 |
| SELFCAL | 0xF0 | 自校准 |
| SYNC | 0xFC | 同步,切换通道后常用 |
| RESET | 0xFE | 软复位 |
这里有个关键点:上电或复位之后,ADS1256 默认处于 RDATAC 模式,也就是“SCLK 一来就吐数据”。在这个模式下写寄存器是无效的,必须先发 SDATAC 退出。这一条不知道坑过多少人,现象就是“寄存器写进去了,读回来还是默认值”。
3.2 字节级收发函数
底层就一个函数,收发合一。因为 SPI 是全双工,发一个字节的同时就在收一个字节,所以收数据时也要送一个哑字节去产生时钟。
/** * 收发一个字节 * @param dat 要发送的字节 * @return 同时接收到的字节 */ unsigned char spi_xfer(unsigned char dat) { SPSTAT = 0xC0; /* 清 SPIF / WCOL */ SPDAT = dat; /* 写 SPDAT 即启动一次传输 */ while ((SPSTAT & 0x80) == 0); /* 等 SPIF 置位,实际项目里要加超时 */ SPSTAT = 0xC0; /* 再清一次,防止 WCOL 残留 */ return SPDAT; /* 读 SPDAT 取回接收字节 */ }逻辑很直白:写 SPDAT 启动时钟,硬件自动发 8 个 SCLK,发完置 SPIF,读 SPDAT 拿到 MISO 上的数据。参数只有一个,就是待发送字节;返回值是同时收到的字节,发送时可以忽略。
要提醒的是那个while循环。如果 SPI 初始化没做对,或者 SCLK 被外部短路,这个循环会死等。我一般会加一个计数器,超过几千次就报错返回,否则现场调试时根本分不清是死机还是等数据。
3.3 WREG / RREG 的封包格式
ADS1256 的寄存器读写有个“数量字节”的设计:命令字节后面跟一个字节表示“要读写几个寄存器,数量减一”。只操作一个寄存器时,这个字节就是 0x00。而且 CS 必须从命令头一直拉到数据尾,中间不能抬。
/* 写单个寄存器 */ void ads_wreg(unsigned char reg, unsigned char val) { ADS_CS = 0; spi_xfer(0x50 | reg); /* WREG 命令 + 寄存器地址 */ spi_xfer(0x00); /* 只写 1 个,数量减一 = 0 */ spi_xfer(val); /* 数据 */ ADS_CS = 1; delay_us(2); /* 命令之间留一点间隔 */ } /* 读单个寄存器 */ unsigned char ads_rreg(unsigned char reg) { unsigned char v; ADS_CS = 0; spi_xfer(0x10 | reg); /* RREG 命令 + 寄存器地址 */ spi_xfer(0x00); /* 只读 1 个 */ v = spi_xfer(0xFF); /* 送 0xFF 只是为产生 8 个时钟 */ ADS_CS = 1; delay_us(2); return v; } /* 无参数命令的通用封装 */ void ads_cmd(unsigned char cmd) { ADS_CS = 0; spi_xfer(cmd); ADS_CS = 1; delay_us(2); }三个函数参数都很简单:reg是寄存器地址(0x00 到 0x0A),val是要写入的值,cmd是命令编码。spi_xfer(0xFF)里的 0xFF 没有任何含义,纯粹是因为主机不发时钟就收不到数据,送什么值都行,习惯上写 0xFF。
delay_us(2)这行别删。ADS1256 手册要求命令之间留 t6 量级的间隔,7.68MHz 下不到 1µs,但 C51 的函数调用开销本来就不确定,留 2µs 更稳妥。省掉这行的后果是偶发性的寄存器写失败,属于最难查的那类问题。
4. 上电初始化到取数:一条完整的采集链路
把上面这些拼起来,就是一条从复位到拿到电压值的完整链路。顺序错一步,后面全是白忙。
4.1 复位、PDWN 与 SDATAC 的顺序
上电时序我踩过一次坑:只做了软件 RESET 命令,没拉硬件 RESET 引脚,结果有一批板子冷启动时 DRDY 一直不低。原因是 ADS1256 内部振荡器起振需要时间,软件复位命令在时钟还没稳的时候发过去,芯片根本没收到。
/** * 初始化,返回 0 表示成功 */ unsigned char ads1256_init(void) { ADS_CS = 1; ADS_PDWN = 1; /* PDWN 高电平 = 正常工作 */ ADS_RST = 0; delay_ms(10); ADS_RST = 1; /* 硬件复位,给内部振荡器留时间 */ delay_ms(20); ads_cmd(0xFE); /* 再补一条软件 RESET,双保险 */ delay_ms(5); if (ads_wait_drdy(100)) /* 等复位完成,DRDY 会拉低一次 */ return 1; ads_cmd(0x03); /* SDATAC:退出连续读模式,否则写寄存器无效 */ ads_wreg(0x00, 0x04); /* STATUS:使能自动校准 ACAL */ ads_wreg(0x01, 0x00); /* MUX:AIN0-AIN1 差分 */ ads_wreg(0x02, 0x00); /* ADCON:关时钟输出,关电流源,PGA=1 */ ads_wreg(0x03, 0x82); /* DRATE:100SPS */ return 0; }ads_wait_drdy是个带超时的等待函数,这里就不展开了,逻辑就是循环判断 DRDY 引脚,超时返回错误码。参数单位是毫秒,100ms 对复位来说足够宽裕。
顺序上,硬件复位 → 软件复位 → 等 DRDY → SDATAC → 写寄存器,一步都不能少。特别是 SDATAC 那一步,我把它单独拎出来说:它必须在写任何寄存器之前发,而且只需要一次。
4.2 SELFCAL 与 DRDY 的等待策略
写完寄存器后要做一次自校准。ADS1256 的自校准会测量内部失调和增益误差,把结果存到 OFC 和 FSC 寄存器里,这个过程耗时跟数据速率有关,100SPS 下大概几十毫秒,2.5SPS 下能到一秒多。
/* 接在上面的 init 后面 */ ads_cmd(0xF0); /* SELFCAL */ if (ads_wait_drdy(2000)) /* 校准完成后 DRDY 会拉低 */ return 2;这里的关键是等 DRDY 而不是死等固定时间。不同 DRATE 下校准时间差十倍以上,用固定延时要么不够要么浪费。至于 DRDY 的等待方式,100SPS 这种低速场景直接轮询就够了,CPU 闲着也是闲着;上了 1kSPS 以上就该考虑接 INT0 了。
4.3 RDATA 读 24 位与符号扩展
取数是整个驱动里最核心的一段。ADS1256 输出 24 位二进制补码,范围是 -8388608 到 +8388607。
/** * 读一次原始码值,返回 -8388608 ~ +8388607 */ long ads1256_read_raw(void) { long v; unsigned char b0, b1, b2; if (ads_wait_drdy(200)) /* DRDY 低电平说明有新数据 */ return 0; ADS_CS = 0; spi_xfer(0x01); /* RDATA 命令 */ delay_us(2); /* 手册要求的 t6 间隔 */ b0 = spi_xfer(0xFF); /* 高 8 位 */ b1 = spi_xfer(0xFF); /* 中 8 位 */ b2 = spi_xfer(0xFF); /* 低 8 位 */ ADS_CS = 1; v = ((long)b0 << 16) | ((long)b1 << 8) | b2; if (v & 0x00800000L) /* 第 23 位是符号位 */ v |= 0xFF000000L; /* 手工符号扩展到 32 位 */ return v; }几个细节。第一,spi_xfer(0x01)之后必须等一小会儿再读数据,手册里叫 t6,7.68MHz 下不到 1µs,但 C51 的函数返回加上位操作差不多就过去了,写个 2µs 更保险。第二,三个数据字节必须连续读,中间 CS 不能抬。第三,符号扩展必须做,否则负电压读数会被当成一千六百万多,显示出来就是“电压突然飙到满量程”。
关于第三点再多说一句。C51 里long是 32 位有符号,把 24 位补码塞进去做算术运算是可以的,但直接赋值v = b0<<16|b1<<8|b2之后,编译器不知道这是补码,得手工做符号扩展。这是 24 位 ADC 驱动里最经典的一步。
4.4 从原始码到电压值
码值换算公式很简单:差分输入下,满量程是 ±VREF/PGA,一个 LSB 等于2 × VREF / (PGA × 2^24)。VREF 取 2.5V、PGA=1 时,LSB ≈ 0.298µV。
/* 浮点版:适合显示层,低频调用 */ float raw_to_uv(long raw) { return (float)raw * (5000000.0f / 16777216.0f); /* ±2.5V 对应 ±5,000,000 uV */ } /* 定点版:高频循环里用,(raw>>4)*305>>6 是 0.298 的近似, 中间结果最大约 1.6e8,不会撑爆 32 位 long */ long raw_to_uv_int(long raw) { return ((raw >> 4) * 305L) >> 6; }浮点版直观,但 Keil C51 的 float 是软件模拟的,一次乘除要几百微秒,放在 100SPS 的采集循环里已经吃掉一半 CPU。定点版精度损失约 0.06%,对绝大多数应用足够,且只做一次乘法和两次移位,几微秒就出来了。
注意:定点版的
(raw>>4)会截掉低 4 位,导致小信号时读数跳变为 0。如果测的是微伏级缓变信号,还是老老实实用浮点,但把它挪到显示/通信层,采集中断里只搬原始码。
5. 避坑排查
本文还有配套的精品资源,点击获取