ADS1118驱动开发:SPI时序、量程切换与工程避坑笔记
简介:面向51单片机开发者的ADS1118高精度ADC驱动源代码,基于C语言编写并带有详细注释,主要解决模拟电压采集、转换及通过串口发送到PC实时显示的问题。程序结构清晰,包含初始化、配置、读取数据等完整函数,适合需要快速上手ADS1118或学习单片机与外部ADC芯片通信的工程师与爱好者。压缩包共17个文件,约41KB,以C源文件、头文件、Keil工程文件、Hex烧录文件以及备份和编译中间文件为主,其中源文件和头文件构成程序主体,工程文件与Hex可方便导入或直接烧录验证。该资源已有841人学习下载,对于理解ADC工作原理和嵌入式数据采集流程有实际参考价值。借助代码注释,可掌握ADS1118的寄存器配置、多通道读取及串口输出逻辑,还能基于现有工程模板快速修改移植到自己的电压采集项目中,减少重复开发工作。 ADS1118这颗芯片,我先后在三个项目里用过:电池单体电压巡检、热电偶冷端补偿采集、称重传感器的毫伏信号采集。第一次调它的时候,我以为只不过是把ADS1115从I2C换成SPI接口而已,结果被“一个SPI帧同时完成读写”这个机制卡了整整一天。后来把寄存器时序和源代码理顺了,才发现这颗16位AD芯片其实非常顺手——既能做高精度采集,又自带温度传感器和PGA放大,单芯片能顶好几个外围器件的活。这篇文章把驱动源码、量程切换时序、以及我实测踩过的坑一次性写清楚,适合正在做嵌入式高精度采集、需要16位分辨率但预算有限、又不想上24位ADS1256的工程师参考。
1. 为什么我会在采集项目里反复选择ADS1118
1.1 16位分辨率与实际有效位数
ADS1118是TI推出的16位Δ-Σ ADC,采样率从8SPS到860SPS可选,供电范围2.0V到5.5V,内置可编程增益放大器(PGA)和低温漂基准。单从参数表看,它就是为“工业级中等精度采集”设计的:16位分辨率意味着在±4.096V量程下理论LSB是125µV,这个精度用于电池电压监测、温度采集、应变桥称重都够用。
但这里我必须说句实在话:16位分辨率不等于16位有效精度。Δ-Σ ADC的有效位数受噪声影响很大,尤其在高增益档位下,实际有效分辨率往往会掉到14位甚至更低。我见过不少新手看到“16位”就以为能分辨到微伏级,结果一测噪声就懵了。所以选型时你要清楚,ADS1118定位是“低成本中等精度”,不是仪表级的24位方案。
1.2 与ADS1115、ADS1256的取舍
很多工程师第一个接触的芯片是ADS1115(I2C接口),然后看到ADS1118觉得“不就是SPI版本吗”。这个理解对了一半。ADS1118和ADS1115核心ADC结构相似,但多了几个关键能力:内置温度传感器、DOUT/DRDY复用引脚、内部上拉使能选项。尤其是内置温度传感器,做热电偶采集时可以用它做冷端补偿,省掉一颗独立的温度传感器芯片,成本和布线都省了不少。
我在项目里做选型时通常这样取舍:
| 芯片 | 接口 | 分辨率 | 内置温度传感器 | 适用场景 |
|---|---|---|---|---|
| ADS1115 | I2C | 16位 | 无 | 常规电压/电流采集 |
| ADS1118 | SPI | 16位 | 有 | 热电偶、称重、多通道采集 |
| ADS1256 | SPI | 24位 | 无 | 高精度测量、微弱信号 |
如果系统里已经有I2C总线且挂载设备不多,用ADS1115够;如果走SPI且需要冷端补偿,ADS1118是更优解;真到了微伏级测量,老老实实上ADS1256。选ADS1118的最大理由,其实是它在“精度、成本、功耗、外围复杂度”之间找到了一个很舒服的平衡点。
2. 配置寄存器和量程切换的关键时序
2.1 配置寄存器的位段拆解
ADS1118的配置寄存器是16位,通过SPI帧写入。它的位段分布和ADS1115不一样,不能直接照搬,我第一次就是栽在这里。按数据手册定义,各位段大致如下(以TI官方手册为准):
- Bit 15:OS。单次模式下写1启动一次转换;连续模式下读取此位可判断转换是否完成。
- Bit 14:保留位。
- Bit 13-10:MUX[3:0],输入多路选择器配置,决定AIN0-AIN3之间是差分还是单端。
- Bit 9-7:PGA[2:0],增益配置,对应±6.144V到±0.256V六档量程。
- Bit 6:MODE,0为连续转换,1为单次转换。
- Bit 5-3:DR[2:0],数据速率配置。
- Bit 2:TS_MODE,0为ADC模式,1为温度传感器模式。
- Bit 1:PULL_UP_EN,使能DOUT/DRDY引脚内部上拉。
- Bit 0:NOP,保留位,通常写0。
写这16位配置字时,我习惯用宏定义把每个字段的编码值先算好,避免硬编码。这样代码的可读性和可维护性会好很多,后面切换通道、切换量程都只是改一个宏的事。
2.2 一个SPI帧同时完成读写
ADS1118和I2C接口芯片最大的不同在于:每次16位SPI传输是同时进行的。主控制器通过DIN引脚移出16位配置字,同时从DOUT引脚移入16位转换结果。也就是说,你在写“下一次配置”的同时,读到的其实是“上一次转换结果”。
这个机制非常高效,但也让很多第一次用的人犯迷糊。比如你想切换量程并读取新数据,如果只发一个帧,拿到的一定是切换前的旧数据。正确做法是先发一个含新配置的帧启动转换,等转换完成后再发第二帧读结果。理解了这一帧两用的机制,后续代码逻辑就顺了。
2.3 重新配置量程后到底要等多久
这是很多人在实际项目中最关心的问题:我改了PGA量程,多久之后读到的数据才是新量程下的有效数据?
答案取决于你用的是单次模式还是连续模式。
单次模式下,你在配置字里置OS=1,芯片会放弃当前空闲状态,按新配置启动一次完整转换。转换完成后DOUT/DRDY引脚会拉低(需先配置上拉或外部上拉),或者你可以按数据率算时间。比如DR设为128SPS,一个转换周期约7.8125ms,实际等10ms左右就能安全读取。如果设成8SPS,转换周期是125ms,你至少要等130ms,提前读回来的码值没有意义。
连续模式下,切换量程后芯片并不会立刻丢弃旧配置下的转换结果。正在进行的那个转换周期仍按旧PGA完成,随后才切换到新配置。所以你写完新配置后需要等1到2个转换周期,并且建议把前1到2次读到的数据直接丢弃,从第三个数据开始使用。这个“丢弃预热数据”的步骤,在连续模式采集里几乎是必须的,别省。
数据速率和转换时间的对应关系,我建议直接记一张表:
| DR设置 | 数据速率 | 单次转换周期 |
|---|---|---|
| 000 | 8 SPS | 125 ms |
| 001 | 16 SPS | 62.5 ms |
| 010 | 32 SPS | 31.25 ms |
| 011 | 64 SPS | 15.625 ms |
| 100 | 128 SPS | 7.8125 ms |
| 101 | 250 SPS | 4 ms |
| 110 | 475 SPS | 2.105 ms |
| 111 | 860 SPS | 1.1628 ms |
如果你用轮询方式而不用DRDY中断,我建议的余量是转换周期的1.2到1.5倍,别卡着理论值。芯片在单次模式下每次启动转换还有一小段内部振荡器稳定时间,卡太紧会导致偶尔读到旧数据,排查起来很费劲。
3. ADS1118驱动源代码:一次SPI帧完成读写
3.1 底层SPI传输层
先把底层SPI传输函数写好。这里我给一个不依赖具体MCU平台的模拟SPI实现,方便你移植到任意芯片。用硬件SPI的话,把下面函数替换成你平台的SPI收发函数即可,核心逻辑不变。
// ads1118.h
#ifndef __ADS1118_H
#define __ADS1118_H
#include <stdint.h>
// 配置寄存器位定义(按数据手册,不同版本请以手册为准)
#define ADS1118_CFG_OS (1u << 15) // 单次转换启动位
// MUX 输入通道选择,起始位 bit10
#define ADS1118_MUX_AIN0_AIN1 (0u << 10)
#define ADS1118_MUX_AIN0_AIN3 (1u << 10)
#define ADS1118_MUX_AIN1_AIN3 (2u << 10)
#define ADS1118_MUX_AIN2_AIN3 (3u << 10)
#define ADS1118_MUX_AIN0_GND (4u << 10)
#define ADS1118_MUX_AIN1_GND (5u << 10)
#define ADS1118_MUX_AIN2_GND (6u << 10)
#define ADS1118_MUX_AIN3_GND (7u << 10)
// PGA 增益,起始位 bit7
#define ADS1118_PGA_6144 (0u << 7) // ±6.144V
#define ADS1118_PGA_4096 (1u << 7) // ±4.096V
#define ADS1118_PGA_2048 (2u << 7) // ±2.048V
#define ADS1118_PGA_1024 (3u << 7) // ±1.024V
#define ADS1118_PGA_512 (4u << 7) // ±0.512V
#define ADS1118_PGA_256 (5u << 7) // ±0.256V
// MODE,bit6
#define ADS1118_MODE_CONTINUE (0u << 6)
#define ADS1118_MODE_SINGLE (1u << 6)
// DR 数据速率,起始位 bit3
#define ADS1118_DR_8SPS (0u << 3)
#define ADS1118_DR_16SPS (1u << 3)
#define ADS1118_DR_32SPS (2u << 3)
#define ADS1118_DR_64SPS (3u << 3)
#define ADS1118_DR_128SPS (4u << 3)
#define ADS1118_DR_250SPS (5u << 3)
#define ADS1118_DR_475SPS (6u << 3)
#define ADS1118_DR_860SPS (7u << 3)
// TS 温度传感器模式,bit2
#define ADS1118_TS_ADC (0u << 2)
#define ADS1118_TS_TEMP (1u << 2)
// PULL-UP EN,bit1
#define ADS1118_PU_ENABLE (1u << 1)
uint16_t ads1118_transfer_16(uint16_t tx);
float ads1118_read_voltage(uint16_t mux, uint16_t pga, uint16_t dr);
float ads1118_read_temperature(void);
#endif
底层传输我用简单GPIO模拟,循环16次逐位收发。注意采样边沿要和芯片匹配,ADS1118是在SCLK上升沿采样DOUT,所以主机置高SCLK后读取输入引脚。如果你用硬件SPI,配置成SPI Mode 1(CPOL=0, CPHA=1)或Mode 3(CPOL=1, CPHA=1)一般都能跑通,具体以你平台实测为准。
// ads1118.c
#include "ads1118.h"
#include "delay.h"
#include "gpio.h" // 平台相关GPIO操作,自行适配
#define ADS1118_CS_LOW() GPIO_CS_RESET()
#define ADS1118_CS_HIGH() GPIO_CS_SET()
#define ADS1118_SCLK(x) GPIO_SCLK_WRITE(x)
#define ADS1118_DIN(x) GPIO_DIN_WRITE(x)
#define ADS1118_DOUT() GPIO_DOUT_READ()
// 16位数据传输,发送tx同时接收rx
uint16_t ads1118_transfer_16(uint16_t tx)
{
uint16_t rx = 0;
int8_t i;
ADS1118_CS_LOW();
for (i = 15; i >= 0; i--) {
ADS1118_DIN((tx >> i) & 0x01);
ADS1118_SCLK(0);
ADS1118_SCLK(1); // 上升沿,主机采样DOUT
rx = (rx << 1) | (ADS1118_DOUT() ? 1 : 0);
}
ADS1118_SCLK(1);
ADS1118_CS_HIGH();
return rx;
}
3.2 单次模式读取流程
单次模式是我最推荐的工作模式。虽然多了一个等待转换周期,但功耗低、逻辑清晰、时序好控制,多通道采集时不会串数据。读取一个通道电压的完整流程如下:
static float ads1118_lsb(uint16_t pga)
{
switch (pga) {
case ADS1118_PGA_6144: return 6.144f / 32768.0f;
case ADS1118_PGA_4096: return 4.096f / 32768.0f;
case ADS1118_PGA_2048: return 2.048f / 32768.0f;
case ADS1118_PGA_1024: return 1.024f / 32768.0f;
case ADS1118_PGA_512: return 0.512f / 32768.0f;
case ADS1118_PGA_256: return 0.256f / 32768.0f;
default: return 4.096f / 32768.0f;
}
}
static void ads1118_wait_conversion(uint16_t dr)
{
// 按数据率计算等待时间,这里按128SPS配置示例
// 更稳妥的方式是等待DOUT/DRDY引脚拉低
delay_ms(10); // 128SPS 对应约8ms,取10ms余量
}
float ads1118_read_voltage(uint16_t mux, uint16_t pga, uint16_t dr)
{
uint16_t cfg = mux | pga | ADS1118_MODE_SINGLE | dr | ADS1118_TS_ADC;
int16_t raw;
// 第一帧:写入配置并置OS启动转换,返回值是旧数据,丢弃
(void)ads1118_transfer_16(cfg | ADS1118_CFG_OS);
// 等待转换完成
ads1118_wait_conversion(dr);
// 第二帧:读回本次转换结果,同时写入相同配置
raw = (int16_t)ads1118_transfer_16(cfg | ADS1118_CFG_OS);
return (float)raw * ads1118_lsb(pga);
}
第一帧会把配置写进寄存器并启动转换,第二帧顺带写入相同配置是为了保持寄存器状态不变。如果你在循环里连续调用这个函数,每一帧既是上一帧的延续,又是下一帧的开始,逻辑很顺畅。
3.3 连续模式读取流程
连续模式下不需要每次启动转换,芯片会按设定的数据率不停转换。你只要周期性地拉CS发帧,就能在DOUT上读到上一次的结果。这里有个容易踩的坑:连续模式下,配置字里不要置OS位。一旦置了OS,芯片可能从连续模式跳回单次行为,结果会乱。
float ads1118_read_voltage_continuous(uint16_t mux, uint16_t pga, uint16_t dr)
{
uint16_t cfg = mux | pga | ADS1118_MODE_CONTINUE | dr | ADS1118_TS_ADC;
static uint8_t first_read = 1;
int16_t raw;
if (first_read) {
// 上电或切换通道后第一次读,配置写入并丢弃预热数据
(void)ads1118_transfer_16(cfg);
delay_ms(20);
first_read = 0;
}
raw = (int16_t)ads1118_transfer_16(cfg);
return (float)raw * ads1118_lsb(pga);
}
连续模式的优点是读取速度快、无需反复启动转换,适合需要连续监测的场景。但多通道扫描时你得自己管理通道切换和预热丢弃,稍麻烦一些。我的经验是:如果不是对采样连续性有硬性要求,单次模式更省心。
3.4 内置温度传感器读取
ADS1118内置温度传感器的读取方式和ADC通道不同,只需要把TS_MODE位置1。转换结果是14位补码数据,左对齐存在16位寄存器的高14位,实际使用时右移2位,再乘以0.03125°C/LSB。
float ads1118_read_temperature(void)
{
uint16_t cfg = ADS1118_MUX_AIN0_AIN1 | ADS1118_PGA_4096 |
ADS1118_MODE_SINGLE | ADS1118_DR_128SPS |
ADS1118_TS_TEMP;
int16_t raw;
float temp;
// 启动温度转换
(void)ads1118_transfer_16(cfg | ADS1118_CFG_OS);
delay_ms(10);
// 读回温度结果
raw = (int16_t)ads1118_transfer_16(cfg | ADS1118_CFG_OS);
temp = (float)(raw >> 2) * 0.03125f;
return temp;
}
这个温度传感器精度虽然比不上专业的数字温度芯片,但做热电偶冷端补偿足够。实际项目中,我通常开机先读一次温度作为冷端参考,然后周期性更新,这样热电偶测出来的温度就是冷端补偿后的真实温度,误差能控制在可接受范围。
4. SPI时序细节与几个实测过的坑
4.1 时钟极性和相位不能拍脑袋定
ADS1118的SPI时序要求SLCK空闲时高低电平、数据在哪个边沿采样,这些都是数据手册里明确画了的。我最初直接用主控默认的Mode 0,结果读出来的数据完全不对,后来仔细对时序才发现采样边沿反了。
ADS1118更适合用SPI Mode 1(CPOL=0, CPHA=1)或者Mode 3(CPOL=1, CPHA=1),核心是主机在SCLK上升沿采样DOUT数据,而从机在下降沿更新数据。用模拟SPI时,我上面的代码已经固定了“先拉低SCLK,再拉高,然后采样”的顺序,相当于Mode 1。用硬件SPI时,建议先在数据手册上核对采样边沿,再用逻辑分析仪实测波形,别光靠猜。
4.2 DOUT/DRDY复用引脚不能随便接
ADS1118的DOUT和DRDY共用同一个引脚,这一点和ADS1115有明显区别。很多参考电路里这个引脚直接接MCU的MISO,但如果你想用中断方式等待转换完成,得把这个引脚同时接一个GPIO输入,或者用带中断功能的MISO引脚。
我踩过的坑是这样的:单次模式里不查DRDY、只靠延时,本来没问题,但我把延时算得太死,数据率从128SPS切到860SPS时忘了改延时,程序偶尔会读到上一次转换的旧数据。后来改成等待DRDY引脚拉低再读,问题立刻消失。所以我的建议是:如果IO资源允许,DOUT/DRDY最好接一个可读取的GPIO,用“配置启动→等DRDY→读数据”这套流程,比死等延时可靠得多。
4.3 配置写进去但不生效的排查链路
如果你发现改了PGA或通道后读到的值完全没变化,先别怀疑芯片坏了,按下面顺序排查:
- 确认写入的配置字真的通过SPI发出去了。用逻辑分析仪抓DIN上的波形,逐位核对配置字。
- 确认CS时序完整。CS拉低期间SCLK必须完整跑16个边沿,提前拉高会导致配置字只写入一半,芯片直接忽略。
- 确认没有在连续模式下误置OS位。这个问题我在3.3里提过,一旦置位,芯片可能卡在单次逻辑里。
- 确认SPI时钟不超过4MHz。ADS1118的最高SCLK频率约4MHz,我试过用10MHz时钟跑,读出来的数据偶发错位,表现为个别bit随机跳变,很难定位。
其中第4个问题很隐蔽。你如果只是做简单验证,低速跑没问题,一旦系统时钟跑快,随机误码就开始冒出来。这也是为什么我建议SPI时钟降一档,3MHz以内用起来很稳定。
5. 从源代码到稳定采集:噪声、标定和PCB经验
5.1 不同量程下的有效分辨率实测
我特意做过一组测试:把ADS1118输入短接到GND,在不同PGA档位下连续读1000个点,统计码值波动范围。结果是PGA=±4.096V档位时波动大约±2~3个LSB,PGA=±0.256V档位时波动明显增大,实际有效分辨率会掉到14位附近。这说明高增益放大的同时,输入等效噪声和基准噪声都被一起放大了。
所以你在选择量程时,不是越小越好。如果信号是1V左右的直流电压,我建议选±2.048V或±4.096V档,留够余量的同时兼顾分辨率。只有像称重传感器这种毫伏级信号,才需要上高增益档,但这时就要做好软件滤波。
简单滤波我常用滑动平均和中值滤波结合:滑动平均对高斯噪声效果好,中值滤波能干掉偶尔出现的毛刺。比如128SPS采样率,取16点滑动平均,等效噪声能降一个量级左右,响应速度还在可接受范围。
5.2 内部基准、电源去耦和PCB布局
ADS1118内置基准,省掉了外部基准芯片,但内部基准不是完美的。温度变化会引起基准漂移,进而影响转换结果。如果你做的是跨季节、跨温区的户外采集设备,建议定期做一次标定,或者把设备温度一起记录,后续软件补偿。
供电上去耦电容要靠近电源引脚放,我用的是0.1µF陶瓷电容加4.7µF钽电容组合,分别放在芯片电源脚附近。DVDD和AVDD最好分别走线,别共用一个细走线。模拟输入走线越短越好,在输入端加一阶RC低通滤波(比如1kΩ加0.1µF),截至频率约1.6kHz,对高频干扰有明显抑制。
多说一句PCB布局:ADS1118是SSOP-10小封装,引脚间距小,手工焊接容易连锡。焊完后用万用表量一遍电源对地阻抗,确认没短路再上电。我第一次画板就吃过这个亏,一个连锡导致A0通道电压始终不对,查了半天才发现是两个相邻引脚焊在一起了。
5.3 量程切换后的标定流程
每次量程切换后,不能直接信任读出的电压,尤其是高增益档位。我常用的标定流程很简单:
- 输入短接到GND,读100个码值取平均,记录零点偏移。
- 输入一个已知精密电压(比如用参考电压源或高精度LDO输出),读100个码值取平均,计算实际增益。
- 把零点和增益写入Flash,每次上电后做一次两点校准。
实际操作中,ADS1118的零点偏移在单次模式下通常很小,但高增益档下不做零点修正,测量结果会有明显偏差。做了这个标定之后,我测得的最差精度基本都能落在0.1%以内,对于项目需求已经足够了。
我个人在实际项目里最深的体会是:ADS1118这颗芯片本身很皮实,绝大多数问题都出在时序和配置上。把寄存器位字段吃透,把SPI帧一图两用的机制理解透,再结合DRDY做同步,代码写起来就会非常稳。如果你正在调这颗芯片,建议先把单次模式跑通,再逐步加连续模式、温度传感器和量程切换,一步一步验证,别指望一把梭能调通。
更多推荐

所有评论(0)