STM32驱动MCP3561/2/4:24位ADC寄存器配置与SPI时序实战
简介这份资源是面向嵌入式软硬件工程师与STM32开发者的MCP3561/2/4高精度ADC驱动程序基于STM32F373芯片平台调试通过采用硬件SPI四线接口收发未接中断脚移植到其他单片机也较为方便。由于MCP3564相关驱动资料稀缺作者结合工作实际耗时约一周完成调试对正在使用该系列ADC做数据采集的读者有直接参考价值。压缩包共78个文件以37个.h头文件与30个.c源文件为主体另含Keil工程文件、启动汇编、HEX固件及JLink调试配置等整体约224KB工程结构完整涵盖CMSIS内核文件、STM32F37x标准外设库、systick与usart等用户模块以及mcp3564驱动与主程序可直接编译下载验证。目前已有1331人学习下载适合需要快速搭建MCP356x采集链路、研究SPI时序与寄存器配置的开发者参考借鉴。1. 从一颗 24 位 ADC 说起MCP3561/2/4 驱动为什么值得在 STM32 上啃一遍如果你手头有微弱信号采集需求——应变片、热电偶、称重传感器、电化学前端——大概率绕不开 24 位 ΔΣ ADC。MCP3561/2/4 是 Microchip 这一系列里比较能打的一档单/双/四差分通道可选、内置可编程增益、最高 153.6 ksps 采样率、SPI 接口封装也小。但真正让一线工程师头疼的不是芯片本身而是驱动寄存器多、时序讲究、STM32 的 SPI 配置稍有不慎就读回一堆 0x00 或 0xFF。这篇笔记就按「先搞懂它怎么工作 → 在 STM32 上把 SPI 打通 → 把配置寄存器写对 → 把采样读回来 → 踩过的坑列清楚」的顺序走一遍目标是让你照着能复现而不是看完只知道它是个 ADC。2. MCP3561/2/4 的寄存器模型与 SPI 时序先把黑匣子拆开2.1 器件内部到底有哪些寄存器MCP3561/2/4 的寄存器空间是 16 位地址、8/16/24 位数据宽度混用的。刚上手最容易翻车的地方是它不像很多 ADC 那样「读一个寄存器返回一个值」而是通过命令字节 地址字节的组合来访问。核心寄存器大致分四类类别代表寄存器作用配置类CONFIG0~3时钟源、电流源、增益、过采样比通道类MUX正负输入通道选择状态类ADC_DATA、IRQ转换结果、中断状态校准类SCAN、TIMER自动扫描、延时控制CONFIG0 里最关键的是 CLK_SEL时钟源选择和 ADC_MODECONFIG1 管过采样比 OSR 和增益 GAINCONFIG3 管转换模式 CONV_MODE单次/连续/待机。这些位如果配错现象往往是「读回来一直是满量程」或者「数据不动」而不是报错——这就是它作为黑匣子的地方。2.2 SPI 模式与命令字节的对应关系MCP3561/2/4 的 SPI 是 Mode 0/3 兼容CPOL0/CPHA0 或 CPOL1/CPHA1但实际调试中我一般固定用 Mode 0因为 STM32 的 SPI 在 Mode 0 下最稳。命令字节的高 2 位决定操作类型0b01写寄存器0b10读寄存器0b11读 ADC 数据快速命令写寄存器时先发命令字节含地址高 4 位再发地址低 4 位然后发数据。读寄存器类似但数据阶段要留够时钟。下面是一个最小可用的读写函数骨架// 假设已有一个 SPI 句柄 hspi1CS 用 GPIO 手动控制 // MCP3561 命令字节bit7-6 操作码bit5-4 地址高 4 位 #define MCP_CMD_WRITE 0x40 #define MCP_CMD_READ 0x80 #define MCP_CMD_FAST 0xC0 static void mcp_cs_low(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); } static void mcp_cs_high(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } // 写 8 位寄存器 void mcp_write_reg8(uint8_t addr, uint8_t val) { uint8_t tx[3]; tx[0] MCP_CMD_WRITE | ((addr 4) 0x0F); // 操作码 地址高 4 位 tx[1] (addr 4) 0xF0; // 地址低 4 位左移 tx[2] val; mcp_cs_low(); HAL_SPI_Transmit(hspi1, tx, 3, 100); mcp_cs_high(); } // 读 24 位 ADC 数据快速命令不需要地址 uint32_t mcp_read_adc_data(void) { uint8_t tx[4] { MCP_CMD_FAST, 0x00, 0x00, 0x00 }; uint8_t rx[4] {0}; mcp_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 4, 100); mcp_cs_high(); // 返回 24 位有符号值注意符号扩展 uint32_t raw ((uint32_t)rx[1] 16) | ((uint32_t)rx[2] 8) | rx[3]; if (raw 0x800000) raw | 0xFF000000; // 符号扩展 return raw; }逻辑说明mcp_write_reg8里地址被拆成高 4 位和低 4 位是因为命令字节只有 6 位可用bit7-6 是操作码所以地址高 4 位塞进命令字节低 4 位地址低 4 位单独发一个字节。mcp_read_adc_data用的是快速命令不需要地址直接读 3 字节数据。参数上HAL_SPI_Transmit的超时我一般给 100ms实际远小于这个值CS 手动控制是为了保证命令字节和地址字节之间 CS 不拉高否则芯片会认为一次传输结束。2.3 STM32 SPI 初始化里必须锁死的几个参数STM32CubeMX 里配 SPI 时下面几个参数如果不对后面全是玄学Clock Polarity / PhaseMode 0CPOLLow, CPHA1EdgeData Size8 BitsFirst BitMSB FirstBaud Rate Prescaler先给 64 或 128调通后再往上提NSSSoftware用 GPIO 手动控制我见过有人把 NSS 设成 Hardware结果 CS 时序和命令字节对不上读回来全是 0xFF。血泪经验MCP3561/2/4 对 CS 的建立/保持时间有要求手动控制最省心。3. 在 STM32 上把驱动跑起来从复位到第一次采样3.1 上电复位与器件 ID 校验MCP3561/2/4 上电后需要一次复位确保寄存器回到默认值。复位方式有两种硬件复位拉低 RESET 引脚和软件复位写 CONFIG0 的复位位。我一般先硬件复位再读器件 ID 确认 SPI 通了。器件 ID 在寄存器 0x0C 附近不同型号略有差异读回来应该是固定值。如果读回 0x00 或 0xFF先查 SPI 接线和 CS别急着怀疑芯片。// 硬件复位RESET 引脚拉低至少 100us void mcp_hard_reset(void) { HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(5); // 等待内部振荡器稳定 } // 读器件 ID 验证 SPI 通信 uint8_t mcp_read_dev_id(void) { uint8_t tx[2] { MCP_CMD_READ | 0x00, 0xC0 }; // 地址 0x0C 的读命令 uint8_t rx[2] {0}; mcp_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 100); mcp_cs_high(); return rx[1]; }参数说明复位后延时 5ms 是保守值实际数据手册要求更短但多等几毫秒不影响。读 ID 时地址 0x0C 的高 4 位是 0低 4 位是 0xC所以命令字节是0x80 | 0x00地址字节是0xC0。3.2 配置 CONFIG0~3时钟、增益、过采样比配置顺序建议先 CONFIG0 定时钟和模式再 CONFIG1 定增益和 OSR最后 CONFIG3 定转换模式。下面是一个典型配置内部时钟、增益 1、OSR 128、连续转换模式。void mcp_config(void) { // CONFIG0: 内部时钟ADC 使能当前源关闭 mcp_write_reg8(0x01, 0x00); // 具体位定义参考数据手册 // CONFIG1: 增益 1OSR 128 mcp_write_reg8(0x02, 0x20); // CONFIG2: 默认 mcp_write_reg8(0x03, 0x00); // CONFIG3: 连续转换模式CRC 关闭 mcp_write_reg8(0x04, 0x40); }逻辑说明这里写的是「典型值」实际位定义要对着数据手册的寄存器映射表逐位确认。比如 CONFIG1 里 GAIN 占 3 位OSR 占 4 位不同型号的位偏移可能不同。我一般会先写一个已知值再读回来对比确认写入生效。3.3 读回 24 位采样值并转成电压采样值读回来后是 24 位有符号数需要根据增益和参考电压换算成实际电压。公式是V (raw / 2^23) * (Vref / Gain)其中 raw 是符号扩展后的 24 位值Vref 是参考电压内部或外部Gain 是 CONFIG1 里设的增益。下面是一个换算函数float mcp_raw_to_voltage(int32_t raw, float vref, uint8_t gain) { // raw 范围 -2^23 ~ 2^23-1 float fs (float)(1 23); // 满量程 return ((float)raw / fs) * (vref / (float)gain); }参数说明vref如果用内部参考典型值是 2.4V如果用外部按实际接的参考源填。gain要和 CONFIG1 里设的一致否则换算出来差一个倍数。注意 raw 要先做符号扩展否则负数会变成很大的正数。4. 避坑与排查那些让驱动「看起来通了其实没通」的细节4.1 读回全是 0x00 或 0xFF现象SPI 收发函数返回成功但读任何寄存器都是 0x00 或 0xFF。原因最常见的是 CS 时序不对——CS 在命令字节和地址字节之间被拉高了芯片认为一次传输结束后续字节被忽略。其次是 SPI 模式配错CPHA 不对导致采样边沿错位。解决用逻辑分析仪抓 CS、SCK、MOSI 三根线确认 CS 在整个命令序列期间保持低电平。如果没有逻辑分析仪先把 SPI 速率降到最低分频 256再试。4.2 采样值跳动很大或固定在某个值现象读回的 ADC 数据要么不动要么跳得离谱。原因MUX 寄存器没配正负输入通道选错了或者 CONFIG3 的转换模式没设对芯片根本没启动转换。解决先读回 MUX 和 CONFIG3 确认写入生效。如果写入不生效检查写寄存器的地址拆分是否正确——地址高 4 位和低 4 位的顺序很容易搞反。4.3 增益切换后数据不对现象改了 CONFIG1 的增益位但换算出来的电压和实际差一个倍数。原因换算公式里的 gain 和寄存器里的 gain 没对齐或者增益位写错了位置。解决写一个已知增益比如 1读回 CONFIG1 确认位值再改增益重新读回对比。不要凭记忆写位偏移。4.4 连续转换模式下数据更新不及时现象连续转换模式但读回的数据一直是旧值。原因没有检查 IRQ 状态位或者读数据太快在转换完成前就读了。解决读 ADC_DATA 前先读 IRQ 寄存器确认转换完成或者用 DRDY 引脚做中断触发。连续模式下数据更新率由 OSR 决定OSR 越大更新越慢。4.5 电源和参考电压的噪声耦合现象数据能用但噪声大有效位数上不去。原因参考电压不稳或者模拟电源和数字电源没分开滤波。解决参考引脚加 1uF100nF 去耦模拟电源用 LDO 单独供数字地和模拟地单点连接。24 位 ADC 对电源噪声很敏感这一步省不得。5. 进阶技巧用 SCAN 模式做多通道轮询与数据校验单通道调通之后下一步一般是多通道轮询。MCP3561/2/4 支持 SCAN 模式可以自动按顺序扫描多个通道省去手动切 MUX 的麻烦。配置 SCAN 的关键是设置扫描通道列表和扫描延时。下面是一个两通道轮询的配置示例// 配置 SCAN 模式通道 0 和通道 1 交替 void mcp_config_scan(void) { // 设置 MUX 为通道 0 正输入、通道 1 负输入差分 mcp_write_reg8(0x05, 0x01); // MUX 寄存器 // 设置 SCAN 寄存器使能扫描延时 32 个时钟 mcp_write_reg8(0x06, 0x80 | 0x20); // CONFIG3 设为连续转换 SCAN mcp_write_reg8(0x04, 0x60); }逻辑说明SCAN 寄存器的具体位定义各型号有差异核心是使能位和延时字段。扫描模式下每次转换完成 IRQ 会置位读 ADC_DATA 后自动切到下一个通道。参数上延时字段决定通道切换后的稳定时间信号源阻抗高就加大延时。数据校验方面MCP3561/2/4 支持 CRC 校验CONFIG3 里使能。如果 SPI 线较长或环境噪声大建议打开 CRC读数据时多读一个 CRC 字节做校验。校验失败就丢弃这次采样重新读。这个习惯在工业现场能省很多后悔药。另外一个小技巧读 ADC_DATA 时如果连续读两次第二次的值可能和第一次相同转换没完成也可能不同新数据。我一般会读两次如果相同就等一个转换周期再读避免拿到重复值。这个判断逻辑放在主循环里比单纯延时更可靠。最后说一个我自己的习惯每次改完寄存器配置先把所有关键寄存器读回来打印一遍确认写入生效再往下走。这个步骤看起来笨但能省掉大量「以为是代码问题其实是配置没写进去」的排查时间。希望帮到你。本文还有配套的精品资源点击获取