简介:这套资源是一份基于STM32的24位高精度ADC采集与OLED显示完整工程,面向嵌入式开发者,尤其适合需要精密模拟信号测量和数据可视化的场景。包内共包含两百四十二个文件,以C语言源文件、头文件、编译中间文件及Keil工程配置为主,并附有可烧录的hex与axf文件,压缩包整体约十点八二兆字节,可直接在Keil环境下打开查看或重新编译。项目将AD7173这款24位Σ-Δ型模拟数字转换器与STM32单片机结合,通过SPI等接口完成寄存器配置与连续采样,再由OLED屏幕实时刷新显示转换结果,覆盖了ADC驱动初始化、数据滤波处理、显示驱动等嵌入式开发关键环节。已有七百五十五人学习下载,适合作为高精度采集系统开发、STM32外设驱动学习及OLED显示集成的参考资料,也可为环境监测、电力质量分析等应用提供直接可复用的设计思路。
1. 24位ADC不是玄学:AD7173 + OLED 这套资源到底解决什么
第一次用 24 位 ADC 的时候,我以为换上 AD7173 就能把温度、称重、压力信号测到微伏级,结果焊完板子读数满屏乱跳,折腾了两天才发现是基准电压的地线走错了。这套 AD7173 + OLED 的 STM32 资源包,解决的就是从硬件接线、SPI 寄存器配置到 OLED 实时显示这一整条链路:AD7173 负责把模拟量变成 24 位码值,STM32 通过 SPI 把码值读回来,OLED 把电压、温度或原始码直接显示出来。适合正在做称重仪表、热电偶采集、压力变送器或者实验室仪表的工程师,新手照着接线和 demo 能跑出第一屏数字,熟手可以拿它当寄存器时序的参考框架,少走几段冤枉路。
2. AD7173 接线与电源规划:先让模拟地和数字地别打架
2.1 引脚接线与选型理由
AD7173 是 ADI 的 24 位 Σ-Δ ADC,最大输出速率能到 31.25kSPS,内部集成 PGA(增益 1 到 128)、输入缓冲和数字滤波器。相比 STM32 内置 12 位 ADC,它最值钱的地方不是位数高,而是低噪声加上可编程增益——微弱信号不用先放大就能直接进 ADC,省掉一级仪表放大器。和 ADS1256 比,AD7173 的输入通道配置更灵活,差分和单端混用不用额外切 MUX,做多通道采集少操很多心。
下面是我在用这套资源时的最小接线,SPI 全部接在 STM32 的 SPI1,方便直接套包里的例程:
| 信号 | 接到 STM32 引脚 | 说明 |
|---|---|---|
| SCLK | PA5 / SPI1_SCK | SPI 时钟,软件里先按 1MHz 起步 |
| DIN | PA7 / SPI1_MOSI | 主机输出从机输入 |
| DOUT/RDY | PA6 / SPI1_MISO | 数据输出,同时兼数据就绪指示 |
| CS | PA4 | 软件控制片选,不用硬件 NSS |
| REF+ | 2.5V 基准源 | 直接决定满量程,别用 STM32 的 3.3V 凑合 |
| AIN+ / AIN- | 传感器差分输出 | 没接传感器前,先短接 AGND 测基线 |
AVDD 和 DVDD 我习惯分开供,模拟电源先用一块 LDO 单独出,DVDD 可以和 STM32 共用 3.3V,中间串一个磁珠或 10Ω 电阻隔离。模拟地和数字地必须单点连接,ADC 底下铺完整地平面,这条线没做好,后面滤波和校准都会很吃力。
2.2 SPI 时序与复位序列:先让 DOUT/RDY 拉低
AD7173 的 SPI 访问方式比较特殊:每次读或写寄存器之前,都要先通过通信寄存器把目标寄存器地址传进去。通信寄存器是 8 位,bit7 表示读还是写,bit5 到 bit0 是寄存器地址。SPI 模式上,数据手册支持 Mode 1(CPOL=0,CPHA=1)和 Mode 3(CPOL=1,CPHA=1),我一般先按 Mode 1 配,读不回 ID 再切 Mode 3。
芯片上电后第一件事是软件复位,让 DOUT/RDY 拉低表示芯片已经就绪。复位序列是 CS 拉低后连续灌 64 个以上高电平时钟,常见写法是发 8 个字节的 0xFF:
// 软件复位:通信寄存器位流全部置 1,至少 64 个 SCLK uint8_t reset_buf[8]; memset(reset_buf, 0xFF, sizeof(reset_buf)); HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, reset_buf, sizeof(reset_buf), HAL_MAX_DELAY); HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_SET); HAL_Delay(10);这段代码的逻辑是:寄存器访问前 CS 必须拉低,传输完再拉高,中间不允许被中断打断。8 个 0xFF 一共 64 个时钟,满足手册要求的最少脉冲数。复位后第 10ms 再开始后续配置,给内部上电稳定留时间。这里是踩坑高发区,有人只发 4 个字节,结果芯片偶尔复位偶尔不复位,后面读 ID 全是 0xFFFF。
复位之后先读 ID 寄存器确认通信链路通不通。ID 寄存器地址是 0x30,AD7173-8 正常返回 0x9030:
uint16_t ad7173_read_reg(uint8_t addr) { uint8_t cmd = (1 << 7) | (addr & 0x3F); // bit7 = 1 表示读 uint8_t buf[2] = {0, 0}; HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, 2, HAL_MAX_DELAY); HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_SET); return (buf[0] << 8) | buf[1]; } uint16_t id = ad7173_read_reg(0x30); // 正常情况 id == 0x9030,否则查供电、SPI 模式和接线命令字节只取地址低 6 位是为了避开通信用寄存器里其它标志位,读出来的两个字节是高字节在前。如果读回 0xFFFF,先查 CS 和 MISO 是不是接反;读回 0x0000,多半是 SPI 模式不对或者复位没成功。ID 能读对,后面配寄存器才不是白忙活。
2.3 电源与基准的细节才是稳定性的关键
我把这块单独拎出来说,因为 24 位 ADC 对基准和电源噪声的敏感程度超乎想象。REF+ 和 REF- 两端各放一个 0.1μF 陶瓷电容并联 10μF 钽电容,靠近 REF 引脚放置。基准芯片选 2.5V 的精密基准,比如 ADR421 或 REF195,温漂指标直接决定整个系统的温漂水平。如果板子空间有限,用 AD7173 内部基准也能跑,写 ADC 模式寄存器时把内部基准使能位置 1 就行,但绝对精度会输给外部基准。
PGA 增益调高以后,输入端的噪声和偏置电流都会被放大,这是很多人把增益设为 128 后读数乱跳的真正原因。开启输入缓冲可以降低对传感器源阻抗的要求,但缓冲本身会引入微小的偏置电压。这个偏置不是坏的,后面用内部零点校准就能消掉,别在硬件上盲目调电阻。
3. 寄存器配置与数据读取:从 SPI 时序到第一片 24 位数据
3.1 三组关键寄存器:模式、通道配置、滤波
AD7173 的寄存器是 16 位宽,每次访问都要先写通信寄存器再传数据。这套资源包里驱动代码已经封装好了读写函数,但你要听懂它在配什么,否则改通道、改增益时还是会翻车。最常用的寄存器就这几个:
| 寄存器地址 | 名称 | 作用 |
|---|---|---|
| 0x01 | ADC 模式寄存器 | 连续转换/单次转换、内部基准开关 |
| 0x02 | 接口模式寄存器 | 4 线/3 线 SPI、CRC 开关 |
| 0x09 | 配置寄存器 | 输入通道选择、PGA 增益、双极性/单极性 |
| 0x0A | 滤波寄存器 | sinc3/sinc4、增强滤波、输出速率 |
| 0x04 | 数据寄存器 | 24 位转换结果,读三个字节 |
我按默认双极性、AIN0 单端输入、增益 1、sinc3 滤波来初始化,这套组合最容易跑通:
ad7173_write_reg(0x01, 0x0800); // 连续转换模式 + 使能内部基准 ad7173_write_reg(0x02, 0x0000); // 4 线 SPI,不启用 CRC ad7173_write_reg(0x09, 0x0300); // AIN0 单端输入,增益 1,双极性 ad7173_write_reg(0x0A, 0x0000); // sinc3 滤波器,默认输出速率这里想强调两点。第一,0x01 的 0x0800 是“连续转换模式 + 内部基准使能”的常见组合值,实际硬件上如果没有外部基准,这个值能让芯片先跑起来;但对外部基准的板子,要把内部基准关掉才能读到准确的外部 REF 电压。第二,0x09 这个通道配置寄存器的值不是万能的,换通道、换差分接法都要按数据手册寄存器位重新算,我见过有人把差分输入当单端配,读回来的值永远在正满量程附近,就是这个位域搞错了。
写寄存器封装也很简单,通信寄存器选地址,然后按高字节在前传两个字节:
void ad7173_write_reg(uint8_t addr, uint16_t data) { uint8_t buf[3]; buf[0] = addr & 0x3F; // bit7 = 0 表示写 buf[1] = (data >> 8) & 0xFF; // 高字节先传 buf[2] = data & 0xFF; HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, buf, 3, HAL_MAX_DELAY); HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_SET); }参数层面要注意:addr 只取低 6 位,bit7 归读写标志管;16 位数据高字节必须在前,反了写进去的寄存器值整个错位。还有个习惯值得培养——配置寄存器写完后马上回读一遍,和写入值比对。AD7173 支持寄存器回读,这一步能筛掉一多半 SPI 时序问题,不用怀疑芯片坏了。
3.2 从原始码到电压:24 位符号扩展与量程确认
数据寄存器读出来是 3 字节 24 位,但在 C 语言里 24 位没有原生类型,要自己拼成 32 位再符号扩展。AD7173 在双极性模式下输出二进制补码,满量程范围和基准电压、PGA 增益都有关系。转换关系是:电压 = 原始码 × Vref /(2^23 × PGA)。如果配的是单极性模式,分母变成 2^24,这个别搞混。
typedef struct { uint8_t bytes[3]; } adc_raw_t; float adc_raw_to_voltage(uint32_t raw) { int32_t signed_raw = (int32_t)(raw << 8) >> 8; // 24 位补码符号扩展 float vref = 2.5f; // 按实际基准电压填 float pga = 1.0f; // 与配置寄存器增益位一致 return (float)signed_raw * vref / (8388608.0f * pga); }符号扩展这行是关键:先左移 8 位把符号位移到 int32_t 的最高位,再算术右移 8 位,C 语言对有符号整数右移是算术右移,高位补符号位,这样负数就不会算错。8388608 就是 2^23,双极性下的量程系数。读出 0x7FFFFF 代表接近正满量程,0x800000 代表接近负满量程。
上电后建议先做一次“短接基线测试”:把 AIN+ 和 AIN- 直接短接,读数应该在 0 附近小幅跳动。如果短接后读数偏了几十个微伏,说明有失调,后面做内部校准消掉即可。
3.3 主循环怎么读数据:RDY 下降沿触发
AD7173 的 DOUT/RDY 引脚在数据准备好后会拉低,这是最可靠的取数时机。把 DOUT/RDY 接到 STM32 的一个 EXTI 引脚,下降沿触发,中断里只置标志位,主循环里再读数据。这样不会因为 SPI 读取耗时把中断拖死。
volatile uint8_t adc_data_ready = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == ADC_RDY_Pin) { adc_data_ready = 1; } } uint32_t raw = 0; if (adc_data_ready) { raw = ad7173_read_data(); adc_data_ready = 0; float voltage = adc_raw_to_voltage(raw); // 数据交给上层显示或存储 }ad7173_read_data 内部先写通信寄存器选择 0x04 地址,再连续读 3 个字节拼接成 uint32_t。这里有个细节:读数据寄存器的命令字节通常只在第一次访问时写,有的工程师每次读都写一遍,也能用,但会多花一个 SPI 事务的时间。RDY 下降沿回调里千万别做 SPI 操作,只改标志位,不然中断嵌套会让 SPI 帧碎掉。
如果后面要切多通道,AD7173 内置通道序列器,配置寄存器里设好通道选择,ADC 模式寄存器里使能序列扫描,它会自动按顺序转换,比 STM32 内部 ADC 手动切换通道再等采样的方式稳得多,采样和切换都在芯片内部完成,不会丢点。
4. OLED 显示与刷新策略:别让屏幕拖垮采样率
4.1 SSD1306 与 I2C/SPI 接口选型
资源包里带的 OLED 驱动是 SSD1306,0.96 寸最常见,I2C 和 SPI 版本都有。I2C 接线少,SCL/SDA 两根线就能跑,但整屏刷新要传 128×64 位共 1KB 数据,I2C 400kHz 模式下一帧大概要 20ms 以上,频繁刷新会严重挤占 SPI 总线。HAL 库驱动 OLED 的代码写法大同小异,核心就是初始化序列加显存刷新,包里把这两个功能封装成了 ssd1306_init 和 ssd1306_update,直接调用就行。
实际项目里我更推荐 SPI 接口的 OLED,刷新速度比 I2C 快好几倍,而且和 ADC 共用 SPI1 时还能分时复用。有个常见坑是 0.9 寸的 OLED 和 0.96 寸不兼容,前者 I2C 地址可能是 0x3C,后者可能是 0x3D,模块背面地址焊盘默认状态不一样,换屏后第一件事就是重新确认 I2C 地址。
4.2 显存缓冲与低频刷新:显示和采集各跑各的
OLED 显示不能直接在中断或者 ADC 读取的紧耦合路径里做。我的做法是显示缓冲独立成一块内存,ADC 每读一次就更新缓冲里的数值字符串,主循环每 200ms 才把整个缓冲刷到屏幕。这样人眼看起来刷新很连续,SPI 总线也不会被 OLED 占满,ADC 实时性基本不受影响。
static oled_frame_t frame; static uint32_t last_ui_tick = 0; while (1) { if (adc_data_ready) { // ADC 读取和电压换算,不碰 OLED raw = ad7173_read_data(); voltage = adc_raw_to_voltage(raw); adc_data_ready = 0; } if (HAL_GetTick() - last_ui_tick >= 200) { char line_buf[16]; sprintf(line_buf, "%.4fV", (double)voltage); ssd1306_draw_string(&frame, 0, 0, line_buf); ssd1306_update(&frame); last_ui_tick = HAL_GetTick(); } }200ms 这个刷新间隔是实际体验和性能的折中:小于 100ms 时 SPI 总线负担明显加重,ADC 偶发丢数据;大于 500ms 时数字变化看着卡顿。sprintf 只用来格式化一行数字,不会有性能问题。特别注意 OLED 刷新期间不要去读 ADC,否则读回来的码值偶尔会跳几个 LSB,这种“偶发性误差”最耗时间排查,本质就是刷屏抢了总线。数据想高保真记录,还是走串口或 SD 卡,OLED 只当人机界面用。
4.3 显示汉字与 float 格式化的取舍
OLED 显示汉字需要先在取模软件里生成字模数据,再在代码里按字形索引查表显示。这里有个隐藏坑:STM32 工程里源文件编码如果是 GBK,字符串里直接写中文没问题,但如果你用 VSCode 编辑,文件默认 UTF-8,两者混用就会出现汉字显示错乱。所以要么统一 GBK,要么统一 UTF-8 并在 OLED 驱动里做 UTF-8 到字模的映射,资源包里已经按 GBK 字库处理好了,你替换字模时注意别混编码就行。
float 格式化也有取舍。24 位 ADC 的电压分辨率能做到微伏级,OLED 上显示 4 位小数已经够人眼观察,但 4 位小数背后其实丢了很多信息。我一般 OLED 上显示“电压 mV”配 3 位小数,串口调试里才打印完整 6 位有效数字。sprintf 的浮点格式化比较费资源,主循环里每 200ms 调一次没问题,千万别放在高频率的 ADC 中断里。
5. 避坑指南:SPI 卡死、零点漂移和 OLED 白屏的排查记录
5.1 SPI 偶发卡死:DOUT/RDY 一直高
现象:程序跑一段时间后 adc_data_ready 不再置位,DOUT/RDY 引脚一直保持高电平,ADC 完全不响应。
原因:SPI 通信被 OLED 刷新或者其他中断打断,位序对不上,芯片进入未定义状态。另一种可能是 SPI 时钟超过手册上限,AD7173 的 SPI 时钟并不是所有电源电压下都能跑满 10MHz,低压供电时超频会随机丢帧。
解决:先降低 SPI 时钟到 1MHz 试稳定性,再在每次读数据超时后执行一次软件复位序列。复位序列就是 2.2 节那 8 个 0xFF,这条“后悔药”帮我修过三次类似问题,简单粗暴但有效。
5.2 读回来全是 0xFFFFFF 或 0x000000
现象:无论输入端接什么信号,读数都是同一个固定值,偶尔还会在满量程和零之间来回跳。
原因:通道配置寄存器的输入选择位写错,或者差分/单端模式配置反了。还有一种常见情况是 SPI 模式不对,数据手册虽然写了支持 Mode 1 和 Mode 3,但每颗芯片的异步复位行为有差异,有的板子在 Mode 1 下能读 ID,配置寄存器却写不进去。
解决:先回读 ID 和配置寄存器,逐位核对。读 ID 返回 0x9030 后,再把 0x09 寄存器读回来对比写入值。这里和 STM32 读 TFT 屏遇到“ILI9341 读 ID 是 0xA1A1”是同一个道理,多半不是芯片坏了,而是片选时序或 SPI 模式没对上,换一个模式重新读。
5.3 零点漂移几十微伏,短接输入不为零
现象:AIN+ 和 AIN- 短接后,读数不是 0,而是稳定偏移几十微伏,或在一个范围内漂动。
原因:基准噪声、PGA 增益放大、PCB 表面漏电、地平面回流都会贡献漂移。把数据连算两天你会发现漂移方向往往和室温变化相关,基准温度系数才是主因。
解决:滤波寄存器从 sinc3 切到 sinc4,采样率降到 16kSPS 以下;开启输入缓冲;做完这些再做一次内部零点校准。模拟地和数字地必须单点连接,如果这两块地在 PCB 上大面积相连,零点漂移通常很难压下去。
5.4 OLED 白屏或花屏
现象:刚上电 OLED 白屏,按一下复位键又正常显示,或者显示内容缺行。
原因:I2C 地址不对是第一位原因,0.3D 写成 0x3C 这类问题最常见。还有 OLED 的复位引脚没有做上电复位时序,主控比屏先跑起来,初始化序列发过去时屏还没准备好。0.9 寸和 0.96 寸 OLED 的 I2C 兼容性问题也常在这里冒出来,换屏后没重新确认地址和上拉电阻。
解决:先写一段 I2C 地址扫描代码,确认实际地址再初始化。复位引脚上电后拉低至少 10ms 再拉高,确保屏先就绪。总线上加 4.7kΩ 上拉电阻,SCL/SDA 接反时 OLED 会一直不响应,先用万用表量一下引脚电平确认硬件。
5.5 输入悬空时读数漂到几百微伏
现象:输入端悬空时数据漂移严重,接上固定电压源后抖动依旧很大。
原因:悬空时噪声耦合本来就不小;传感器源阻抗过高和输入缓冲不匹配;基准源纹波大也会直接体现在转换结果上。
解决:悬空漂移是正常的,不要拿这个当故障指标。正确做法是把输入短接到 AGND 测噪声基线,基线抖动超过 100μV 就要查基准和电源。源阻抗高于 10kΩ 时开启输入缓冲,但要注意缓冲带来的偏置,用内部校准抵消。我每次上电后先记录基线偏移量,再在测量结果里做软件扣除,比改硬件快得多。
6. 进阶用法:开增强滤波和 CRC 校验,把有效位数再抠几个 bit
6.1 增强 sinc4 滤波与 ODR 设置
想把 24 位 ADC 的有效分辨率榨干,最常见的手段是增强滤波模式加降采样率。滤波寄存器 0x0A 的 bit15 到 bit13 就是滤波器模式选择位,普通 sinc3 换成增强 sinc4 后,带外噪声衰减更陡,RMS 噪声会明显下降。代价是建立时间变长,输入信号突变后需要多个转换周期才能稳定,测缓变信号划算,测快速跳变会吃亏。
uint16_t filter_reg = ad7173_read_reg(0x0A); filter_reg &= ~0xE000; // 清掉 bit15-13 的滤波器模式 filter_reg |= 0x2000; // 增强 sinc4 模式,具体值以手册为准 ad7173_write_reg(0x0A, filter_reg);这段代码先读回原值再改位,避免把输出速率设置覆盖掉。0x2000 这个值对应的滤波器模式要对着手册确认一遍,因为 AD7173 和 AD7172 在寄存器位定义上略有差异,直接照搬会白调。
6.2 用 CRC 校验让 SPI 帧不再像黑匣子
接口模式寄存器 0x02 里打开 CRC 校验后,每一帧 SPI 传输的末尾都要多带一个 CRC8 字节。这样数据被干扰时能立刻发现,而不是拿到一个错码还在算电压。CRC 多项式按手册指定值算,代码实现很小:
uint8_t ad7173_crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0xFF; // 初始值按手册取 while (len--) { crc ^= *data++; for (uint8_t i = 0; i < 8; i++) { crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x07) : (crc << 1); } } return crc; }CRC 校验在长线传输或者电机干扰强的环境里特别值得开,它能帮你分辨“是硬件问题还是数据链路问题”。我现在的习惯是开发阶段先不开,把采样链路调通后再打开,避免一开始被 CRC 报错带偏排查方向。从那以后,我每次拿到新 ADC 板子都会强制走一遍“ID 回读、内部校准、100 个样本均方根噪声统计”三个动作,这能省掉一整天查硬件的时间。希望帮到你。
本文还有配套的精品资源,点击获取