☰
ADS1118在STM32上的完整驱动:SPI配置、多通道采集与温度测量
2026/10/21 19:07:16 网站建设 项目流程

简介:基于STM32F103和STM32F407两款单片机,为德州仪器16位ADC芯片ADS1118提供一套完整的驱动程序,覆盖四路单端输入、两路差分输入以及内部温度测量三大部分,可直接应用于工业自动化、医疗设备和精密测量等需要高精度数据采集的嵌入式项目。压缩包采用RAR格式,大小约11.82MB,内含模块化驱动源码、多组实验测试数据以及ADS1118官方手册和相关资料。驱动源码中,初始化配置、数据读取、参数设置和通信异常处理等环节一应俱全,每种功能模块均有清晰划分;测试数据也分别记录了单端、差分和温度通道在不同采样条件下的转换结果,便于实际调试时对照分析精度和稳定性,减少问题定位时间。随附的芯片手册与技术文档详细说明了寄存器配置、采样时序和电气特性,能帮助开发者快速理解16位ADC的底层工作机制。目前已有321人学习该资源,无论是刚接触ADS1118的初学者,还是正在做项目移植的工程师,都可以借助这套代码与实测数据快速上手,降低开发门槛。

1. 项目定位:用STM32驱动ADS1118,这次把四路单端、双差分和温度一网打尽

年初在产线上调试一台采集设备,MCU用的是STM32F103,外挂一颗ADS1118做16位ADC采样。按理说SPI对接个ADC是很常规的活,结果在调四路单端和内部温度传感器时翻了车——读数乱跳、通道切换后第一帧数据总是上一通道的残留,折腾了一整天才发现是配置时序和DRDY引脚处理的问题。所以当我看到这套同时覆盖STM32F103和STM32F407的ADS1118完整驱动时,第一反应是:终于不用再对着数据手册逐位翻译寄存器了。

这套驱动解决的问题非常明确:在STM32F103和STM32F407两个平台上,把ADS1118的完整功能跑通——四路单端输入、两路双差分输入,以及芯片内置温度传感器读取,全部封装成可直接调用的接口。适合三类人:一类是正在做工业采集、仪器仪表项目的嵌入式工程师,一类是课程设计需要快速跑通ADC采样的学生,还有一类是想移植ADS1118驱动到其他MCU平台的开发者。驱动代码不是零散例程,而是带完整SPI时序处理、寄存器配置和通道切换逻辑的工程级代码。

下面从驱动设计思路、核心代码实现、实际踩坑和二次开发几个维度拆开讲,尽量每段都能对着代码看懂。

2. 驱动设计:SPI协议、寄存器配置和STM32侧代码结构

2.1 ADS1118的SPI时序特点:别按常规16位SPI去写

ADS1118和普通SPI ADC最大的不同在于它的SPI帧是16位起步,但配置寄存器和转换数据共用同一条总线。也就是说,主机要先发16位配置命令,再读16位转换结果,而且读写操作在一条指令周期内交替完成。很多移植失败的案例都是把ADS1118当成普通SPI从机,发完配置就等着收数据,结果读到的全是零。

ADS1118的SPI时序实际上分三段:第一段是16位配置字写入,第二段是8位命令加上8位数据输出,第三段是最后的8位CRC校验(可选)。但在STM32的常规SPI模式下,我们通常用16位数据帧格式,一次交换完成配置写入和结果读取。

以单次转换模式为例,STM32发送16位配置字后,需要再触发一次SPI传输才能读到转换结果。这里有个关键细节:ADS1118的DOUT/DRDY引脚在转换完成时拉低,主机可以在第二段SPI传输前检查这个引脚状态,确保读到的是新数据而不是旧值。驱动代码里对DRDY的处理方式直接决定了采样数据的实时性。

2.2 STM32侧的SPI初始化:F103和F407的差异点

