简介:面向STM32F103RCT6微控制器的STM32_ADS1256驱动程序,主要服务于嵌入式开发者和电子工程师,解决高精度模拟数字转换器ADS1256的数据采集与控制问题,并借助TFT屏幕实现模拟信号实时显示,适用于工业测控、科学实验等需要精确测量微小电压的场景。驱动基于STM32的SPI接口实现与ADS1256的高速通信,支持多输入通道配置、可编程增益放大及多种采样率设置,程序已针对原子mini板进行适配,包含ADS1256.C、lcd.c、usart.c、timer.c等完整源码模块,可直接接手二次开发。压缩包为ZIP格式,共260个文件,以C源码、头文件、编译中间文件(.o、.d、.crf)以及Keil工程配置为主,另包含可烧录的hex文件和axf调试文件,整体仅7.84MB,便于下载与工程部署。已有1645人学习,通过学习这份驱动,可掌握STM32 HAL库下SPI通信配置、ADC转换结果读取、TFT屏幕数据格式化显示等关键环节,也能理解中断服务程序如何保证数据采集实时性,为更高精度的小信号测量项目提供可直接复用的模板。
1. STM32 驱动 ADS1256:把 24 位 ADC 的精度从“纸面参数”变成实测数据
当你要采集称重传感器的 1mV 级信号,用 STM32 内置 12 位 ADC 往往会陷入“满刻度都没占满,噪声已经把有效位吃掉一半”的窘境。换成 ADS1256 这颗 24 位 ADC 之后,同样的传感器,曲线立刻可用。这份 STM32_ADS1256 驱动程序覆盖了 SPI 初始化、寄存器配置、DRDY 同步读取、连续转换和避坑处理,不需要你重新翻 datasheet 逐条对时序。适合正在做应变片、热电偶、电化学仪器或者高精度数据采集卡的人。只要你会用 STM32CubeMX 生成基础工程,照着下面的代码和步骤,半小时内可以把数据用串口打出来。
2. 硬件与 SPI 初始化:上电顺序、参考电压和时钟边沿不是玄学
2.1 引脚分配与电源:先把模拟地和数字地处理好
ADS1256 是模拟前端芯片,AGND 和 DGND 之间不能走小阻值直连,常见做法是用 0Ω 电阻连成单点地,让数字返回电流和模拟返回电流在各自主地上回流。模拟电源、数字电源、参考电压各自加 0.1μF 和 10μF 电容,特别注意参考电压引脚 REFP 与 REFN 之间的电容要尽量靠近引脚。若用外部基准,REFN 接 AGND,基准输出接到 REFP。
典型接线如下:
| 引脚 | 方向 | 接 STM32 | 说明 |
|---|---|---|---|
| CS | 输入 | 任意 GPIO | 片选,低有效,多机时控制总线 |
| SCLK | 输入 | SPI_SCLK | 时钟,Mode 1 下空闲为低 |
| DIN | 输入 | SPI_MOSI | 写命令/寄存器 |
| DOUT | 输出 | SPI_MISO | 读数据/寄存器 |
| DRDY | 输出 | 外部中断 GPIO | 转换完成握手信号 |
| RESET | 输入 | GPIO 或 RC | 上电后可拉高释放 |
| SYNC | 输入 | GPIO | 正常拉高,用于重启滤波器 |
| CLKIN | 输入 | 7.68MHz 方波 | 必须提供外部时钟 |
CLKIN 是这部分最容易忽略的点。ADS1256 没有内置振荡器,必须由外部提供主时钟,常见用 7.68MHz 无源晶振,也可以用 STM32 MCO 引脚输出一个 7.68MHz 时钟。用 MCO 的好处是省掉一颗晶振,但 MCO 输出的抖动有时会让噪声变差;对测量需求不敏感可以用,要我选,高精度采样我还是习惯独立晶振。上电后要让晶振起振后再操作,先延时 20ms 以上,否则复位命令可能无效。
2.2 SPI 模式与参数:CPOL 和 CPHA 错一个就读不到数
ADS1256 的 SPI 通信协议要求时钟空闲为低,数据在 SCLK 下降沿被主机采样,对应 STM32 的 SPI Mode 1,即 CPOL = 0,CPHA = 1。很多工程直接拿其他传感器的 CPOL = 0、CPHA = 0 模板来套,结果芯片有 DOUT 输出但数据全错,这是因为芯片在上升沿移出数据,而主机在上升沿也采样,两边正好差半拍。
SCLK 频率也不能想拉多高就拉多高。ADS1256 内部数字接口要求 SCLK 周期不小于主时钟周期的 4 倍,按 7.68MHz 主时钟计算,SCLK 上限约 1.92MHz。STM32 的 SPI 频率必须设置为不超过 1.8MHz,我一般取 1MHz,读取连续数据完全够用。低速率反而更容易通过示波器或逻辑分析仪观察时序,调试初期不建议一上来就开最快。
以下是 STM32CubeMX 生成的 SPI 初始化,用 HAL 库:
void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1,Mode 1 hspi1.Init.NSS = SPI_NSS_SOFT; // 片选手动控制 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 按实际总线时钟换算 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }这里有一处必须改:如果 APB2 时钟是 72MHz,16 分频后 SPI 时钟是 4.5MHz,已经超过 ADS1256 的承受范围;在 CubeMX 里要换成 64 分频或更高,把 SCLK 压到 1.8MHz 以内。数据长度固定为 8 位,不要用 16 位帧,否则读写寄存器时字节顺序会对不上。NSS 必须关闭硬件片选,ADS1256 的 DRDY 握手要求主机在一条完整事务结束之后再拉高 CS,用硬件 NSS 很难做到。
2.3 初始化序列代码:复位、状态寄存器写入与自校准
初始化顺序非常关键。上电后,芯片要等外部晶振准备好,然后复位,再配置寄存器,最后自校准。乱序会导致校准基于错误的配置参数,出来的数据带固定偏移。
先封装底层读写函数:
#define ADS_CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET) #define ADS_CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET) uint8_t ADS_SPI_WriteByte(uint8_t byte) { uint8_t rx; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, 50); return rx; } void ADS_WriteRegister(uint8_t regAddr, uint8_t data) { ADS_CS_LOW(); ADS_SPI_WriteByte(0x50 | regAddr); // WREG 命令,高四位 0101 ADS_SPI_WriteByte(0x00); // 写入寄存器数量减一,0 表示写 1 个 ADS_SPI_WriteByte(data); ADS_CS_HIGH(); }WREG 命令字节由 0x50 和寄存器地址合成。ADS1256 寄存器地址是低四位,所以状态寄存器 0x00 对应 0x50,ADCON 寄存器 0x01 对应 0x51。第二个参数 0x00 表示只写一个寄存器,如果要连续写多个寄存器,这里填 n-1。片选在整条命令完成前不能拉高,否则芯片会丢弃命令。
然后是完整初始化流程:
void ADS1256_Init(void) { HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_RESET); HAL_Delay(5); HAL_GPIO_WritePin(RESET_GPIO_Port, RESET_Pin, GPIO_PIN_SET); HAL_Delay(50); // 等晶振和内部电源起来 ADS_CS_LOW(); ADS_SPI_WriteByte(0xFE); // RESET 命令 ADS_CS_HIGH(); HAL_Delay(20); while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) == GPIO_PIN_SET); // 等 DRDY 拉低 ADS_WriteRegister(0x00, 0x01); // STATUS:开启输入缓冲,寄存器默认值 ADS_WriteRegister(0x01, 0x00); // ADCON:PGA = 1,关闭 CLKOUT ADS_WriteRegister(0x02, 0x08); // DRATE:数据率 30SPS ADS_CS_LOW(); ADS_SPI_WriteByte(0xF0); // SELFCAL 自校准命令 ADS_CS_HIGH(); while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) == GPIO_PIN_SET); // 校准完成 }这段代码有两处等待 DRDY:第一次是等待芯片复位完成,第二次是等待自校准结束。ADS1256 的校准时间长短和 DRATE 有关,数据率越低,校准越慢。如果配置完寄存器后没有等 DRDY 就发 SELFCAL,校准命令会被芯片忽略。还有一个容易踩的细节:SELFCAL 必须在 PGA 和数据率都配置好之后发,否则校出来的偏移是针对另一套参数,后续换增益或换速率时要重新校准。
3. 读取转换结果:单次模式、连续模式和 DRDY 边沿同步
3.1 命令集与 24 位数据格式
ADS1256 的串行接口命令不多,但每条命令的时序要求不一样。先把我在驱动里用到的基础命令列出来:
| 命令 | 字节 | 用途 |
|---|---|---|
| RDATA | 0x01 | 读一次转换结果 |
| RDATAC | 0x03 | 进入连续读数模式 |
| SDATAC | 0x04 | 退出连续读数模式 |
| RREG | 0x20 + 地址 | 读指定寄存器 |
| WREG | 0x50 + 地址 | 写指定寄存器 |
| SELFCAL | 0xF0 | 自校准 |
| SYNC | 0xFC | 同步,用于通道切换后复位滤波器 |
转换结果是以 24 位补码形式输出的,最高位是符号位。三个字节的顺序是 MSB 在前,拼成 32 位整数后还需要做一次符号扩展。要换算成电压,常见工程里用的公式是:
float ADS1256_ToVoltage(int32_t raw, float vref, uint8_t pga) { return (float)raw * (2.0f * vref) / (8388608.0f * pga); }比如 VREF = 2.5V,PGA = 1,那么满量程对应 ±5V,24 位全量程 8388608 个码。这个公式成立的前提是芯片工作在全差分模式,输入范围是 ±2 倍参考电压除以增益。如果你把负输入端接到一块参考地而不是真正的差分信号,实际范围会被共模电压限制,不能完全套用公式。
3.2 单次读取流程:等待 DRDY、发命令、读 3 字节
单次模式是最可靠的数据读取方式,适合多通道切换、低速传感器扫描。流程是:等待 DRDY 变低,发送 RDATA 命令,时钟连续送 24 个脉冲把三个字节读出来。
int32_t ADS1256_ReadSingle(void) { uint8_t buf[3]; uint32_t code = 0; int32_t signed_code; while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) != GPIO_PIN_RESET); // 等新数据 ADS_CS_LOW(); ADS_SPI_WriteByte(0x01); // RDATA 命令 buf[0] = ADS_SPI_WriteByte(0xFF); // 最高字节 MSB buf[1] = ADS_SPI_WriteByte(0xFF); buf[2] = ADS_SPI_WriteByte(0xFF); // 最低字节 LSB ADS_CS_HIGH(); code = ((uint32_t)buf[0] << 16) | ((uint32_t)buf[1] << 8) | buf[2]; if (code & 0x800000) // 24 位补码转 32 位有符号 signed_code = (int32_t)(code | 0xFF000000); else signed_code = (int32_t)code; return signed_code; }DRDY 下降沿代表一组新数据已经准备好,读完当前数据后,DRDY 会自动回到高电平。这里有一个很关键的习惯:不要在 DRDY 还没拉低时就发 RDATA,否则读到的可能是上一次的旧数据,甚至会把芯片内部的读指针搞乱。读数据时 CS 必须保持低电平直到三字节全部收完,中途拉高会导致后续字节丢失。
3.3 连续模式与中断读取:把 SPI 事务让给主循环
RDATAC 连续模式适合高速采样,进入模式后不需要每次发 RDATA 命令,每次 DRDY 变低后直接读三字节。但这个模式也容易出问题:如果你在主循环里读数据,中途又来一个 DRDY 中断,新数据会把正在读的数据覆盖掉,读出来的结果就是高字节和低字节不是同一次转换的。
我常用的做法是在中断里只置一个标志位,真正读数据放到主循环做:
volatile uint8_t ads_ready_flag = 0; volatile int32_t ads_last_raw = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DRDY_Pin) { ads_ready_flag = 1; } }主循环检测到标志后,再执行一次完整的三字节读取:
if (ads_ready_flag) { ads_ready_flag = 0; uint8_t buf[3]; ADS_CS_LOW(); buf[0] = ADS_SPI_WriteByte(0xFF); buf[1] = ADS_SPI_WriteByte(0xFF); buf[2] = ADS_SPI_WriteByte(0xFF); ADS_CS_HIGH(); int32_t raw = ((int32_t)buf[0] << 16) | ((int32_t)buf[1] << 8) | buf[2]; if (raw & 0x800000) raw |= 0xFF000000; ads_last_raw = raw; }如果你想在中断里直接调 HAL_SPI_TransmitReceive,必须确认这个中断优先级不会打断主循环里正在进行的其他 SPI 事务,否则两个 SPI 操作会互相踩踏。STM32 的阻塞式 HAL 传输本身不适合在中断里频繁调用,我建议中断只负责打标记,读取放到主循环。连续模式下如果中途要切换通道,必须先把模式退出到单次,改完寄存器再重新进入,直接改 MUX 寄存器不会立即对当前周期生效。
4. 避坑手册:ADS1256 调试中我踩过的 5 个典型问题
4.1 现象:DRDY 一直保持高电平
拿到板子第一次上电,主程序卡在等 DRDY 的 while 循环里,DRDY 用万用表量始终是 3.3V。
排查后是外部时钟没起来。ADS1256 必须要有 CLKIN 输入才会开始转换,我最初用的是 STM32 MCO 输出 7.68MHz,但 CubeMX 里 MCO 分频配置里把输出频率设成了 1MHz,芯片内部状态机根本没跑起来。解决方法是先用示波器确认 CLKIN 引脚有波形,或者干脆换独立晶振。还有一种情况是 SYNC 引脚悬空被噪声拉低,芯片一直处于同步等待状态;SYNC 要接一个 GPIO 并初始化为高,不能悬空。
4.2 现象:读出的数据全是 0xFFFFFF
初始化没报错,寄存器也能写,但读回来的码值一直是全 1。
原因是 SPI 的相位错了。如果主机的 CLKPhase 配置和 ADS1256 不匹配,DOUT 线上送出来的数据会被主机在错误的边沿采样。尤其当 DOUT 默认是高电平时,全 1 很容易被误解成“芯片坏了”。解决方法是把 SPI 改成 CPOL = 0、CPHA = 1,也就是 Mode 1,同时用逻辑分析仪抓 DOUT 在 SCLK 下降沿时是否是真数据。还有一个隐蔽原因:HAL 库的 SPI 通信如果超时返回会残留旧数据,检查 HAL_SPI_TransmitReceive 的返回值,不能只关注收到的字节。
4.3 现象:低位字节始终为 0,分辨率只有 16 位
输入信号有变化,但读出来最后两个 bit 完全不动,看起来像 16 位 ADC 在工作。
这个现象大多数时候是软件只读了两个字节,第三个字节根本没有进到代码里。比如我用某一次 DMA 配置,把数据长度设成了 2 字节,DMA 自动把第三次时钟忽略掉,芯片侧的数据移位寄存器少移了 8 位,后面每次读都错位。解决方法是检查代码里是不是连续读满三字节,CS 有没有在读第三个字节之前被拉高。另一个原因是 SPI 的数据帧设成了 16 位,第一次收发吞掉 8 个时钟,低字节永远落在第二次传输里。
4.4 现象:单端测量时,负电压或接近 0V 的信号读数跳动很大
用 ADS1256 做单端采集,信号从 AIN0 进,负端直接接 AGND,结果小信号附近数值乱跳。
这本质上不是 ADS1256 的随机噪声,而是共模电压没落在合理区间。ADS1256 的单端模式并不是真正的“对地测量”,它内部仍然是一个差分 ADC,负输入端要接到 AINCOM 通道。如果 AINCOM 接到 AGND,接近 0V 的信号会让输入缓冲接近截止区,线性度变差。解决方法是把 AINCOM 接到一个干净的低阻电位,常见做法是接到一个 2.5V 的虚拟地和信号地之间做单端偏置,让共模处于量程中段。硬要用 AGND 做参考,必须关掉输入缓冲并且严格限制输入电压范围。
4.5 现象:切换通道后前 1~2 个数据明显乱跳
用连续模式扫多个通道,每次切通道后的第一组数据都和实际值差很远,后面的数据才正常。
原因是数字滤波器里还残留旧通道的采样数据。改变 MUX 寄存器后,滤波器要继续处理完当前的数据窗口,你看到的前几个码值其实是新旧通道混叠的结果。解决方法是:先发 SDATAC 退出连续模式,写 MUX 通道选择寄存器,再发 SYNC 命令复位数字滤波器,然后等待 DRDY 变低,丢弃前两个样本后再记录。如果你用的是单次模式,每次切换后也要加一个“丢弃第一帧”的逻辑,尤其在高增益下,第一帧的建立时间影响非常明显。
5. 从“能读”到“读得准”:零点校准与两点标定
5.1 用外部校准补齐芯片自校准的盲区
SELFCAL 只能校掉芯片内部的偏移和增益误差,校不掉传感器、放大网络和基准源引入的系统误差。所以拿到能正常读数之后,我的下一步通常是做外部零点校准和满量程标定,而不是直接信任原始码值。
方法是先让传感器处于零位,读 100 次取平均值,得到一个零点码 zero;再输入一个已知电压标准,读一组平均值,算出每码对应的电压增量:
typedef struct { int32_t zero_code; float scale_code; // 每个码对应多少 mV } ads_calib_t; ads_calib_t calib; float ADS1256_CalibratedVoltage(int32_t raw) { int32_t delta = raw - calib.zero_code; return (float)delta * calib.scale_code; }校准时的关键点是让被测信号和满量程尽量接近但不削顶。比如量程是 ±5V,那就用 4.5V 标准源做高点,比用 2.5V 做的斜率更准。标定的 scale_code 建议多测几个点做拟和,最少也要两点,一个零点,一个接近满量程的高点。
5.2 数据滤波:滑动平均之外再留一手
ADS1256 在 30SPS 下本身的噪声很低,但系统里如果有 50Hz 工频干扰,输出码还是会跳。我一般会先用移动平均滤波,窗口长度取 4、8、16,不能盲目加大,不然响应变慢,动态信号会被拉平。
#define FILTER_LEN 8 int32_t g_buf[FILTER_LEN]; uint8_t g_idx = 0; int64_t g_sum = 0; int32_t ADS1256_MovingAverage(int32_t new_raw) { g_sum -= g_buf[g_idx]; g_buf[g_idx] = new_raw; g_sum += new_raw; g_idx = (g_idx + 1) % FILTER_LEN; return (int32_t)(g_sum / FILTER_LEN); }对于偶尔出现的尖峰干扰,滑动平均不如中值滤波。我的做法是先用 5 点中值滤波干掉脉冲噪声,再套一层 4 点滑动平均。注意中值滤波的窗口长度不能太大,否则信号上升沿会被削掉,最佳效果要配合你的实际采样率来调。
5.3 用逻辑分析仪验证时序,而不是只信眼睛
调试 ADS1256 最有效的工具不是示波器,而是逻辑分析仪。把 CS、SCLK、DIN、DOUT、DRDY 接上采集,看三个地方:DRDY 下降沿到 CS 拉低之间的时间、SCLK 极性是否正确、DOUT 数据是否在第九个时钟沿开始稳定。很多“玄学”问题,抓到波形之后十秒钟就能定位。
从那以后,我每次拿到新板子都会先跑一个只读状态寄存器的冒烟测试:能稳定读回 0x01,再往下调 DRATE、PGA、连续模式。这个习惯帮我躲掉了一大半硬件焊接和 SPI 配置上的低级错误,也希望把你从满地找数据线的状态里捞出来,希望帮到你。
本文还有配套的精品资源,点击获取