简介:这是一套针对TI公司16位ADC芯片ADS1118、基于51单片机编写的C语言源代码工程,面向嵌入式初学者与需要高精度电压采集的开发者,适用于传感器信号采集、工业控制、仪表测量等场景,可直接学习或移植到实际项目。压缩包共17个文件、约41KB,内包含ads1118.c、main.c等核心源码与ADS1118.H头文件,以及Keil工程文件(uvproj/uvopt)、编译生成的hex/obj/lst/m51文件和若干备份;其中.c和.h用于代码阅读与修改,.uvproj/.uvopt用于打开工程,.hex可烧录,.lst/.m51便于分析编译细节,结构完整清晰。目前已有841人学习浏览,适合理解ADS1118的寄存器配置、SPI通信时序以及51单片机串口收发机制。代码注释详尽,主程序完整演示了初始化芯片、读取电压值并通过串口发送到PC的流程,还涉及波特率、通信参数等配置说明,能帮助使用者快速上手,并在理解ADC工作原理后自行调整代码,实现多通道采集或数据记录功能。 做16位数据采集项目时,ADS1118是一颗绕不开的芯片:SPI接口、16位分辨率、内置4.096V基准、自带温度传感器,价格不贵,非常适合做多路模拟量采集。但真正动手写驱动时你会发现,网上大量教程都是ADS1115的,I2C接口,寄存器配置和SPI时序完全是另一套逻辑,直接套用根本跑不通。这篇文章就把ADS1118的SPI时序、命令寄存器、源代码实现一次讲清楚,并且重点回答一个经常被忽略的问题:重新配置量程(PGA)之后,到底需要等待多久才能读到有效数据。
1. ADS1118到底是一颗什么样的ADC
1.1 ADS1118和ADS1115的本质区别
很多人选型时会先搜到ADS1115,再用到ADS1118,结果发现两者从接口到寄存器都不是一回事。ADS1115是I2C接口,而ADS1118是SPI接口,频率最高可以跑到4MHz。更关键的是,ADS1118内部集成了4.096V基准电压源,还带一个板载温度传感器,这两点是ADS1115没有的。也就是说,ADS1118不需要外部基准芯片,数据可以直接换算成绝对电压值,省掉一路基准电路。
另外要留意,ADS1118的PGA和ADS1115的PGA虽然看起来都是3位配置,但FSR档位并不完全一致。ADS1118常见的可用档位从±4.096V往下到±0.256V,共5档(个别版本手册也列了±6.144V,实际工程中极少使用)。这一点在写配置命令时特别容易踩坑——抄了ADS1115的寄存器值,很可能配置出一个超出芯片实际能力的量程。
1.2 写驱动前必须确认的四个关键参数
动手写代码之前,我建议先把下面四个参数在数据手册里圈出来,因为它们直接决定了驱动函数怎么写。
第一个是数据格式。ADS1118的转换结果是16位二进制补码,最高位是符号位。计算真实电压的公式是:
电压值 = 原始码值 × FSR / 32768例如PGA配置为±4.096V时,1个LSB对应4.096 / 32768 = 0.125mV。注意不是除以65536,因为FSR是正负对称的完整量程。
第二个是PGA和FSR的对应关系。ADS1118的PGA[2:0]位位于命令寄存器的bit11~bit9,常用配置如下:
| PGA[2:0] | FSR | 分辨率(1 LSB) |
|---|---|---|
| 001 | ±4.096V | 0.125mV |
| 010 | ±2.048V | 0.0625mV |
| 011 | ±1.024V | 0.03125mV |
| 100 | ±0.512V | 0.015625mV |
| 101 | ±0.256V | 0.0078125mV |
第三个是数据速率。DR[2:0]位于bit7~bit5,支持从8SPS到860SPS共8档,这个参数不仅决定采样快慢,也决定后面要讲的“配置生效等待时间”。
第四个是SPI工作模式。ADS1118要求SPI Mode 1,也就是CPOL=0、CPHA=1,这一点比寄存器配置还容易让人翻车。很多人照着ADS1115的代码写了个I2C转SPI的壳子,结果读回来的数据全是0xFF,问题往往就出在这里。
2. 驱动源码的结构与关键设计
2.1 命令寄存器的每一位到底怎么填
ADS1118的SPI通信核心是16位命令寄存器。每次传输数据时,首先要把这16位配置写到DIN引脚上。这16位的排列顺序是高位在前,具体的位定义如下:
bit15 保留位,写0 bit14~12 MUX[2:0],通道选择 bit11~9 PGA[2:0],增益配置 bit8 MODE,1表示单次模式,0表示连续模式 bit7~5 DR[2:0],数据速率 bit4 TS_MODE,1表示温度传感器模式,0表示ADC模式 bit3 PULL_UP_EN,DOUT引脚内部上拉使能 bit2 NOP,写0 bit1~0 保留位,写0通道选择MUX的编码也要记清楚,因为ADS1118的引脚不是纯粹的“8选1”单端输入,而是支持多种差分组合。比如MUX=000表示AIN0和AIN1差分输入,MUX=100表示AIN0对GND的单端输入。写驱动时最好把每个组合定义成宏,否则直接在函数里填数字,过两周自己都看不懂了。
这里有一个很容易忽略的点:命令寄存器的bit3是上拉使能位。如果你的DOUT引脚没有外部上拉,建议把这一位置1,否则在SCLK频率较高时,MISO线上的信号沿可能不够陡峭,导致采到错误数据。
2.2 SPI通信时序:一次CS低周期内发生了什么
ADS1118的SPI时序和普通SPI从机不太一样。常规做法是CS拉低,发送16位命令,然后继续发送16个SCLK来读取转换结果。也就是说,一次有效操作需要完整的32个SCLK,中间CS不能拉高。
具体过程是这样的:CS拉低后,主机向DIN发送配置命令,此时芯片会根据命令内容启动一次新的转换;接着主机再发送16个时钟,ADS1118会把最近一次完成的转换结果从DOUT线逐位移出。第一次发送命令时DOUT上也会移出数据,但那是上一次的旧数据,通常直接丢弃。因此代码里最简单的通信函数就是这样:
static uint16_t ads1118_transfer16(uint16_t tx) { uint8_t tx_buf[2] = { (uint8_t)(tx >> 8), (uint8_t)(tx & 0xFF) }; uint8_t rx_buf[2] = { 0, 0 }; HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, 10); return ((uint16_t)rx_buf[0] << 8) | rx_buf[1]; }这里要注意字节顺序。寄存器定义是高位在前,所以发送时先发高8位,再发低8位。读取时同样把收到的第一字节作为高8位。如果你用的MCU是STM32的HAL库,SPI配置为8位数据帧就可以,不用开16位帧模式。16位帧模式在部分STM32型号上会因为字节序配置不对导致高低字节互换,反而多一层麻烦。
3. 可直接使用的源代码实现
3.1 头文件与关键宏定义
下面这套驱动代码基于STM32 HAL库编写,但核心逻辑不依赖HAL,移植到其他平台只需要替换底层HAL_SPI_TransmitReceive和两个CS控制函数即可。
首先是头文件,把命令寄存器的常用配置全部定义成宏:
// ads1118.h #ifndef ADS1118_H #define ADS1118_H #include <stdint.h> // MUX通道配置 #define ADS1118_MUX_DIFF_0_1 0x0000 #define ADS1118_MUX_DIFF_0_3 0x1000 #define ADS1118_MUX_DIFF_1_3 0x2000 #define ADS1118_MUX_DIFF_2_3 0x3000 #define ADS1118_MUX_AIN0_GND 0x4000 #define ADS1118_MUX_AIN1_GND 0x5000 #define ADS1118_MUX_AIN2_GND 0x6000 #define ADS1118_MUX_AIN3_GND 0x7000 // PGA增益配置 #define ADS1118_PGA_4_096V 0x0200 #define ADS1118_PGA_2_048V 0x0400 #define ADS1118_PGA_1_024V 0x0600 #define ADS1118_PGA_0_512V 0x0800 #define ADS1118_PGA_0_256V 0x0A00 // 工作模式 #define ADS1118_MODE_CONTINUOUS 0x0000 #define ADS1118_MODE_SINGLE 0x0100 // 数据速率 #define ADS1118_DR_8_SPS 0x0000 #define ADS1118_DR_16_SPS 0x0020 #define ADS1118_DR_32_SPS 0x0040 #define ADS1118_DR_64_SPS 0x0060 #define ADS1118_DR_128_SPS 0x0080 #define ADS1118_DR_250_SPS 0x00A0 #define ADS1118_DR_475_SPS 0x00C0 #define ADS1118_DR_860_SPS 0x00E0 // 温度传感器模式 #define ADS1118_TS_MODE_ADC 0x0000 #define ADS1118_TS_MODE_TEMP 0x0010 uint16_t ads1118_read_raw(uint16_t config); float ads1118_read_voltage(uint16_t mux, uint16_t pga, uint16_t dr); float ads1118_read_temperature(void); #endif这里我把PGA从±4.096V开始定义,没有放±6.144V。ADS1118的内置基准就是4.096V,±6.144V档位在实际工程中几乎用不到,而且输入范围很容易超出电源轨导致芯片损坏,所以直接忽略反而更安全。
3.2 底层SPI收发函数与单次读取实现
C文件里先实现最底层的CS控制和SPI收发,然后实现一次完整的读写周期。单次模式的关键在于:发送配置命令后必须等转换完成,再发起第二次SPI传输读取结果。
// ads1118.c #include "ads1118.h" #include "spi.h" #include "gpio.h" static void ads1118_cs_low(void) { HAL_GPIO_WritePin(ADS1118_CS_GPIO_Port, ADS1118_CS_Pin, GPIO_PIN_RESET); } static void ads1118_cs_high(void) { HAL_GPIO_WritePin(ADS1118_CS_GPIO_Port, ADS1118_CS_Pin, GPIO_PIN_SET); } static uint16_t ads1118_transfer16(uint16_t tx) { uint8_t tx_buf[2] = { (uint8_t)(tx >> 8), (uint8_t)(tx & 0xFF) }; uint8_t rx_buf[2] = { 0, 0 }; HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, 10); return ((uint16_t)rx_buf[0] << 8) | rx_buf[1]; } static void ads1118_wait_conversion(uint16_t dr_cfg) { uint32_t delay_ms = 0; switch (dr_cfg) { case ADS1118_DR_8_SPS: delay_ms = 125; break; case ADS1118_DR_16_SPS: delay_ms = 63; break; case ADS1118_DR_32_SPS: delay_ms = 32; break; case ADS1118_DR_64_SPS: delay_ms = 16; break; case ADS1118_DR_128_SPS: delay_ms = 8; break; case ADS1118_DR_250_SPS: delay_ms = 4; break; case ADS1118_DR_475_SPS: delay_ms = 3; break; case ADS1118_DR_860_SPS: delay_ms = 2; break; default: delay_ms = 10; break; } HAL_Delay(delay_ms); } uint16_t ads1118_read_raw(uint16_t config) { uint16_t result = 0; ads1118_cs_low(); ads1118_transfer16(config); // 写配置,启动转换 if (config & ADS1118_MODE_SINGLE) { ads1118_wait_conversion(config & 0x00E0); } result = ads1118_transfer16(config); // 发送相同配置,读回转换结果 ads1118_cs_high(); return result; }注意第二次传输时发送的仍然是同样的config,而不是0。原因在前文说过:任何一次16位SPI传输都会被ADS1118当作配置命令写入。如果第二次传0,等于把PGA、数据速率、模式全部清掉,芯片配置就乱了。传相同的config虽然会多触发一次转换,但当前读取到的是上一次转换已经完成的结果,不会出错,这个代价可以接受。
3.3 连续模式与温度传感器读取
连续模式下不需要每次等待转换完成,因为芯片一直在后台转换,读到的始终是最近一次结果。初始化一次配置后,后续读取速度可以很快,适合对采样率有要求的场景。
float ads1118_read_voltage(uint16_t mux, uint16_t pga, uint16_t dr) { uint16_t config = mux | pga | ADS1118_MODE_SINGLE | dr | ADS1118_TS_MODE_ADC; uint16_t raw = ads1118_read_raw(config); int16_t code = (int16_t)raw; float fsr = 4.096f; switch (pga) { case ADS1118_PGA_4_096V: fsr = 4.096f; break; case ADS1118_PGA_2_048V: fsr = 2.048f; break; case ADS1118_PGA_1_024V: fsr = 1.024f; break; case ADS1118_PGA_0_512V: fsr = 0.512f; break; case ADS1118_PGA_0_256V: fsr = 0.256f; break; } return (float)code * fsr / 32768.0f; } float ads1118_read_temperature(void) { uint16_t config = ADS1118_MUX_AIN0_GND | ADS1118_PGA_4_096V | ADS1118_MODE_SINGLE | ADS1118_DR_8_SPS | ADS1118_TS_MODE_TEMP; uint16_t raw = ads1118_read_raw(config); int16_t temp_code = (int16_t)(raw & 0xFFFC); // 低2位无效,清零 return (float)(temp_code >> 2) * 0.03125f; }温度传感器是自检驱动最方便的通道。它的数据格式是14位有符号数,每LSB对应0.03125摄氏度,低两位始终为0。读出来的值在常温下应该是20到40摄氏度之间,如果明显偏离这个范围,基本可以确定SPI时序或字节序有问题。
4. 重新配置量程后到底要等多久
4.1 量程切换为什么不能“立即生效”
这是标题里那个问题:ADS1118如果重新配置量程,需要等待多少时间?很多人在调试时会发现,切换PGA后第一次读到的电压值还是老样子,或者数值明显不对,然后开始怀疑代码写错了。
原因在于ADC内部的前端模拟电路需要稳定时间,而且芯片的转换过程是持续进行的。写入新配置后,正在进行的转换可能仍然基于旧配置,新配置要到下一个转换周期才会完整生效。更稳妥的说法是:修改PGA或DR之后,必须丢弃最新读到的一个结果,从第二次读取开始才是新量程下的有效数据。
所以等待时间的下限是至少一个转换周期。数据手册里的转换周期可以近似为1/DR。
4.2 一张表搞定所有数据率下的等待时间
按转换周期估算,各档数据率下的最短等待时间如下:
| 数据速率 | 转换周期 | 修改PGA后建议等待时间 |
|---|---|---|
| 8 SPS | 125ms | 250ms |
| 16 SPS | 62.5ms | 125ms |
| 32 SPS | 31.25ms | 62.5ms |
| 64 SPS | 15.625ms | 31.25ms |
| 128 SPS | 7.8125ms | 15.625ms |
| 250 SPS | 4ms | 8ms |
| 475 SPS | 2.105ms | 4.2ms |
| 860 SPS | 1.163ms | 2.3ms |
这里“建议等待时间”按两个转换周期计算,也就是保留一个周期的裕量。如果应用对时间要求极苛刻,可以只等一个周期,但第一次读到的数据要主动丢弃。
4.3 代码里的等待实现与优化
前面代码中的ads1118_wait_conversion已经按一个转换周期做了延时。如果应用层切换PGA,正确的调用顺序是这样的:
// 切换到±2.048V量程 ads1118_read_voltage(ADS1118_MUX_AIN0_GND, ADS1118_PGA_2_048V, ADS1118_DR_128_SPS); // 丢弃第一次 float v = ads1118_read_voltage(ADS1118_MUX_AIN0_GND, ADS1118_PGA_2_048V, ADS1118_DR_128_SPS); // 有效值调一次ads1118_read_voltage内部已经包含了写配置加等待,所以连续调用两次,第二次拿到的就是稳定结果。如果连第一次的浪费都不想接受,可以把ads1118_read_raw改成只写配置不读数据,然后延时两个转换周期再读,但代码可读性会差一些。
另一个优化点是用DOUT/DRDY脚做硬件判断。ADS1118的DOUT/DRDY在转换完成时会有电平变化,可以接到MCU的外部中断引脚,转换完成后再发起SPI读取。这样省掉固定延时,同时能获得最大吞吐率。不过这个功能在SPI多从机总线上的表现需要仔细看手册,如果嫌麻烦,固定延时方案在绝大多数项目里都够用了。
5. 调试实录:我踩过的几个坑和排查方法
5.1 SPI模式写成Mode 0,读回全0xFF
新手最容易犯的错误就是把SPI初始化成Mode 0(CPOL=0, CPHA=0)。这会导致主机在错误的时钟边沿采样MISO线上的数据,ADS1118输出的信号在采样点还没稳定下来,读回的自然是一堆0xFF。
排查方法很简单:先初始化SPI为Mode 1,用逻辑分析仪抓SCLK、MOSI、MISO三根线,确认一次CS低周期内MOSI上有16位配置数据,MISO上有16位回读数据。如果没有逻辑分析仪,就反复切换MUX通道,观察读到的值是否跟着变化,如果始终是同一个值,大概率是SPI模式不对。
5.2 电压值抖动严重
如果你的输入信号是稳定的直流电压,读出来的值却在几十个LSB范围内来回跳,先别怀疑芯片。检查一下电源和参考地。ADS1118内部虽然有4.096V基准,但基准电路的稳定性依然依赖AVDD和GND的干净程度。建议在AVDD和GND之间放一个1uF陶瓷电容加一个0.1uF高频电容,位置尽量靠近芯片引脚。
软件层面可以加分均值滤波。以128SPS采样率为例,连续读16次取平均,等效数据率降到8SPS,噪声能显著压低。如果对速度不敏感,直接把数据速率降到32SPS或16SPS,是改善抖动最省事的方法。
5.3 用温度传感器自检驱动
每次写好驱动,我做的第一件事不是接传感器,而是读温度。因为温度传感器不需要外部电路,只要SPI通信正常,读出来的值一定是合理温度。如果温度读数在预期范围内,说明SPI时序、寄存器写入、数据拼接都没问题了,再接外部信号排查干扰才有意义。
这个习惯帮我省了很多时间。有一次硬件上MISO和MOSI接反了,我第一时间去查SPI模式,查了半小时没结果。后来把MUX配置成AIN0对GND,把AIN0引脚直接接地,读到的值依然是乱码,才反应过来是硬件接反。所以调试顺序应该是:先软件自检,再硬件排查。
5.4 多通道采集时配置切换顺序
ADS1118每次读取都要重新发送完整配置,所以多通道循环采样时,要注意“丢弃第一次”的规则。比如你从AIN0切换到AIN1,如果立即读取,拿到的可能还是AIN0的数据。切换通道后,先丢一个结果再开始采集,这个操作表面上看浪费时间,实际能省掉后续大量数据校验工作。
如果项目里有多个模拟通道,建议把每个通道的配置都存成常量,循环采样时复用配置值。不要每次都临时算一遍PGA和MUX的位或结果,这样容易在通道切换时漏掉某个位,导致量程忽大忽小。我见过有人因为宏定义里漏了一个0x1000,所有AIN0单端读数都变成了差分读数,折腾了一天才定位到。
最后再分享一个经验:拿到ADS1118后,第一件事不要急着接复杂电路。先按数据手册搭一个最小系统,用温度传感器自检,再短接AIN0到GND和AVDD各测一次,确认满偏值与0点都对得上,再接入真实传感器。这套流程走下来,驱动层面基本不会再有玄学问题。
本文还有配套的精品资源,点击获取