STM32F103和F407的SPI外设寄存器大部分兼容,但有几个细节需要注意:F407的SPI时钟最高可达42MHz,而F103最高只有18MHz;另外F407的SPI1默认挂载在APB2总线上,时钟源是84MHz(经过分频后)。如果直接照搬F103的SPI配置到F407上,很可能因为时钟分频设置不当导致SPI通信速率为0或波形异常。

实际驱动中,SPI初始化主要关注四件事:引脚复用功能、SPI模式配置、时钟分频计算以及DMA的可用性。

/* STM32F407 SPI1 初始化,PA5=SCK, PA6=MISO, PA7=MOSI */ void ADS1118_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; /* 使能GPIOA和SPI1时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); /* 配置PA5、PA6、PA7为复用功能 */ GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStructure); /* SPI1 主机模式,16位数据帧,模式1(CPOL=0, CPHA=1) */ SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

这段代码里最关键的是CPOL和CPHA的取值。ADS1118数据手册明确要求SPI模式1(CPOL=0,CPHA=1),也就是时钟空闲为低电平,数据在第二个边沿采样。很多初学者习惯性地用模式0(CPOL=0,CPHA=0),那样读回来的数据每一位都会错位,症状是寄存器值完全对不上、温度读数乱跳。

2.3 寄存器配置封装:用结构体管理配置位而不是裸写十六进制

ADS1118的16位配置寄存器每个位都有明确含义,从高到低依次是:SS(启动转换)、MUX[2:0](输入通道选择)、PGA[2:1](增益)、MODE(单次或连续)、DR[2:0](数据速率)、TS_MODE(温度测量)、DO_COMP(比较器极性)、LAT(锁存)、DRDY(数据就绪极性)。把这16位用手算成十六进制数再填进代码,后期维护就是灾难。

驱动里用位域结构体来管理配置字,让代码可读性提升一个档次:

