简介:这是一套专为STM32F103RCT6设计的ADS1256高精度ADC驱动程序,适配正点原子mini板,适合嵌入式开发者进行多通道模拟信号采集与TFT实时显示。代码基于STM32 HAL库与SPI接口实现高速数据传输,并通过中断服务程序管理转换与读取,可直接用于工业监测、医疗仪器等需要精确测量微小电压的场合。压缩包共260个文件、7.84MB,主要包含C源码、H头文件,以及编译生成的O目标文件、CRF交叉引用、依赖文件、UVProj工程、HEX固件和MAP映射文件等,覆盖从工程配置、编译调试到程序烧录的完整开发链路。源码中ADS1256.c/h负责ADC核心驱动,lcd.c用于屏幕显示,delay、usart、timer等模块提供基础支持,目录结构清晰,便于按需调用和二次开发。已有1645人学习,代码注释较完整,适合需要快速集成ADS1256到STM32项目中的中高级开发者参考,可有效缩短驱动调试周期,并作为学习SPI通信与ADC应用的实例。
1. STM32_ADS1256驱动程序:24位精度为什么一上电就翻车
A同学在某图像处理Demo里换用STM32_ADS1256驱动程序做高精度采集,上电后读数在满量程附近来回跳,换了一片芯片还是一样。折腾一整天,最后发现是SPI模式少配置了一个位。STM32_ADS1256驱动程序的核心并不在“读寄存器”本身,而在于把三件事串起来:SPI时序要对、DRDY握手要同步、多通道切换要等建立时间。这个组合常见于称重仪表、工业变送器、振动监测、电池测试等对分辨率有硬需求的场合。这篇笔记按原理、接线、初始化、读数、踩坑、验证的顺序展开,新手可以直接抄初始化代码,熟手可以重点看多通道建立时间和校准边界。
2. ADS1256的采样引擎与STM32接线:三套SPI方案怎么选
2.1 Δ-Σ架构带来的速率-建立时间权衡
ADS1256是24位Δ-Σ ADC,不是SAR型。Δ-Σ架构内部有数字滤波器,输出的是过去N个过采样点的加权结果。这个特性决定了它读数的“建立时间”比SAR长得多,选数据速率不能只看SPS数字好不好看。
DRATE寄存器直接决定输出速率,常用几挡是:0x03对应150SPS,0x04对应300SPS,0x05对应1000SPS,0x06对应2000SPS,0x07对应4000SPS,再往上到0x0B对应30000SPS。速率越高,单个样本的噪声越差,建立时间越短;速率越低,噪声越低,但通道切换后要等更久才能采到稳定值。
工程上做多通道轮询扫描时,一般按“3倍数据周期”预留建立时间。比如150SPS下,一个周期约6.67ms,三个周期约20ms。如果只等一个周期就把数据拿去用,读到的经常是上一次通道的残留,这在高速扫描场景里是主要翻车点。记住这个权衡:想快就降分辨率预期,想准就得把速率降下来,二者不可兼得。
2.2 三种接线方式:硬件SPI、模拟SPI与DMA
ADS1256对外只有SPI接口,没有I2C选项,所以STM32这边接线非常标准。常用接线分配如下,以SPI1为例:
| STM32引脚 | ADS1256引脚 | 说明 |
|---|---|---|
| PA5 | SCLK | SPI时钟,模式1,空闲低电平 |
| PA6 | DOUT | MISO,芯片输出数据 |
| PA7 | DIN | MOSI,芯片接收命令 |
| 任意GPIO | CS | 片选,低有效,每个命令周期拉低再拉高 |
| 任意GPIO | DRDY | 数据就绪,下降沿表示新样本可用 |
| 任意GPIO | RESET | 复位,低有效,可接高电平省掉 |
接线方案有三类。第一类是直接用STM32硬件SPI,代码最省,主频允许时SCK可以跑到2MHz以上,适合连续读数据。第二类是软件模拟SPI,引脚任意、调试直观,缺点是SCK频率受GPIO翻转速度限制,但ADS1256的SPI速率本来也不高,低速场景下模拟SPI反而更灵活。第三类是硬件SPI加DMA,适合30kSPS满速率连续采样,CPU只在DRDY中断里搬运结果,不参与字节级收发。
我一般先跑硬件SPI,读不通就切模拟SPI对照,排除是芯片问题还是时序问题。注意CS不要一直拉低。每个命令周期结束后拉高CS,否则DRDY和命令状态的配合容易出问题。
2.3 SPI模式1的时序边界:DRDY为什么不能当普通GPIO用
ADS1256要求SPI模式1:CPOL为0,CPHA为1,MSB先行。模式0和模式1在寄存器配置上只差一个CPHA位,但结果完全不同。模式0下芯片会在SCK的第一个边沿采样,而ADS1256在第二个边沿采样,命令字和读回数据全部错位。
用硬件SPI时,在STM32的SPI初始化里把CPOL设为Low、CPHA设为2Edge,数据宽度8位,MSB先行。SCK频率建议先压到1MHz以内,跑通后再往上提。很多人一上来就拉满SPI分频,读回0xFF就开始怀疑芯片,其实先降速再排查才是正道。
DRDY引脚不是普通状态脚,它是芯片内部数据就绪的中断信号,下降沿代表输出寄存器已经更新。不要用轮询SCK的方式去猜数据是否就绪,直接在DRDY上做下降沿检测或外部中断。需要注意,DRDY每次拉低后,只有在SPI主机发出RDATA命令并读完三字节,或者拉高CS结束当前命令后,它才会重新变高。读数据这件事必须和DRDY生命周期对齐,否则会出现“读到上一次旧数据”的情况。
3. 寄存器配置与SPI通信层:从命令字到自校准
3.1 寄存器地图与命令字:WREG/RREG的收发规则
ADS1256有一组配置寄存器,常用的是0x00状态寄存器STATUS、0x01通道控制寄存器MUX、0x02模拟控制寄存器ADCON、0x03数据速率寄存器DRATE。MUX寄存器高四位选正输入,低四位选负输入;ADCON低三位选PGA增益;DRATE存速率编码。
写寄存器用WREG命令,命令字是0x50加上寄存器起始地址。比如从MUX开始写三个连续的寄存器,命令字是0x53,第二个字节写0x02,表示还要再写两个寄存器,第三个字节开始依次放MUX、ADCON、DRATE的值。读寄存器用RREG命令,命令字是0x10加上寄存器地址,第二个字节同样写寄存器个数减一,第三个字节开始读数据。
底层收发函数我一般封装成下面这样:
void ads1256_write_reg(uint8_t reg, uint8_t val) { CS_LOW(); spi_write_byte(0x50 | reg); /* WREG命令 */ spi_write_byte(0x00); /* 只写1个寄存器 */ spi_write_byte(val); /* 数据字节 */ CS_HIGH(); } uint8_t ads1256_read_reg(uint8_t reg) { uint8_t val; CS_LOW(); spi_write_byte(0x10 | reg); /* RREG命令 */ spi_write_byte(0x00); /* 只读1个寄存器 */ val = spi_write_read_byte(0x00); /* 发送哑字节,同时收数据 */ CS_HIGH(); return val; }第二个字节的坑在于它是“寄存器数量减一”,写一个寄存器填0,连续写三个填2,很多人第一次都会在这里多写或少写一个字节。读寄存器时第3个字节必须由主机先发一个哑字节去产生SCK时钟,否则MISO上没有脉冲,读回的全是0xFF。CS的拉低和拉高不能省,命令结束后必须复位片选状态。
3.2 初始化函数:上电复位到自校准的完整步骤
初始化顺序比代码本身更值得较真。上电后先等芯片内部复位完成,然后先发一条SDATAC退出连续读模式,再写配置寄存器,最后做自校准。完整代码如下:
void ads1256_init(void) { CS_HIGH(); HAL_Delay(200); /* 等上电稳定 */ CS_LOW(); spi_write_byte(0x0F); /* SDATAC,退出连续读模式 */ CS_HIGH(); /* 配置:MUX=AIN0+AINGND,PGA=1,DRATE=150SPS */ uint8_t cfg[3] = { 0x18, 0x00, 0x03 }; CS_LOW(); spi_write_byte(0x53); /* WREG,从MUX寄存器开始 */ spi_write_byte(0x02); /* 连续写3个寄存器,个数减一 */ spi_write_bytes(cfg, 3); CS_HIGH(); /* 自校准 */ CS_LOW(); spi_write_byte(0xF0); /* SELFCAL */ CS_HIGH(); uint16_t timeout = 10000; while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) == GPIO_PIN_SET) { HAL_Delay(1); if (--timeout == 0) break; } }这里MUX值为0x18,二进制是0b00011000,高四位是1表示正输入选AIN0,低四位是8表示负输入选AINCOM,也就是把单端信号接在AIN0和对地之间。ADCON为0x00表示PGA为1倍,不放大。DRATE为0x03表示150SPS,适合先拿一个低速稳定点验证链路。
注意SDATAC这条命令不是必须的,上电默认并不在连续读模式,但初始化开头发一次能防止芯片状态不确定时卡死在RDATAC。自校准命令必须等DRDY拉低才算完成,否则校准中途断电会留下错误的偏移校准值。
3.3 校准命令不是万能药:SELFCAL与SELFOCAL的适用场景
SELFCAL同时校准偏移和增益误差,SELFOCAL只校准偏移,SELFGCAL只校准增益。上电自检或更换PGA增益后,用SELFCAL最简单。但如果输入信号源是高阻或者悬空,自校准会把悬浮噪声当成偏移存进寄存器,结果比校准前更差。我碰到过这个现象:传感器没接好,校准完零点反而偏了十几mV。原因是校准期间芯片内部短接了输入端,但外部悬空噪声耦合进来,等效于给校准电路注入了一个随机偏移。
正确的做法是先把传感器或前端电路接好,让输入处于真实工作状态,再执行SELFCAL。如果系统不允许在校准时断开业务信号,那就只做SELFOCAL,或者干脆不校准,用软件在应用层减去零点偏移。
另外,校准寄存器会被新的SELFCAL覆盖,也会被写配置寄存器之后的重新校准覆盖。所以初始化顺序应该是:先写MUX、ADCON、DRATE,最后做校准。如果先校准后改PGA,PGA切换引入的偏移和增益变化会让校准值完全失效,等于白做。
4. 数据读取和多通道扫描:DRDY同步与电压换算
4.1 单通道连续读取:查询DRDY还是中断
单通道读取最简单可靠的方式是等待DRDY变低后发RDATA命令,连续读三个字节。注意这里不能用HAL库的SPI_Receive,因为ADS1256不是纯从机,它要主机先发哑字节才肯吐数据,每一字节都是“先发送后接收”的完整交换。
int32_t ads1256_read_single(void) { uint8_t b[3]; uint16_t timeout = 10000; /* 等待DRDY拉低,超时保护 */ while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) == GPIO_PIN_SET) { if (--timeout == 0) return 0x80000000; /* 超时标志 */ } CS_LOW(); spi_write_byte(0x01); /* RDATA命令 */ b[0] = spi_write_read_byte(0x00); b[1] = spi_write_read_byte(0x00); b[2] = spi_write_read_byte(0x00); CS_HIGH(); int32_t raw = ((int32_t)b[0] << 16) | ((int32_t)b[1] << 8) | b[2]; if (raw & 0x800000) { raw |= 0xFF000000; /* 24位有符号扩展 */ } return raw; }超时保护不是可选项。DRDY如果因为芯片异常一直不拉低,代码会卡死在等待循环里,整个采集任务停摆。超时返回值选一个不可能出现的数,调用方要检查它。
关于连续读模式RDATAC,它能省掉RDATA命令字,每次DRDY下降沿直接读三字节,看起来效率更高。代价是每一次读取必须严格三字节,一旦中间任何一次多读或少读一位,后续数据全部错位,而且很难察觉。我一般不用RDATAC,单次读模式多传一个命令字节,换来的是状态清晰、好排查。
4.2 多通道轮询扫描:通道切换后先丢两个点
多通道扫描的核心不是SPI读得快不快,而是通道切换后数字滤波器需要时间来稳定。MUX寄存器切换后立刻读,读到的第一个样本其实是旧通道数据和过渡数据的混合。工程经验是直接丢弃前两个样本,从第三个样本开始使用。
有的资料只说“丢掉第一个样本”,这在高速率下不够。ADS1256的滤波器阶数不是一阶,一个数据周期后滤波器输出仍在过渡中,丢两个点更稳。如果PGA同时切换,我还会多丢一个点。代码如下:
void ads1256_set_channel(uint8_t ch) { uint8_t mux = (uint8_t)((ch << 4) | 0x08); /* 单端模式,负端接AINCOM */ CS_LOW(); spi_write_byte(0x50 | 0x01); /* WREG,从MUX开始 */ spi_write_byte(0x00); spi_write_byte(mux); CS_HIGH(); /* 丢弃两个过渡样本 */ ads1256_read_single(); ads1256_read_single(); }这里每次read_single都会等待DRDY,所以两个样本实际等待时间约为两个数据周期。在150SPS下大约是13.3ms,如果做8通道轮询,一轮下来就超过100ms,这是多通道扫描的实际速度瓶颈。
想要提高多通道吞吐,可以把同一通道连续读多个点再切换,减少切换次数。或者放弃单端轮询,改用ADS1256的输入多路复用做差分扫描时,把公共负端固定到同一个AIN通道,只切换正端,这样MUX操作更轻量。
4.3 裸数据换算成电压:LSB公式与PGA、VREF的关系
ADS1256输出的是二进制补码格式的有符号24位整数,满量程对应正负参考电压除以PGA。换算电压的公式是:
LSB = 2×VREF / (PGA×2^23)
其中2^23等于8388608,这是24位ADC正半幅的数字量程。VREF是参考电压引脚两端的实际电压,不是标称值。PGA是放大倍数,不是寄存器编号。得到LSB后用原始值直接乘以LSB就是电压值,单位随VREF单位走。
float ads1256_raw_to_voltage(int32_t raw, float vref, uint8_t pga) { float lsb = 2.0f * vref / ((float)pga * 8388608.0f); return (float)raw * lsb; }举个例子,VREF等于2.5V,PGA等于1时,LSB约0.596μV,一个大数0x7FFFFF对应约2.5V。PGA设为64时,LSB约9.3nV,看起来分辨率极高,但噪声也被放大64倍,实际有效位往往不如PGA为1时。这一点经常让人误以为PGA越大越好,实际是“分辨率提升、精度下降”。
浮点换算方便但不适合长时间连续采集,跑在无FPU的低主频芯片上会占CPU。批量数据处理建议转成整数计算:电压毫伏数等于raw乘以某个定点系数再右移16位,速度和一致性都好很多。
5. ADS1256驱动避坑指南:读回0xFF到通道串扰的排查记录
5.1 SPI读到固定0xFF或0x00:先查时序再怀疑芯片
现象是读取任何寄存器或数据都返回固定的0xFF,偶尔是全0。换一片芯片结果一样,排除芯片损坏。
原因通常是SPI模式不对,MISO和MOSI接反,或者SCK频率过高导致边沿采样错位。模式0和模式1的CPHA就差一个比特,但ADS1256只认模式1,配置错了所有命令字都白搭。MISO和MOSI接反时,主机发出的命令到达不了芯片,读回的自然全是高电平。
解决方法是先降SCK到1MHz以内,确认CPOL为Low、CPHA为2Edge,然后用逻辑分析仪抓CS、SCK、MOSI、MISO四根线。在CS低电平窗口里,SCK应该有恰好八个脉冲,MISO上能对应读出寄存器值。用杜邦线交叉MISO和MOSI做对照,是确认接反最快的土办法。
5.2 数据跳变严重,有效位只有16位
现象是读数不是满量程乱跳,而是在某个值附近来回抖,抖动的幅度换算成电压有几十μV甚至几百μV,和24位精度预期严重不符。
原因分三类:参考电压VREF纹波大、模拟电源AVDD和数字电源DVDD没有分开、输入信号本身是浮空或高阻。还有一个隐蔽原因是PGA倍数开太高,PGA为64时理论LSB极小,但输入噪声也被乘以64,抖动幅度自然按比例放大。
解决方法是先把模拟前端断开,把AIN0和AIN1短接,让差分输入为零,再读100个点看峰峰值。这个基准值能区分是芯片链路问题还是外部信号问题。如果短接后依然抖动到几百μV,就去查AVDD的LDO输出纹波、DVDD的开关噪声、VREF引脚上的去耦电容和PCB地平面分割。模拟地和数字地在ADC底下单点汇合是硬要求,靠一根杜邦线飞地调试高精度采集,大概率要返工。
5.3 多通道扫描第一个数据不对:建立时间踩坑
现象是只做单通道时数据完全正常,一做多通道轮询,某个通道的第一个点明显偏大或偏小,后面的点又恢复正常。
原因不是SPI收发错位,而是MUX切换后,芯片内部的数字滤波器还没有完成过渡。滤波器的历史缓存里还是上一个通道的数据,新的通道值要经过若干个数据周期才会完全替代旧值。
解决办法就是切换通道后主动丢掉前两个样本。这不算浪费,因为ADS1256在速率较高时,建立期间读到的数据本身就不具备业务价值。还有一个习惯能减少踩坑:把通道切换操作放在上一次读取完成后立刻做,给滤波器留出最长的时间窗口进行过渡,而不是在下一个DRDY到来前才切换。
5.4 DRDY长时间不拉低:命令状态机卡死
现象是初始化代码卡在等待DRDY的循环里,用示波器量DRDY引脚一直为高电平,发什么命令都没有反应。
原因通常是芯片停留在连续读模式RDATAC,或者CS片选时序异常导致命令字没有完整送达。RDATAC模式下芯片的DRDY行为与单次模式不同,如果初始化流程没有确保退出该状态,DRDY可能不会按预期释放。
解决办法是先强制发一条SDATAC命令,接着读一次STATUS寄存器,确认SPI链路是通的。读STATUS返回的值里如果还有明显的位变化,说明芯片接收命令正常,问题在状态机。如果STATUS读出来还是死值,再回头检查CS下降沿、SCK极性和供电电压。不要反复复位Power-On Reset来碰运气,先排除通信层问题,再考虑芯片是否因为某个非法命令进入了未定义状态。
6. 进阶验证:短接噪声测试与滤波参数整定
6.1 用短接噪声测试给整块板卡定基准
每块新板子回来,我做的第一件事不是接传感器,而是做一次短接噪声测试。把AIN0和AIN1短接,配置PGA为1、速率150SPS,连续采集100个点,记录最大最小值之差。这个峰峰值就是板卡的底噪基线。
记录时给每个常见速率和增益组合留一行表格,比如150SPS、1000SPS、3000SPS,PGA为1、8、64。对比后能直接看出哪个组合的噪声不可接受,也为后面的软件滤波提供依据。别在未做基线测试的情况下直接联调,否则传感器信号和系统噪声混在一起,排查效率极低。
6.2 数字滤波与DRATE配合:避免第二轮噪声放大
在ADC输出之后加一阶低通是常见做法,但滤波系数不能乱填。我用一个简单的滑动平均加一阶惯性滤波,系数alpha取0.1到0.3,配合150SPS左右的数据速率,既能压住高频抖动,又不会把真实变化抹平。
static float filtered = 0.0f; float ads1256_apply_filter(float raw_mv, float alpha) { filtered += alpha * (raw_mv - filtered); return filtered; }alpha越大响应越快、滤波越弱;alpha越小越平滑、滞后越明显。验证方法是用信号发生器给AIN0输入一个100Hz方波,看输出上升沿是否被拉得太长。如果方波变成斜坡,说明alpha选小了。如果噪声还压不住,优先降DRATE而不是继续加滤波阶数,因为源端噪声和数字滤波是两回事,后者压掉的只是采样结果里的高频分量。
那次0.3%偏差的联调经历让我长了个记性:读数稳定、线性正常、但整体偏大,查到最后是前端分压电阻精度不够,和ADC没有任何关系。所以现在每块板子都先跑短接基线,再联调传感器,省掉很多回头找问题的麻烦。希望这套流程对你也有用。
本文还有配套的精品资源,点击获取