简介:基于STM32F103与STM32F407的ADS1118完整驱动方案,主要面向嵌入式开发者、电子竞赛学生以及从事高精度数据采集的工程师。程序覆盖4路单端、2路双差分和片内温度传感器三种采集模式,实现从寄存器初始化、SPI读写时序到多通道数据转换与错误处理的全流程封装。代码结构清晰,关键函数均附带注释,便于直接移植到实际项目,适合用于温度监测、压力采集或工业模拟量输入等场景。资源同时提供对应的实验测试数据,包含不同输入模式下的转换结果与温度读数,可借此校验驱动正确性,并了解芯片实际精度与噪声表现。配套的ADS1118技术手册与应用说明,能帮助读者深入理解16位ADC的配置逻辑、增益选择和采样率设定,是一份上手与进阶兼顾的实用资料。整套资源以RAR压缩包发布,大小约11.82MB,已吸引318人学习参考,对于希望快速掌握ADS1118开发的工程师和学生而言,具有较高的学习和参考价值。 做多路模拟量采集或者热电偶测温的时候,ADS1118 算是一颗绕不开的片子:16 位分辨率、内置 PGA、自带温度传感器,还通过 SPI 接口和 MCU 通信。但真开始写驱动才发现,网上能找到的例程大多是 ADS1115 的 I2C 版本,SPI 的 ADS1118 要么是寄存器片段,要么只支持单通道,连 STM32F103 和 STM32F407 的初始化差异都要自己慢慢试。前段时间我把这套驱动完整整理了一遍,覆盖 4 路单端、2 路差分和内部温度传感器三种模式,顺手把两代 STM32 的移植差异也理清了。这篇就按我的实际工程顺序讲一遍——哪些地方必须设置对、哪些坑我踩过之后不会再踩,照着抄基本能一次点亮。
1. 先从硬件说起:为什么 ADS1118 值得单独写一份驱动
ADS1118 是 TI 的 16 位 ΔΣ ADC,和更常见的 ADS1115 最大的区别是接口:ADS1115 走 I2C,ADS1118 走 SPI。很多人一开始会疑惑,同样是 16 位 ADC,为什么不用 I2C 版本?答案很简单,SPI 没有设备地址限制、速率更高,而且 ADS1118 的数据率最高能到 860SPS,做多通道轮询时比 I2C 舒服很多。更重要的是,ADS1118 内部集成了一个温度传感器,这颗传感器是专门为热电偶冷端补偿设计的。也就是说,一路差分接热电偶,同时读内部温度做冷端补偿,一颗芯片全干完了。
硬件接线不复杂,核心引脚就是标准的 SPI 四线:
| ADS1118 引脚 | STM32F103 | STM32F407 | 说明 |
|---|---|---|---|
| SCLK | PA5 | PA5 | SPI1 时钟 |
| DIN | PA7 | PA7 | SPI1 MOSI,写命令字 |
| DOUT/DRDY | PA6 | PA6 | SPI1 MISO,读数据/转换完成指示 |
| CS | PA4 | PA4 | 任意 GPIO,软件控制 |
| VDD | 3.3V | 3.3V | 模拟和数字电源共用 |
| GND | GND | GND | 必须共地 |
有两个硬件细节我建议一开始就处理好。第一,ADS1118 的输入绝对电压范围不能超过 VDD + 0.3V,即使 PGA 配置成 ±6.144V 满量程,输入引脚也扛不住 6V。如果你要测 5V 或者更高的电压,必须用电阻分压,把信号按比例降到 3.3V 以内。第二,DOUT/DRDY 这个引脚比较特殊,它既是数据输出,又是转换完成标志。有的板子 DOUT 悬空时电平不稳定,建议在 DOUT 上接一个 10kΩ 上拉到 3.3V,这样查询 DRDY 低电平时更可靠,也不会影响 SPI 时序。
至于电源去耦,ADS1118 的 AVDD 和 DVDD 可以接在一起,但芯片旁边至少要放一个 0.1µF 陶瓷电容,最好再并一个 10µF 钽电容。模拟输入如果来自高阻抗源,比如热电偶,信号线上加一个 RC 低通滤波会稳很多,后面实测部分我再细说。
2. 移植前必须先搞清楚的 SPI 初始化差异(F103 vs F407)
ADS1118 对 SPI 的时序要求很明确:CPOL=1、CPHA=1,也就是常说的 SPI Mode 3,数据帧必须是 16 位,高位在前,SCLK 最高 4MHz。这几个参数只要有一个不对,读回来的数据就是乱的。
为什么必须是 Mode 3?简单说,ADS1118 在 SCLK 的下降沿更新输出数据,主机在上升沿采样。SPI Mode 3 空闲时时钟为高,第一个边沿是下降沿,正好用来更新数据,第二个边沿是上升沿,主机采样。如果你配置成 Mode 0,采样沿和更新沿完全反过来,数据就错位了。这个错位不是每一次都错,偶尔能读出看似正常的数据,但会时不时跳变,非常难排查。
时钟分频也要算一下。ADS1118 的 SCLK 上限是 4MHz,STM32F103 的 SPI1 挂在 APB2 上,72MHz 主频下 16 分频是 4.5MHz,超了;STM32F407 的 SPI1 同样在 APB2,如果按 168MHz 主频、APB2 取 84MHz,16 分频是 5.25MHz,同样超了。所以两边我都用 32 分频,F103 实际 2.25MHz,F407 实际 2.625MHz,都落在安全范围内。实测下来这个速度对 ADS1118 完全够用,16 位数据一次事务也就几微秒。
初始化代码我用标准外设库写,F103 和 F407 的差异点主要在两处:RCC 使能时钟的写法相同,但 F407 需要额外调用 GPIO_PinAFConfig 把引脚复用成 SPI1 功能。SPI 外设本身的初始化参数,两套平台完全一致:
/* F103 与 F407 通用:SPI1 初始化 */ void ADS1118_SPI_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_I2S_DeInit(SPI1); 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_High; /* Mode 3 */ SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; /* Mode 3 */ 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); }GPIO 部分 F103 这样写:
void ADS1118_GPIO_Init_F103(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK PA5, MOSI PA7 复用推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* MISO PA6 浮空输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); /* CS PA4 普通推挽输出,默认拉高 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); }F407 的差别就是复用映射:
void ADS1118_GPIO_Init_F407(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 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_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); /* CS 仍然普通 GPIO */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); }CS 用普通 GPIO 控制,别用 SPI 的硬件 NSS。ADS1118 要求在每次 SPI 事务之间有完整的 CS 高电平恢复时间,软件手动控制最灵活,硬件 NSS 在多字节传递时会自动翻转电平,容易出问题。
3. ADS1118 的核心交互逻辑:写一次配置,读回上一轮数据
ADS1118 的配置寄存器一共 16 位,每次 SPI 事务,主机在 MOSI 上发送 16 位命令字,ADS1118 同时在 MISO 上把上一次的转换结果移出来。这个"边写边读旧值"的机制是所有驱动的关键,不理解它,代码写出来就是乱的。
配置寄存器各位的定义如下:
| 位 | 名称 | 作用 |
|---|---|---|
| bit15 | SS | 单次转换启动位,1 触发转换 |
| bit14-12 | MUX | 输入通道选择 |
| bit11-9 | PGA | 满量程/增益配置 |
| bit8 | MODE | 1 单次模式,0 连续模式 |
| bit7-5 | DR | 数据率选择 |
| bit4 | TS | 1 启用内部温度传感器 |
| bit3-0 | 保留 | 写 0 |
单次模式下的完整转换流程应该是:
- CS 拉低,发送带 SS=1 的配置字,启动一次转换。此时 MISO 移出的是上电默认值或者上一次转换的结果,这个数据不能要。
- CS 拉高,等待转换完成。等待时间由数据率决定,也可以用 DOUT/DRDY 引脚查询。
- CS 再次拉低,发送不带 SS 位的相同配置字,MISO 返回的就是本次转换结果。
- CS 拉高,解析数据。
为什么第二次发送配置字要不带 SS?因为 ADS1118 在单次模式下,SS=1 会再触发一次新转换。你只是想读当前结果,结果又启动了一次转换,逻辑就乱了。不带 SS 只是把相同配置重新写一遍,不触发转换,MISO 移出的正好是刚转换完的数据。
第一次读回的数据为什么不能要?很多人上电后第一次读,发现读回一个 0xFFFF 或者奇怪的固定值,以为是硬件问题。其实就是因为 MISO 移出的是旧数据,和当前输入信号一点关系都没有。这个不是故障,是芯片的设计行为。
转换等待时间直接查数据率:
| 数据率 | 转换时间 | 建议延时 |
|---|---|---|
| 8 SPS | 125ms | 130ms |
| 16 SPS | 62.5ms | 70ms |
| 32 SPS | 31.25ms | 40ms |
| 64 SPS | 15.625ms | 20ms |
| 128 SPS | 7.8ms | 10ms |
| 250 SPS | 4ms | 6ms |
| 475 SPS | 约 2.1ms | 4ms |
| 860 SPS | 约 1.16ms | 2ms |
延时是最简单的方案,适合绝大多数项目。如果想更精确,可以查询 DOUT/DRDY 引脚。这个引脚在转换完成时会输出低电平,检测到低电平再读数据,不需要留那么多裕量。但要注意,DOUT 就是 SPI 的 MISO 引脚,查询低电平时要把引脚从 SPI 复用模式切回普通输入模式,读完再切回去,代码会麻烦不少。我的经验是,除非对采样间隔有严格实时性要求,否则直接延时反而更省心。
4. 四路单端、两路差分和温度传感器的全部驱动代码
驱动文件我拆成 ads1118.h 和 ads1118.c 两部分,全部代码可以直接搬进工程。先看头文件,配置宏都定义在这里:
/* ads1118.h */ #ifndef __ADS1118_H #define __ADS1118_H #include "stm32f10x.h" /* 如果用 F407,改成 stm32f4xx.h */ /* 配置寄存器位定义 */ #define ADS1118_CFG_SS 0x8000u #define ADS1118_CFG_MUX_MASK 0x7000u #define ADS1118_CFG_MUX_DIFF_0_1 0x0000u #define ADS1118_CFG_MUX_DIFF_0_3 0x1000u #define ADS1118_CFG_MUX_DIFF_1_3 0x2000u #define ADS1118_CFG_MUX_DIFF_2_3 0x3000u #define ADS1118_CFG_MUX_AIN0 0x4000u #define ADS1118_CFG_MUX_AIN1 0x5000u #define ADS1118_CFG_MUX_AIN2 0x6000u #define ADS1118_CFG_MUX_AIN3 0x7000u #define ADS1118_CFG_PGA_6144 0x0000u #define ADS1118_CFG_PGA_4096 0x0200u #define ADS1118_CFG_PGA_2048 0x0400u #define ADS1118_CFG_PGA_1024 0x0600u #define ADS1118_CFG_PGA_512 0x0800u #define ADS1118_CFG_PGA_256 0x0A00u #define ADS1118_CFG_MODE_CONT 0x0000u #define ADS1118_CFG_MODE_SINGLE 0x0100u #define ADS1118_CFG_DR_8SPS 0x0000u #define ADS1118_CFG_DR_16SPS 0x0020u #define ADS1118_CFG_DR_32SPS 0x0040u #define ADS1118_CFG_DR_64SPS 0x0060u #define ADS1118_CFG_DR_128SPS 0x0080u #define ADS1118_CFG_DR_250SPS 0x00A0u #define ADS1118_CFG_DR_475SPS 0x00C0u #define ADS1118_CFG_DR_860SPS 0x00E0u #define ADS1118_CFG_TS 0x0010u /* 默认基础配置:PGA=2048,单次模式,128SPS */ #define ADS1118_BASE_CFG (ADS1118_CFG_PGA_2048 | ADS1118_CFG_MODE_SINGLE | ADS1118_CFG_DR_128SPS) #define ADS1118_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define ADS1118_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) void ADS1118_Init(void); uint16_t ADS1118_Transfer(uint16_t cmd); uint16_t ADS1118_ReadADC(uint16_t mux_cfg); int16_t ADS1118_ReadDiff(uint16_t mux_cfg); float ADS1118_ReadTemp(void); #endif底层的 SPI 传输函数就是一次完整的 16 位收发,CS 包住整个事务:
/* ads1118.c */ #include "ads1118.h" void ADS1118_Init(void) { /* 调用前面写的 GPIO 和 SPI 初始化函数 */ ADS1118_GPIO_Init_F103(); /* 或 F407 */ ADS1118_SPI_Init(); } uint16_t ADS1118_Transfer(uint16_t cmd) { uint16_t rx = 0; ADS1118_CS_LOW(); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, cmd); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); rx = SPI_I2S_ReceiveData(SPI1); ADS1118_CS_HIGH(); return rx; } static void ADS1118_WaitConvert(uint16_t cfg) { /* DR 编码对应数据率:8/16/32/64/128/250/475/860 SPS */ static const uint32_t wait_ms[8] = {130, 70, 40, 20, 10, 6, 4, 2}; uint8_t dr = (uint8_t)((cfg >> 5) & 0x07); delay_ms(wait_ms[dr]); }单端和差分共用一个底层,区别只是 MUX 配置不同。读单端时原始值按无符号解析,读差分时按有符号二进制补码解析:
uint16_t ADS1118_ReadADC(uint16_t mux_cfg) { uint16_t cfg = ADS1118_BASE_CFG | (mux_cfg & ADS1118_CFG_MUX_MASK); /* 写配置并启动转换,此时读回的是旧数据,丢弃 */ ADS1118_Transfer(cfg | ADS1118_CFG_SS); /* 等待转换完成 */ ADS1118_WaitConvert(cfg); /* 重新写入相同配置,但不带 SS,读回本次转换结果 */ return ADS1118_Transfer(cfg); } int16_t ADS1118_ReadDiff(uint16_t mux_cfg) { uint16_t cfg = ADS1118_BASE_CFG | (mux_cfg & ADS1118_CFG_MUX_MASK); ADS1118_Transfer(cfg | ADS1118_CFG_SS); ADS1118_WaitConvert(cfg); return (int16_t)ADS1118_Transfer(cfg); }温度读取更简单,关键是把配置里的 TS 位置 1,然后读回来的原始值除以 128.0 就是摄氏度:
float ADS1118_ReadTemp(void) { uint16_t cfg = (ADS1118_BASE_CFG & ~ADS1118_CFG_MUX_MASK) | ADS1118_CFG_TS; ADS1118_Transfer(cfg | ADS1118_CFG_SS); ADS1118_WaitConvert(cfg); return (float)((int16_t)ADS1118_Transfer(cfg)) / 128.0f; }主程序里把三组数据全跑一遍,可以直观看到整个驱动的调用方式:
/* main.c 示例 */ const uint16_t mux_single[4] = { ADS1118_CFG_MUX_AIN0, ADS1118_CFG_MUX_AIN1, ADS1118_CFG_MUX_AIN2, ADS1118_CFG_MUX_AIN3 }; int main(void) { uint16_t single_raw[4]; int16_t diff_raw[2]; float temp; float voltage[4]; const float fsr = 2.048f; /* 和 BASE_CFG 里的 PGA_2048 对应 */ SystemInit(); ADS1118_Init(); while (1) { /* 4 路单端 */ for (int i = 0; i < 4; i++) { single_raw[i] = ADS1118_ReadADC(mux_single[i]); voltage[i] = (float)single_raw[i] * fsr / 32768.0f; } /* 2 路差分:AIN0-AIN1 和 AIN2-AIN3 */ diff_raw[0] = ADS1118_ReadDiff(ADS1118_CFG_MUX_DIFF_0_1); diff_raw[1] = ADS1118_ReadDiff(ADS1118_CFG_MUX_DIFF_2_3); /* 内部温度 */ temp = ADS1118_ReadTemp(); delay_ms(50); } }电压换算公式就是电压 = 原始值 × 满量程 / 32768。PGA=2048 时满量程是 ±2.048V,所以 1 LSB 对应 62.5µV。差分通道的满量程和单端一样,只是原始值要按 int16 处理,负电压对应负值。
温度换算为什么除以 128?ADS1118 内部温度传感器实际是 14 位有效数据,左对齐到 16 位寄存器,最后两位固定为 0。14 位数据每个 LSB 是 0.03125°C,左对齐后相当于 16 位数据每个 LSB 是 0.0078125°C,也就是除以 128。这个细节网上很多代码都没讲清楚,直接给一个 magic number,现在知道原理就不用怕了。
5. 实测最容易踩的四个坑
5.1 重新配置量程后,第一笔数据到底能不能要
这个问题也是很多人问过的:如果我重新配置量程,需要等待多少时间?答案是:不仅要等转换完成,还要丢掉切换后的第一笔转换结果。ADS1118 内部有可编程增益放大器,切换 PGA 增益后,放大器建立需要时间。你写完新配置立刻启动转换,虽然 DRDY 也正常拉低,转换时序也完整,但增益切换瞬间引入的建立误差还没消失,这笔数据往往偏大或偏小,看起来像毛刺。
工程上的做法是切换量程或通道后,先做一次完整转换并丢弃,再采集有效值。写成一个函数就是:
uint16_t ADS1118_ReadADC_Stable(uint16_t mux_cfg) { ADS1118_ReadADC(mux_cfg); /* 第一次结果丢弃 */ return ADS1118_ReadADC(mux_cfg); /* 第二次结果作为有效值 */ }如果你的采样循环一直在同一通道上跑,不需要每次都丢弃,只有切换配置的那一次需要处理。如果 4 路通道轮流采集,MUX 一直在切换,建议每路都按这个方式读,代价是采样周期变长,但数据可信度明显更高。
5.2 SPI Mode 用错后的症状
ADS1118 必须工作在 SPI Mode 3,也就是 CPOL=1、CPHA=1。我曾经为了在某个工程里和另一片 Flash 共用 SPI 总线,图省事把 CPOL 改成了 0,结果读出来的数据表现为:低位偶尔跳变、整数值偏差固定倍数、数据率和实际对不上。当时一度怀疑是 PCB 飞线太长干扰,后来单独用逻辑分析仪抓时序才发现是 SPI 模式不匹配。
判断 SPI 模式是否搞对,最直接的方法是切到 8 SPS 低速,读温度传感器。温度是缓慢变化的,如果连续读 20 次数值稳定、室温在合理范围,基本可以确认时序没问题。如果每次读出来数据乱跳,先查 Mode,别怀疑芯片。
5.3 DOUT 读不到数据的排查方向
读数据一直读到 0xFFFF 或者每次都一样,先检查三件事。第一,CS 拉低后有没有足够的建立时间,CS 下降沿到第一个 SCLK 上升沿之间至少要留出一个 SCLK 周期的时间,用软件控制时一般没问题,但如果 CS 引脚速度配置太慢,要留意。第二,DOUT 引脚是否浮空,ADS1118 的 DOUT/DRDY 在一些配置下需要上拉电阻,加了 10kΩ 上拉基本能解决。第三,确认 SPI 配置真的是 16 位数据帧末尾采样,如果错配成 8 位模式,一次事务只传输半个命令字,芯片根本不响应,读回的全是垃圾。
还有一个容易忽略的点:ADS1118 的 DOUT 在 CS 拉高时处于高阻态,如果 MISO 线上只有一个设备,问题不大;如果 MISO 上挂了多个 SPI 设备,务必确保其他设备的 CS 是拉高的,否则总线冲突会把数据拉坏。
5.4 采样稳定性与滤波建议
ADS1118 虽然分辨率高,但数据率越高,噪声越大。860SPS 下的有效分辨率明显不如 8SPS。实测 128SPS 是性价比最高的档位,响应速度和噪声控制都比较均衡。如果测的是温度这类缓变信号,建议降到 32SPS 或 16SPS,数据会稳很多。
硬件的 RC 滤波对抑制共模噪声很有用。输入引脚对地各接一个 100Ω 到 1kΩ 的串联电阻,再对地接 0.1µF 电容,能明显减少长线引入的毛刺。软件上做多次平均也有效,比如连续读 16 次取平均,代价是采样周期变长,但 128SPS 下 16 次平均仍然能保持每秒 7~8 次的更新率,绝大多数传感器场景都够用。我自己的项目里,128SPS 加 16 次平均,测 0~2V 的传感器信号,稳定度能做到最后一位基本不动,这个组合你们可以直接参考。
本文还有配套的精品资源,点击获取