/* ADS1118 配置寄存器位映射结构体 */ typedef struct { uint16_t SS:1; // 启动转换,1=开始转换 uint16_t MUX:3; // 通道选择:0=AIN0-AIN1, 1=AIN2-AIN3等 uint16_t PGA:3; // 增益:0=±6.144V, 1=±4.096V, 2=±2.048V... uint16_t MODE:1; // 0=连续转换,1=单次转换 uint16_t DR:3; // 数据速率:0=8SPS, 1=16SPS ... 7=860SPS uint16_t TS_MODE:1; // 1=温度传感器模式,0=ADC模式 uint16_t DO_COMP:1; // 比较器模式 uint16_t LAT:1; // 比较器锁存 uint16_t DRDY:1; // 数据就绪引脚极性 } ADS1118_ConfigBits; /* 联合体:方便同时按位和按字节访问 */ typedef union { uint16_t value; ADS1118_ConfigBits bits; } ADS1118_Config_t;

在C语言里用位域加联合体的方式操作寄存器,编译后生成的机器码和直接操作uint16_t本质一样,但源码可读性完全不同。比如要配置AIN0和AIN1的差分输入、PGA为±4.096V、8SPS采样率,只需要赋值结构体成员而不是计算那一大串十六进制。

/* 配置AIN0-AIN1差分输入,±4.096V,8SPS,单次模式 */ ADS1118_Config_t config; config.value = 0; // 先清零 config.bits.SS = 1; // 启动转换 config.bits.MUX = 0; // AIN0-AIN1差分 config.bits.PGA = 1; // ±4.096V config.bits.MODE = 1; // 单次转换 config.bits.DR = 0; // 8 SPS config.bits.TS_MODE = 0; // ADC模式 config.bits.DO_COMP = 0; config.bits.LAT = 0; config.bits.DRDY = 1; // DRDY低电平有效

这样配置寄存器,谁来看代码都能立刻明白当前处于什么采样模式,不需要对照数据手册算上半天。后面要扩展新的通道组合或修改增益,也只需要改动一行结构体赋值。

2.4 目录结构:把平台相关代码和驱动逻辑剥离开

整套驱动的目录设计遵循一个原则:芯片驱动逻辑不依赖具体MCU型号,平台差异全部通过宏定义和底层接口隔离。这个设计直接为后期移植到其他MCU省了大量功夫。

ADS1118_Driver/ ├── Inc/ │ ├── ads1118.h // 芯片寄存器定义、接口声明 │ └── ads1118_cfg.h // 引脚配置、SPI句柄映射 ├── Src/ │ ├── ads1118.c // 核心驱动:通道切换、读取、温度计算 │ ├── ads1118_stm32f1.c // F103平台SPI底层封装 │ └── ads1118_stm32f4.c // F407平台SPI底层封装 ├── Examples/ │ ├── f103_4ch_single.c // F103四路单端示例 │ ├── f407_2ch_diff.c // F407双差分示例 │ └── temp_measure.c // 温度测量示例 └── README.md

ads1118.c里不出现任何GPIO操作,所有SPI收发都调用ads1118_cfg.h中声明的函数指针或宏,这样换平台时只需要替换底层封装文件。F103和F407的底层SPI代码差异集中在时钟使能和引脚复用配置上,这部分单独放文件是合理的。

3. 核心代码拆解:四路单端、双差分和温度采集的完整链路

3.1 四路单端采集:通道切换的时序陷阱

四路单端采集是ADS1118最常见的应用场景。MUX配置位设置为100~111时,分别对应AIN0、AIN1、AIN2、AIN3相对GND的测量。需要注意的是,单端模式下输入电压不能超过电源电压,否则芯片的输入保护二极管会导通,轻则测量偏差,重则损坏芯片。

通道切换后读到的第一帧数据往往是上一通道的残留,这是ADS1118内部采样电容未完成充放电导致的。解决方式有两种:一是在切换通道后丢弃第一次转换结果,二是延长两次转换之间的间隔。驱动里采用丢弃首帧的方式,这是实践中性价比最高的方案。

/* 读取指定单端通道的ADC值,chn=0~3分别对应AIN0~AIN3 */ uint16_t ADS1118_ReadSingleChannel(uint8_t chn) { ADS1118_Config_t config; uint16_t raw_value; uint8_t i; if (chn > 3) return 0; /* 构造单端模式配置字 */ config.value = 0; config.bits.SS = 1; config.bits.MUX = 4 + chn; // 100=AIN0, 101=AIN1, 110=AIN2, 111=AIN3 config.bits.PGA = 1; // ±4.096V config.bits.MODE = 1; // 单次转换 config.bits.DR = 3; // 64 SPS config.bits.TS_MODE = 0; // ADC模式 /* 第一次写入配置,启动转换 */ ADS1118_SPI_Send16(config.value); /* 等待DRDY引脚拉低,表示转换完成 */ while (ADS1118_DRDY_READ() == 1); /* 丢弃第一帧残留数据 */ ADS1118_SPI_Send16(config.value); ADS1118_SPI_Receive16(&raw_value); /* 再次触发转换并读取有效数据 */ ADS1118_SPI_Send16(config.value); while (ADS1118_DRDY_READ() == 1); ADS1118_SPI_Receive16(&raw_value); return raw_value; }

这段代码里有几个细节值得说明。MUX字段直接用了4+chn,目的是和ADS1118的数据手册对应上,手册中AIN0单端的MUX编码就是100。DRDY引脚的等待是阻塞式的,在实际项目中如果你需要ADC采样和主流程并行,这里改成中断或DMA方式会更合适。第三点是丢弃首帧的时机,放在设备初始化阶段也可以,但每次通道切换后都执行会更保险,因为通道切换本身也会导致采样电容重新充电。

3.2 双差分采集:MUX编码和伪差分模式的区别

两路双差分在ADS1118上对应的MUX编码是000和011,分别是AIN0-AIN1和AIN2-AIN3。这里有一个容易踩的坑:ADS1118还支持AIN0-AIN3的伪差分模式(MUX=101),它和真正的双差分完全是两回事。伪差分模式下负输入端是固定的AIN3,不适合用于需要严格共模抑制的场景。

双差分模式适合桥式传感器、热电偶等输出微弱差分信号的场景。因为差分输入能抵消共模噪声,而PGA增益可以放到很大,所以实际使用中经常搭配高增益配置。

/* 读取双差分通道,diff_ch=0表示AIN0-AIN1,diff_ch=1表示AIN2-AIN3 */ int16_t ADS1118_ReadDifferential(uint8_t diff_ch) { ADS1118_Config_t config; int16_t result; uint8_t i; config.value = 0; config.bits.SS = 1; config.bits.MUX = (diff_ch == 0) ? 0 : 3; // 000或011 config.bits.PGA = 3; // ±2.048V,适合小信号 config.bits.MODE = 1; config.bits.DR = 3; // 64 SPS config.bits.TS_MODE = 0; ADS1118_SPI_Send16(config.value); while (ADS1118_DRDY_READ() == 1); /* 丢弃首帧 */ ADS1118_SPI_Send16(config.value); ADS1118_SPI_Receive16(&result); /* 读取有效数据 */ ADS1118_SPI_Send16(config.value); while (ADS1118_DRDY_READ() == 1); ADS1118_SPI_Receive16(&result); return result; }

返回值用了int16_t是有原因的。ADS1118的转换结果采用二进制补码格式表示,差分模式下负输入大于正输入时,转换结果就是负数。如果用uint16_t接收,负值会被解释成65535附近的数,后面做电压计算时全部错误。

补码格式在差分模式下还有个隐含坑:当输入电压超过量程范围时,ADC会输出满量程值且不产生溢出标志。比如配置为±2.048V量程,输入差分电压为2.5V时,读数会停在32767而不是继续增加。这种饱和行为需要通过软件判断来识别超量程,避免把饱和值当成真实电压算进控制逻辑。

3.3 温度传感器读取:把原始码转换成摄氏度的换算公式

ADS1118内置温度传感器在芯片出厂时做过校准,精度在±1℃以内,适合用来做PCB板温监测或者冷端补偿参考。温度测量模式通过配置寄存器的TS_MODE位启用,此时MUX、PGA和DR位都不生效,芯片内部自动切换测量通路。

温度传感器的数据格式是14位分辨率,左对齐到16位数据帧中,最低两位始终为0。换算公式在数据手册里有明确说明,但直接用int16_t做算术右移会得到错误结果,因为温度值也可能是负数。

/* 读取芯片温度,返回单位0.1℃,例如返回235表示23.5℃ */ int16_t ADS1118_ReadTemperature(void) { ADS1118_Config_t config; uint16_t raw; int16_t temp_raw; int16_t temp_celsius; config.value = 0; config.bits.SS = 1; config.bits.TS_MODE = 1; // 温度传感器模式 ADS1118_SPI_Send16(config.value); while (ADS1118_DRDY_READ() == 1); /* 连续模式读取时需要保持TS_MODE位不变 */ ADS1118_SPI_Send16(config.value); ADS1118_SPI_Receive16(&raw); /* 丢弃低两位无效位,符号位在bit15 */ temp_raw = (int16_t)(raw >> 2); /* 每LSB对应0.03125℃ */ temp_celsius = (int16_t)(temp_raw * 0.03125 * 10); // 放大10倍避免浮点数 return temp_celsius; }

这个换算逻辑里面有一个精细操作:raw >> 2先做逻辑右移,再强制类型转换成int16_t。如果直接写(int16_t)raw >> 2,C语言的运算符优先级会先把raw转成int16_t再移位,对于负温度值来说,算术右移会保留符号位扩展,得到的结果反而正确。但为了避开不同编译器对带符号右移的差异,先逻辑右移再转类型是更可移植的写法。

如果环境温度低于0℃,temp_raw是负数,乘以0.03125后结果正确,但直接int16_t赋值会截断小数部分。代码里乘了10再存,用整型方式保留一位小数精度,这个手法在资源受限的MCU上比用浮点数更高效。

3.4 数据速率和PGA增益的搭配策略

ADS1118的数据速率从8SPS到860SPS可调,但数据速率和数字滤波器的性能是挂钩的。数据手册里明确指出,速率越低,内部数字滤波器对工频干扰的抑制能力越强。在工业现场做传感器采集时,50Hz工频干扰是躲不开的,建议优先选择16SPS或32SPS,这样滤波器的陷波点能落在50Hz附近。

PGA增益的选取直接决定了测量量程和分辨率。增益越大,量程越小,但LSB对应的电压值越精细。实际项目中如果传感器输出电压范围是0~20mV,选择PGA=7(±0.256V量程)能获得最高的分辨率,但对外部噪声也更敏感。PCB走线和参考电压的稳定性在此时就变得非常关键,否则读数波动幅度大得让你怀疑ADC坏了。

驱动中已经把PGA和量程的对应关系封装成宏定义:

/* PGA增益与满量程对照表 */ #define ADS1118_PGA_6_144V 0 // ±6.144V, LSB=0.1875mV #define ADS1118_PGA_4_096V 1 // ±4.096V, LSB=0.125mV #define ADS1118_PGA_2_048V 2 // ±2.048V, LSB=0.0625mV #define ADS1118_PGA_1_024V 3 // ±1.024V, LSB=0.03125mV #define ADS1118_PGA_0_512V 4 // ±0.512V, LSB=0.015625mV #define ADS1118_PGA_0_256V 5 // ±0.256V, LSB=0.0078125mV #define ADS1118_PGA_0_256V_B 6 // ±0.256V, 增益同5 #define ADS1118_PGA_0_256V_C 7 // ±0.256V, 增益同5

使用这些宏定义后,切换量程不需要查手册,代码可读性也强很多。在二次开发中你可能会发现,F103和F407的ADC精度其实一般,所以把模拟前端的分辨率尽量发挥出来是值得花时间优化的方向。

4. 避坑指南:ADS1118在STM32上最常见的五个问题

4.1 读回来的数据全是0xFF或0x00

现象:SPI通信正常,但配置寄存器写进去后再读出来全是0xFF,或者数据全是0。

原因:最常见的情况是SPI模式配置错误。STM32的SPI外设支持4种模式,ADS1118要求模式1(CPOL=0, CPHA=1)。如果配置成模式0,MISO上采样的数据位正好错开半个时钟周期,读回来的每一位都是乱的。另一个可能原因是CS引脚没有正确拉低,ADS1118的CS是低电平有效,如果CS悬空或初始化为高电平,芯片根本没有被选中。

解决:第一步用示波器或者逻辑分析仪抓SCK、MOSI、MISO三根线的波形,确认时钟空闲电平是低、数据在第二个边沿变化;第二步检查CS引脚初始化顺序,必须在SPI通信前先拉低CS。还见过一种情况是芯片供电电压低于2.0V,ADS1118在欠压状态下SPI接口不响应,检查一下AVDD引脚电压是否稳定在数据手册要求的范围内。

4.2 通道切换后的第一帧数据异常

现象:从CH0切换到CH1,读到的第一个数据明显偏大或偏小,之后恢复正常。

原因:ADS1118的输入前端是开关电容结构,切换通道后采样电容里的残留电荷需要几个转换周期才能泄放干净。尤其是从前一个高阻抗信号源切换到低阻抗信号源,恢复时间更长。

解决:驱动代码里已经有了丢弃首帧的处理逻辑,但如果你在应用层做了更频繁的通道切换,比如每次主循环都切换,那就要检查丢弃帧数是否足够。一般丢一帧够用,但如果信号源阻抗超过10kΩ,建议丢掉前两帧数据再取值。

4.3 温度读数偏高或偏低且不随环境变化

现象:温度读数稳定在某个值,比如总是45℃左右,用手摸芯片也不变。

原因:大概率是TS_MODE位没有正确保持。ADS1118当TS_MODE=1时,配置字的其他位含义会发生变化,如果你在温度模式转换完成后又把配置字改回了ADC模式,芯片内部温度测量电路被关闭,读到的数据是上一次转换的残留值。

解决:读取温度的整个过程中,保持TS_MODE位始终为1。驱动代码中也有体现,每次发送配置字时都重新构造config结构体,确保所有位处于正确状态。不要试图用“上次的配置字只改一位”的方式来精简代码,容易漏掉其他关联位。

4.4 差分模式下负电压读数不对

现象:输入差分电压为负值时,读数不是负数而是很大的正数(比如65400附近)。

原因:数据类型和移位运算处理不当。前面已经解释过,ADS1118输出的是二进制补码,负值最高位为1。如果用uint16_t类型接收,再去计算电压,就会把这些负数当成大正数处理。

解决:接收数据时用int16_t类型,或者uint16_t转int16_t。注意有些编译器在优化时对int16_t的移位操作有未定义行为,最好用强制类型转换而不是依赖隐式转换。

4.5 高速采样时数据丢帧

现象:配置为860SPS连续采样模式,但主循环里读取数据时发现在快速连续读取时偶尔读到旧数据。

原因:连续转换模式下,ADS1118内部的转换速率固定;当SPI读取速度跟不上转换速率时,数据寄存器被新数据覆盖,读出来就是半新半旧的数据。DRDY引脚下降沿标志着新数据就绪,但如果主机没有在下一次转换完成前读完,就会丢帧。

解决:要么提高SPI时钟频率让读取更快,要么降低采样速率让读取更从容,要么使用DRDY触发的中断方式读取数据,保证数据就绪后再读。实际项目里最常见的问题是SPI分频系数设得太大导致通信速率过慢,比如F407跑84MHz时钟却用了256分频,SPI只有328kHz,而860SPS每秒需要至少860×16=13.76kHz的搬移速率,理论上够但加上延迟就悬了。

5. 工程实践:把驱动集成到实际项目中的几个关键步骤

5.1 从示例代码到产品代码的改造路径

这套驱动自带的示例工程以功能演示为主,每个示例对应一种工作模式。在实际产品开发里,你可能需要把四路单端、双差分和温度测量整合到一个模块中,并且让ADC采样和主业务逻辑解耦。

常见的做法是封装一个独立的任务层:

/* 采集任务管理器 */ typedef struct { uint8_t channel_type; // 0=单端,1=差分,2=温度 uint8_t channel_index; // 通道编号 uint16_t raw_data; // 原始ADC值 int16_t scaled_data; // 换算后的物理量(放大10倍) uint8_t data_valid; // 数据有效标志 } ADS1118_Channel_t; #define MAX_CHANNELS 7 /* 4路单端 + 2路差分 + 1路温度 */ ADS1118_Channel_t g_channels[MAX_CHANNELS];

在主循环里轮询各通道的读取时机,把转换计算和业务判断分离。这种结构适合大多数采集系统,每个通道的数据自然隔离,后续加滤波算法或者超限报警都很方便。

5.2 采样数据的软件滤波:均值滤波和中值滤波怎么选

ADS1118的硬件已经带了数字滤波器,但在高增益小信号场景下,软件滤波依然是必要的补充。最常用的是滑动平均滤波和中值滤波。

滑动平均适合抑制高斯白噪声,但对脉冲干扰无能为力;中值滤波对脉冲干扰有奇效,但会平滑掉快速变化的信号细节。工业采集设备里经常把两者串起来用:先中值滤波去脉冲,再滑动平均去随机噪声。

/* 滑动均值滤波,窗口大小为8 */ #define FILTER_WINDOW_SIZE 8 uint32_t filter_buffer[FILTER_WINDOW_SIZE]; uint8_t filter_index = 0; uint32_t filter_sum = 0; uint16_t ADS1118_Filter(uint16_t new_value) { filter_sum -= filter_buffer[filter_index]; filter_buffer[filter_index] = new_value; filter_sum += new_value; filter_index = (filter_index + 1) % FILTER_WINDOW_SIZE; return (uint16_t)(filter_sum / FILTER_WINDOW_SIZE); }

使用滑动均值滤波时要注意buffer初始值为0的话,上电后前几个输出值会被拉低,需要做初始化填充或者在启动时跳过前N个滤波输出。更稳妥的方案是用第一次采到的值填充整个buffer。

5.3 校准和误差补偿:热电阻和热电偶场景的零点修正

ADS1118本身有不错的线性度,但系统的零点偏移和增益误差主要是由前级运放、参考电压和PCB布局引入的。一个完整的产品级代码需要在出厂前做两点校准:零点和满量程。

具体做法不复杂:短路输入端进入校准模式,读取此时的ADC值作为零点偏移;接入精密参考电压(比如10mV),读取ADC值计算实际增益系数。校准结果存放在Flash里,运行时动态修正。

/* 两点校准后的电压换算 */ int32_t ADS1118_GetCalibratedVoltage(uint16_t raw_adc, int32_t zero_offset, int32_t gain_error) { int32_t voltage_x10 = (raw_adc - zero_offset) * 10 * 10000 / gain_error; return voltage_x10; // 返回伏特数的100000倍,避免浮点 }

这个公式把校准分解成零点和增益两个参数,在产线上批量校准时,只需要把每个设备测出来的两个参数写入Flash,运行时代码从Flash读取即可。相比每次上电都做全自动校准,这种方案更适合批量生产。

6. 二次开发改造:把这套驱动移植到其他MCU或扩展功能

6.1 移植到其他STM32系列:改动量到底有多大

这套驱动的设计目标就是方便移植,从F103到F407,或者到G0、L4系列,理论上只需要修改ads1118_stm32f1.c和ads1118_stm32f4.c这两个底层文件,核心的ads1118.c不需要改动。

以移植到STM32G0系列为例,底层文件的改动主要集中在时钟树和GPIO初始化上。G0的GPIO复用功能配置不再使用GPIO_PinAFConfig函数,而是直接操作GPIOx->AFR寄存器,或者用更新的HAL库函数。

/* STM32G0系列SPI GPIO配置示例 */ void ADS1118_SPI_Init_G0(void) { /* G0系列GPIO复用模式通过MODER和AFR寄存器配置 */ GPIOA->MODER &= ~(GPIO_MODER_MODE5 | GPIO_MODER_MODE6 | GPIO_MODER_MODE7); GPIOA->MODER |= (GPIO_MODER_MODE5_1 | GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1); // AF模式 GPIOA->AFR[0] &= ~(0xF << (4 * 5)); GPIOA->AFR[0] |= (0x5 << (4 * 5)); // PA5=SPI1_SCK GPIOA->AFR[0] &= ~(0xF << (4 * 6)); GPIOA->AFR[0] |= (0x5 << (4 * 6)); // PA6=SPI1_MISO GPIOA->AFR[0] &= ~(0xF << (4 * 7)); GPIOA->AFR[0] |= (0x5 << (4 * 7)); // PA7=SPI1_MOSI }

移植时最大的坑不是SPI配置本身,而是时钟树。F103的SPI1挂在APB2上,F407也挂在APB2上,但G0系列的SPI时钟源可能是APB1,频率限制和分频计算方式不同。如果没搞清楚时钟树直接搬分频系数,SPI波特率会整整差一倍但很难被发现,因为通信不是完全失败,只是偶尔出错。

6.2 增加轮询和中断双模式支持

原驱动是阻塞式读取,简单可靠但不适合高实时性系统。如果你想在电机控制或高速数据采集项目里用ADS1118,把它改成中断驱动是值得的改造点。

改造思路是:把DRDY引脚连接到STM32的外部中断EXTI输入,在中断服务函数里读取SPI数据并置位标志位,主循环检测到标志位后再去消费数据。

/* EXTI中断方式读取示例 */ volatile uint8_t adc_data_ready = 0; volatile uint16_t adc_value = 0; void EXTI15_10_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line15) != RESET) { uint16_t temp; /* 读取SPI数据 */ temp = ADS1118_SPI_Read(); adc_value = temp; adc_data_ready = 1; EXTI_ClearITPendingBit(EXTI_Line15); } }

中断方式的好处是主循环不需要轮询DRDY引脚,MCU可以专心处理其他任务。但要注意中断服务函数里尽量别做复杂计算,只做数据搬移和标志位设置,电压换算放在主循环里执行。另外在F407上,EXTI15_10_IRQHandler是共享外部中断,你需要在中断函数里识别具体是哪条线触发的。

6.3 扩展多片ADS1118级联:硬件片选和软件配置

有些采集系统需要超过8路模拟输入,此时级联多片ADS1118是成本最低的方案。硬件上每片ADS1118独占一个CS引脚,SCK、MOSI、MISO三根线可以共享并联。软件上只需要在驱动中增加一个“当前片选”参数。

/* 多片级联时的片选控制 */ void ADS1118_SelectChip(uint8_t chip_index) { /* 假设CS引脚接在PB0~PB3上 */ GPIO_WriteBit(GPIOB, GPIO_Pin_0 << chip_index, Bit_SET); // 全部拉高 GPIO_WriteBit(GPIOB, GPIO_Pin_0 << chip_index, Bit_RESET); // 选中目标芯片 }

级联时的MISO线需要注意。ADS1118的MISO是推挽输出还是开漏输出?数据手册标注为推挽,但多片级联时如果某芯片没有被CS选中的话,它的MISO引脚应该处于高阻态而不是强推挽输出。实际验证下来,ADS1118的MISO在CS为高时是高阻态的,可以直接并联,不需要额外加缓冲器。

多片级联还有一个收获:因为时钟线是共享的,你可以让所有芯片同时启动转换(先发一轮配置字),然后再逐一读取数据。这样相当于把采样从串行变成了准并行,系统整体吞吐量接近翻倍。真正的并行需要每片独立的SCK线,那对STM32来说引脚开销就太大了。

6.4 移植到ESP32或树莓派Pico的可行性

这几年轻量级MCU越来越流行,不少人问这套驱动能不能移植到ESP32上。答案是能,而且改动量不大。ESP32的SPI外设支持16位数据帧,CPOL和CPHA可配置,底层函数换成ESP-IDF的spi_device接口就行。

关键点在于ESP32的SPI主设备时钟最高可以到80MHz,但ADS1118的SPI最大时钟是4MHz。所以分频系数要特意调大,比如ESP32使用80MHz时钟源时,至少配置40分频,让SCK落到2MHz以内。信号完整性方面,SPI线越短越好,如果线长超过10cm,建议降低到1MHz以下。从F103迁到ESP32时,你会发现ESP32的浮点运算能力强得多,温度换算全部用float也不会拖慢节奏,这反而让代码更直观。

移植过程中还有一个小技巧:把SPI传输函数统一封装成“发16位同时收16位”的原子操作,这样适配不同的SPI驱动框架时,只需要重写这一个函数。不管是HAL库的HAL_SPI_TransmitReceive,还是ESP-IDF的spi_device_transmit,都能在这一层接住。

我自己在做第二次移植(从F103迁到G0系列)时就发现,一开始把底层的SPI锁存、等待步骤写得太复杂反而难移植。后来统一收敛成发送配置、读取数据的两个基础函数,后续移植基本就是几小时的事。这套驱动最值得学习的不是那些寄存器配置技巧,而是分层和封装的思路——芯片驱动和平台无关,你会愿意往自己的项目库里收藏一套这样的代码。希望这套ADS1118驱动能帮你少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询