资讯动态

ADS1118 SPI时序与源码调试:配置字、单次/连续模式与稳定读数

发布时间:2026/10/15 11:56:13 来源:尧图企业网站定制
简介面向51单片机开发者提供使用ADS1118高精度ADC的完整C语言工程示例。代码包含初始化、寄存器配置、单次转换与连续读取等功能配合详细注释便于理解16位数据采集及串口发送电压值的完整流程适合在智能仪表、环境监测等场景中学习或二次开发。压缩包共17个文件大小41KB以.c/.h源文件为核心附带hex烧录文件、uvproj工程配置、obj/lst/m51等编译中间文件及备份从编译、烧录到调试的常用环节均有覆盖。目前已有851人学习下载。实际使用时可依据硬件调整串口波特率与ADS1118配置寄存器并通过PC端串口监视工具查看被测电压数据。该代码可帮助开发者快速掌握单片机与SPI接口ADC的接线、通信及数据解析方法也可作为前端采集模块的参考模板深入理解ADS1118在嵌入式系统中的应用逻辑。1. 16位AD芯片ADS1118代码跑通容易读准很难拿到ADS1118的第一天多数人和我一样先搜源代码、找参考驱动、改引脚、上电看串口。结果往往是两个极端——要么读数一动不动要么数字乱跳。ADS1118是一颗16位四通道ADC带内部基准、可编程增益放大器和温度传感器走SPI接口比同系列I2C版本更适合做多路采集。真正的问题从来不是“能不能跑通”而是跑通之后你手上的源代码到底是在读数据还是在读噪声。这篇文章就围绕ADS1118的配置字、SPI时序和参考驱动写法讲清楚怎么把代码写到能放心用于实际项目的程度。2. ADS1118一次SPI传输两个16位源码先懂时序再动手2.1 ADS1118为什么一次要传32位配置与数据挤在同一帧ADS1118的SPI接口和普通ADC不太一样。普通ADC大多是“主机发命令、从机回数据”一次CS低电平周期内只做一个方向的事。ADS1118为了省引脚把配置寄存器的写入和转换结果的读出放在同一个片选周期里完成主机通过MOSI发出16位配置字同时从MISO读回16位数据。这带来两个直接影响第一写配置字和读数据是同步发生的你没法“只写配置不读数据”第二MISO上读回来的那16位永远是目前已经完成的那次转换的结果不是刚写入的配置对应的新结果。很多参考源代码把这两个概念混在一起导致新手抄完后发现“读到的值像是上一轮的”。从芯片内部结构理解就更清楚ADS1118内部只有一个16位数据寄存器完成一次转换后结果被锁存到这个寄存器里。主机发起SPI传输时移位寄存器里的旧数据从MISO移出同时MOSI上的配置字被移入配置寄存器。也就是说一次CS拉低期间你既拿回了旧数据也写入了新命令二者互不等待。所以写源码前先建立一个正确的时序模型先CS拉低发配置字并顺手收一个“旧结果”CS拉高等转换完成再CS拉低发任意16位通常发0把“新结果”移出来CS拉高。两次独立的片选周期才完成一次完整读数。2.2 先封装读写函数把SPI收发变成两个清晰动作不管用哪个MCU平台我一般会先封装两个底层函数一个负责“写配置字并读旧值”另一个负责“只读新值”。这样上层流程读起来清楚后面改采样率、改通道、改PGA都不会动到主逻辑。// 底层SPI收发发送16位同时接收16位 // 具体实现取决于MCU的SPI外设这里用抽象函数代替 uint16_t spi_read_write_16(uint16_t tx_data); // 片选控制 void cs_low(void); void cs_high(void); // 写配置字返回同一帧里MISO移出的旧转换结果 uint16_t ads1118_write_cfg(uint16_t cfg) { uint16_t old_data; cs_low(); old_data spi_read_write_16(cfg); // MOSI发配置字MISO收旧结果 cs_high(); return old_data; } // 只读最新结果MOSI发0x0000等待MISO把数据寄存器内容移出 uint16_t ads1118_read_result(void) { uint16_t result; cs_low(); result spi_read_write_16(0x0000); // 任意16位都行常用0 cs_high(); return result; }这两个函数是整个驱动的地基。ads1118_write_cfg返回的那个old_data在首次上电后是无效的必须丢弃ads1118_read_result读到的才是当前配置下完成的转换结果。参数上需要注意两点SPI时钟频率不要超过芯片上限常见参考驱动设置在2MHz到4MHz之间保守一点用1MHz最稳数据位宽必须是16位很多MCU的SPI外设默认8位不改位宽就全乱套。另外CS必须由软件手动控制不能用硬件自动片选因为两次片选周期之间的间隔决定了转换等待时间。2.3 硬件SPI不灵时的备选GPIO模拟SPI的完整写法有些项目里ADS1118挂在低速总线上或者硬件SPI引脚被占用这时GPIO模拟SPI反而更可控。模拟SPI的难点在于SCLK的极性和相位ADS1118要求SCLK空闲时为高电平也就是CPOL1。下面这段代码按“空闲高电平、先发数据再产生时钟边沿”的顺序写实际使用前建议用逻辑分析仪核对一次波形。// GPIO模拟SPI读写的核心函数 // 先输出数据位再翻转SCLK主从设备在边沿采样 uint16_t spi_read_write_16_gpio(uint16_t tx_data) { uint16_t rx_data 0; int i; for (i 15; i 0; i--) { // 先准备好MOSI电平 if (tx_data (1 i)) mosi_high(); else mosi_low(); // SCLK拉低形成下降沿 sclk_low(); // 在SCLK低电平期间读取MISO if (miso_read()) rx_data | (1 i); // SCLK拉高形成上升沿 sclk_high(); } return rx_data; }这段代码不是唯一正确的写法SCLK先拉高还是先拉低、MISO在哪个边沿采样都要和你手上的数据手册时序图对齐。我的经验是模拟SPI出问题十有八九不是逻辑错而是边沿采样点选在了数据线上电平还没稳定的时刻。所以代码里特意把MISO读取放在SCLK拉低之后、拉高之前给数据线留出稳定窗口。使用GPIO模拟时SCLK翻转速度受GPIO操作延时限制通常几百kHz对ADS1118最高860SPS的采样率来说完全够用。注意GPIO模拟不要开中断否则SCLK中间被中断打断时序会多出毛刺读数容易偶发跳变。3. ADS1118配置字逐位拆解16个bit决定代码对错3.1 配置字字段表MUX、PGA、DR、TS怎么排布ADS1118的配置寄存器一共16位每一位的作用和位置必须清楚。网上能找到的参考源代码很多但每个版本对字段的移位方式可能不一样有的用位域结构体有的用宏定义有的直接写死十六进制。不懂字段排布改一个通道就可能把整个配置字破坏掉。位序号字段名作用说明15SS写1启动单次转换读回1表示正在转换14-12MUX[2:0]输入通道选择000表示AIN0-AIN1差分001表示AIN0-AIN3差分100表示AIN0对GND101表示AIN1对GND110表示AIN2对GND111表示AIN3对GND11-9PGA[2:0]增益配置决定满量程范围8MODE1为单次模式0为连续模式7-5DR[2:0]数据速率选择000为8SPS001为16SPS010为32SPS011为64SPS100为128SPS101为250SPS110为475SPS111为860SPS4TS1表示读取内部温度传感器3-0PULL_UP_EN及保留位多数参考驱动写0PGA字段和满量程的对应关系是另一个容易记混的地方PGA[2:0]满量程范围000±6.144V001±4.096V010±2.048V011±1.024V100±0.512V101±0.256V注意这些是满量程范围不是分辨率。16位分辨率分布在正负满量程之间所以计算电压时要用满量程除以32768得到每个LSB对应的电压。例如PGA设为±4.096V时LSB约为0.125mVPGA设为±0.256V时LSB约为0.0078mV。PGA越大LSB越小但能测量的输入范围也越窄。3.2 单次与连续模式两种读序列在源码里的差异单次模式是最容易理解也最常用的模式。每次读数据前先写一次配置字SS位置1启动转换然后等待转换完成再单独读结果。这个模式适合低频传感器采集比如温度、气压、电位器电压功耗也低。// 单次模式读取一次电压 uint16_t ads1118_read_single(uint16_t mux, uint16_t pga, uint16_t dr) { uint16_t cfg 0; cfg | (1 15); // SS1启动转换 cfg | (mux 12); // 通道选择 cfg | (pga 9); // 增益配置 cfg | (1 8); // MODE1单次模式 cfg | (dr 5); // 数据速率 cfg | (0 4); // TS0不使用温度传感器 // 丢弃写配置时回读的旧数据 ads1118_write_cfg(cfg); // 等待转换完成等待时间至少为 1/采样率再加一点余量 delay_ms(1000 / dr_value 10); // 读新结果 return ads1118_read_result(); }这里的等待时间按数据速率计算。例如DR设为64SPS转换周期约15.6ms等待时间就取25ms左右DR设为8SPS转换周期125ms等待就要135ms以上。等得太短会读到上一次的旧结果等得太长浪费CPU实际项目里可以根据转换周期动态计算延时。连续模式则完全不同配置字只写一次之后不再写配置MCU按固定周期直接读结果。注意连续模式下如果再次调用写配置函数相当于触发SS位启动一次新转换会把原有的连续转换节奏打断数据流中间出现一个异常点。我见过不少人把单次的读函数直接套到连续模式里结果数据偶尔跳变排查半天才发现是多写了一次配置。// 连续模式只初始化一次后面循环读 uint16_t cfg 0; cfg | (0 15); // 连续模式下SS位写0 cfg | (mux 12); cfg | (pga 9); cfg | (0 8); // MODE0连续模式 cfg | (dr 5); ads1118_write_cfg(cfg); // 之后不要再调用这个函数 // 主循环里直接读读到的总是上一次已完成的结果 while (1) { int16_t raw ads1118_read_result(); // 根据采样周期决定读的频率不要比转换完成更快 }3.3 用同一套源码读内部温度传感器ADS1118内部有温度传感器很多参考驱动直接把它当作额外通道用。读温度的关键是MUX和TS两个字段的配合TS置1后内部温度传感器的输出被接入测量链路MUX字段实际上被忽略但多数参考驱动习惯把MUX写成111。温度模式的配置字构造如下uint16_t cfg 0; cfg | (1 15); // SS1启动转换 cfg | (7 12); // MUX111多数参考驱动这样写 cfg | (2 9); // PGA±4.096V cfg | (1 8); // 单次模式 cfg | (0 5); // 8SPS cfg | (1 4); // TS1使能温度传感器 ads1118_write_cfg(cfg); delay_ms(150); // 8SPS对应125ms周期留余量 int16_t temp_raw ads1118_read_result();温度原始码换算成摄氏度的公式各个参考驱动存在对齐差异有的直接拿temp_raw * 0.125有的先temp_raw 2再乘0.125原因是温度数据在16位结果里的对齐方式不同。我的建议是不要纠结哪份代码“看起来对”拿到驱动后用室温环境对比水银温度计做一次标定读回来的数值和实际室温一致就说明公式对了。这个坑在后面还会专门展开。4. 把ADS1118源代码跑出第一个正确读数上电、首读、验证4.1 上电初始化顺序先配SPI再碰ADS1118很多人拿到ADS1118就直接调驱动结果读出来的数据要么全0要么全FFFF。我的习惯是先检查上电时序ADS1118的电源和SPI外设谁先准备好代码里必须有明确顺序。先初始化MCU的SPI时钟和引脚再给ADS1118供电最后延时一小段时间让芯片内部上电复位完成然后才开始第一次通信。void ads1118_init(void) { // 1. 初始化SPI引脚和时钟CPOL1CPHA按数据手册时序图确认 spi_init(1000000); // 1MHz时钟 spi_set_format(16, 1, 1); // 16位数据CPOL1CPHA按实际调整 // 2. 初始化片选引脚默认拉高 gpio_init(CS_PIN); gpio_set_dir(CS_PIN, GPIO_OUT); gpio_put(CS_PIN, 1); // 3. 给芯片上电后等待稳定 power_ads1118_on(); delay_ms(100); // 4. 首次通信前先写一个空配置让芯片从复位状态进入正常工作 uint16_t cfg 0x8000; // 只置SS位其余全0 ads1118_write_cfg(cfg); delay_ms(10); }这段初始化里最容易忽视的是第4步。ADS1118上电后内部状态不确定直接按目标配置读取第一个返回值往往不可靠。先发一个无关紧要的配置字把SPI链路打通后面再发正式配置就稳了。另外SPI格式里的CPHA位不同平台的寄存器定义不一样不要照抄网上代码一定以你实际读到的波形为准。4.2 第一次读数的“哑炮”丢弃首个结果才算跑通我第一次调ADS1118时遇到过这样的现象配置字写进去了延时也给了读出来的数据却是0。后来发现不是电路问题而是参考驱动里的读函数本身就隐含了“读一次旧数据、再读一次新数据”的流程我没理解就直接照搬把第一次读到的旧值当成了有效结果。正确流程是写配置后第一个从MISO读出的16位数据属于上一轮转换直接丢弃等待转换完成后再一次发起读操作拿到的才是新结果。如果是连续模式第一次读也是无效的最好连读两次、只取第二次。// 完整的一次单次模式读取流程 int16_t ads1118_read_once(uint16_t channel_cfg) { // 写配置 ads1118_write_cfg(channel_cfg); // 等待转换完成 delay_ms(ads1118_conv_ms(channel_cfg)); // 丢弃第一次读到的旧值 ads1118_read_result(); // 真正拿数据 return (int16_t)ads1118_read_result(); }为什么丢弃第一次读到的值之后还要再读一次因为ADS1118的数据寄存器在转换完成后被更新但你发起第一次读操作时移出的是当前寄存器里的旧内容只有再次发起读操作才会把更新后的内容移出来。这个“双读”逻辑是ADS1118驱动里最常见的写法也是新手最容易漏掉的。4.3 用电压与温度两道校验确认源码可用写完整套驱动后不要直接接传感器先做两道校验。第一道是电压校验把AIN0对GND短路理想读数为0再给AIN0接一个精密基准电压读数应该在±2个LSB以内。第二道是温度校验读内部温度传感器和室温对比偏差在±2摄氏度以内算正常。// 电压校验示例PGA±4.096VAIN0对GND uint16_t cfg (1 15) | (4 12) | (1 9) | (1 8) | (0 5); int16_t raw ads1118_read_once(cfg); float voltage raw * 4.096f / 32768.0f; printf(raw%d, voltage%.4fV\n, raw, voltage);这里把PGA设为±4.096VLSB约0.125mV短接时读到的raw应该在0附近小幅波动。如果读到的是满量程值比如4095或-4096说明输入接反、通道选错或者PGA范围不够。电压校验通过后再读温度传感器两道都过了这个源码才算真正适配你的板子。5. ADS1118源码调试避坑5个高频翻车现场5.1 单次模式首读永远是0或旧值现象按参考代码写好后第一次读到的数据是0第二次读才正常而且每次重新上电都是这个规律。原因ADS1118的MISO在写配置那一帧返回的是上一次转换结果上电后从未完成过转换所以是无效旧值。第一次读数据操作移出的又是数据寄存器里刚写入的旧内容。解决在正式读取流程前增加一次“预热读”也就是写一个空配置后连续读两次直接丢弃结果第三次开始的数据才是有效的。这个步骤不需要额外硬件纯软件就能规避。5.2 内部温度读数跳得离谱现象读温度传感器数值在十几摄氏度到一百多摄氏度之间乱跳完全没法用。原因一是TS位没有置1读的还是模拟输入通道的悬空噪声二是温度原始码是有符号数某些参考驱动把它当无符号数直接乘系数数值就会漂到离谱。另外温度模式下MUX字段的处理各版本不一致也会导致读到的通道不对。解决确认配置字里TS1MUX字段至少置成111温度原始码必须按有符号数处理用int16_t类型接收如果数据仍然跳动连续读4次去掉最大最小、取中间两次均值。温度传感器精确度本来就有限不要指望它当电子温度计用。5.3 抄网上的SPI模式配置波形对不上现象同一个参考驱动在A同学的开发板上读数据正常换到我的板子上读全0或者全FFFF。原因ADS1118的SCLK空闲电平是高还是低数据手册有明确时序图但不同MCU的SPI外设对CPOL、CPHA的定义和寄存器映射不一样。网上源码标注的“模式1”或者“模式3”到别的平台上未必对应同一个物理波形。解决不要盲信注释用逻辑分析仪抓SCLK和MOSI确认空闲电平、数据建立时间、采样边沿这三个参数。ADS1118要求SCLK空闲为高电平这点先满足再对照数据手册调整具体边沿。没有逻辑分析仪时可以连续读同一通道同一稳定电压如果数值不随SPI配置变化而稳定说明时序还是错的。5.4 换PGA档位后读数对不上现象同一个输入电压PGA设为±6.144V时读数正常改成±0.256V时读数反而变小甚至变成反码。原因PGA档位改小后满量程范围变窄LSB变小同样的输入电压对应的数字码变大才对读数变小说明配置字里的PGA字段根本没生效。更多时候是配置字构造错误比如PGA字段的移位量算错或者配置字被后续代码覆盖。解决把配置寄存器里的PGA字段单独提取出来验证。写一个只读配置寄存器内容的测试函数打印十六进制和期望值逐位比对。另外输入电压超过当前PGA满量程时ADC输出会饱和到满码所有bit变成1这也是“换了档位读数反而变大”的另一个原因。5.5 连续模式里重复写配置导致数据断层现象连续模式下数据流整体稳定但每隔一小段就出现一个明显偏离的大跳变像周期性毛刺。原因代码里每次进入循环都调用了一次写配置函数等于在连续模式下反复触发SS位。ADS1118收到SS启动信号会重启转换正在进行的转换被打断那次读到的数据就是无效的。解决连续模式下配置字只写入一次初始化后进入纯读状态。采样率或通道要改时先停掉连续模式改完配置再重新启动。如果项目里必须动态切通道建议直接用单次模式每次读取前重新配置通道逻辑更简单也不容易出错。6. 把ADS1118源代码做成中断驱动DOUT/RDY引脚与滑动平均轮询方式读ADS1118在小系统里够用但几百SPS的采样下CPU大部分时间耗在读状态、等延时上。ADS1118的DOUT/RDY引脚在转换完成时会拉低可以接到MCU的外部中断引脚转换完成自动通知MCU读取时序更精准也省去盲等延时。初始化时把DOUT/RDY配置成下降沿触发的外部中断然后在中断回调里发起读操作// 假设已配置为连续模式 // DOUT/RDY引脚下降沿触发此函数 void ads1118_rdy_isr(void) { // 中断里只读结果不写配置 int16_t raw ads1118_read_result(); // 将raw存入ring buffer主循环处理 ring_buffer_push(raw); }注意中断回调里不能做浮点转换和打印只做轻量操作原始数据先入缓冲区主循环再计算电压或滤波。数据到手后我习惯再加一层滑动平均消掉电源纹波和传感器自噪声。N取4到16之间N越大越平滑但响应变慢。ADC本身的量化噪声是白噪声性质滑动平均能有效压低方差。#define FILTER_N 8 static int32_t sum 0; static int16_t buf[FILTER_N]; static uint8_t idx 0; int16_t ads1118_moving_average(int16_t new_sample) { sum - buf[idx]; buf[idx] new_sample; sum new_sample; idx (idx 1) (FILTER_N - 1); return (int16_t)(sum / FILTER_N); }这段代码要求FILTER_N是2的幂 (FILTER_N - 1)做取模运算避免除法开销。ADC采样的稳定值适合用滑动平均但突变信号不要过度滤波否则会抹掉边沿。我早期在连续模式上吃过亏误以为主循环每次读之前都要重新写一次配置导致数据周期断层后来把配置初始化挪到循环外就好了。ADS1118的源代码不难写难的是理解它“写配置时读旧值、读数据时不写配置”的时序哲学。把这层关系搞清楚剩下的寄存器配置、PGA选择、中断读取都是水到渠成的事。希望这篇文章能帮你少走几趟弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