简介围绕STM32G431RBT6的ADC直接读取与DMA搬运这两种采集方式这套工程资料面向已具备HAL库基础、希望打通模拟信号采集链路的嵌入式学习者与电子竞赛备赛者。内容覆盖ADC1、ADC2的多通道配置、采样时间与分辨率设定以及转换完成触发DMA请求、缓冲区地址映射与中断配合再到采样率与传输长度的性能调优并配有可直接编译运行的完整工程实例便于对照CubeMX配置与HAL驱动源码理解寄存器层面的工作过程。压缩包共1254个文件以c源文件和h头文件为主体配合汇编启动文件s、Keil与IAR的工程配置uvprojx、icf、sct以及hex、axf等可烧录产物和map、lst等编译输出整体约29.73MB目录层次与标准工程一致便于按外设模块检索。已有1978人学习下载适合作为传感器信号采集、电源监测等实时数据采集原型的参考基线。1. 一块 G431 板子上的 ADC 读数为什么“直接读”够用而“多通道”就必须换 DMA去年帮人调一块 STM32G431RBT6 的小板子现象很怪电池电压那条通道串口打出来的码值每隔几秒就跳两百多个 LSB换了稳压源、加了滤波电容照样跳。最后发现问题根本不在模拟前端而是代码里HAL_ADC_Start之后只读了一次就把值当结果用了采样时刻刚好撞上电机 PWM 的开关沿。这件事让我后来做任何 stm32 的 ADC 采集都会先问一句这个通道是“慢变量偶尔读一次”还是“要连续盯着一组通道看波形”。这份资源解决的正是这个分岔口。它围绕 STM32G431RBT6 的 ADC1/ADC2给出两条完整的采集链路单通道直接采集轮询或中断代码短、调试直观和多通道 DMA 扫描采集循环缓冲CPU 不参与搬运。适合刚上手 G4 系列、被 CubeMX 里一堆 DMA 选项绕晕的人也适合已经能跑但读数不稳、想搞清楚采样时间和校准到底影响什么的人。2. 工程骨架与技术选型两条采集链路怎么摆在一个工程里2.1 目录结构CubeMX 生成的初始化代码和业务采集代码要分家我见过太多 G4 工程把所有东西都塞进main.cCubeMX 重新生成一次手写的采集逻辑全没了。这份资源的目录划分很克制CubeMX 管的只让它管初始化采集逻辑单独成文件.ioc之外的手写代码一律不放进生成路径。G431_ADC_Demo/ ├── Core/ │ ├── Inc/ │ │ ├── adc_direct.h # 直接采集对外接口 │ │ ├── adc_dma.h # DMA 扫描采集对外接口 │ │ └── adc_filter.h # 滤波与码值换算 │ └── Src/ │ ├── main.c # 只做调度初始化 主循环取数 │ ├── adc.c # CubeMX 生成只放 MX_ADC1_Init │ ├── dma.c # CubeMX 生成 │ ├── adc_direct.c # 轮询 / 中断两种实现 │ ├── adc_dma.c # DMA 启动、半传输回调、缓冲切换 │ └── adc_filter.c # 滑动平均、一阶低通 ├── Drivers/ │ ├── STM32G4xx_HAL_Driver/ │ └── CMSIS/ ├── G431_ADC_Demo.ioc # CubeMX 工程文件 └── MDK-ARM/ # Keil 工程用 CubeIDE 的话这里是 STM32CubeIDE/这里的关键是adc.c里只有MX_ADC1_Init()任何HAL_ADC_ConfigChannel的二次调用都写在adc_direct.c/adc_dma.c里。因为 CubeMX 重新生成时会覆写adc.c而adc_direct.c属于“用户代码”不会被碰。这个习惯看着小实际能省掉大量“改了个引脚采集逻辑全丢”的血泪经验。技术栈上没什么花活STM32CubeMX 做时钟和外设初始化HAL 库STM32G4xx HAL Driver做驱动Keil MDK 或 STM32CubeIDE 编译串口用printf重定向到 USART2 打印码值。G431RBT6 是 Cortex-M4F 内核带 FPU所以后面码值转电压直接写浮点也不用担心性能但要注意中断回调里做浮点运算仍然要控制频率。2.2 时钟树与 ADC 时钟42.5 MHz 这个数字怎么算出来的G4 的 ADC 有个很多人第一次踩的坑它的时钟不是自动安全的。芯片主频能跑到 170 MHz但 ADC 的输入时钟上限是 60 MHz中间隔着 RCC 的 ADC12 时钟源选择和 CKMODE 分频两级。这份工程的配置是HSI16 作为 PLL 源PLLM 4 得到 4 MHzPLLN 85 得到 340 MHz 的 VCOPLLR 2 得到 SYSCLK 170 MHzADC12 时钟源选 PLLP同样 170 MHzCKMODE 设成 4 分频最终 ADC 时钟 42.5 MHz。这个值既没超限又保证了转换速度。转换时间怎么估12 位分辨率下逐次逼近本身要 12.5 个 ADC 周期加上采样时间一次转换的总周期数是两者之和。采样时间选 47.5 周期时总共 60 个周期42.5 MHz 下约 1.41 µs也就是单通道理论最高约 700 ksps。这个数字决定了你后面 DMA 缓冲开多大、多久处理一次——如果想 10 kHz 采三个通道1.41 µs 完全够甚至有点浪费可以把采样时间压到 12.5 周期换更高吞吐。提示CubeMX 的时钟树页面会直接标红超频项但 ADC 那一路的分频经常被折叠在 Clock Configuration 的右侧容易只盯着 SYSCLK 看。2.3 直接采集还是 DMA用一张表定方案选型不用纠结看三个条件就够了通道数、采样率、以及采集期间 CPU 有没有别的事干。判断维度直接采集轮询/中断DMA 扫描采集通道数12 个2 个以上需要同一时刻的快照采样率几十 Hz 到几 kHz随用随读连续、固定节拍几 kHz 以上CPU 占用轮询期间阻塞中断方式占用小但频繁只在半传输/传输完成时进一次回调代码复杂度低一个函数能跑通中要管缓冲、回调、数据一致性典型场景电池电压、电位器、按键模拟量三相电流、电机 FOC、多路传感器巡检我的建议是先用直接采集把通道和硬件验证通确认引脚、参考电压、采样时间都对再切到 DMA。反过来做的话一旦读数不对你很难判断是 DMA 配置错了还是模拟前端本来就有问题等于给自己造了个黑匣子。3. 直接采集链路拆解轮询、中断、校准三步落地3.1 上电第一件事G4 的 ADC 校准不能省G4 系列的 ADC 内部有偏移误差出厂没校准过的芯片零点可能偏十几个 LSB。HAL 里对应的是HAL_ADCEx_Calibration_Start必须在 ADC 使能之后、任何一次转换之前调用。/* adc_direct.c —— ADC 初始化校准只做一次放在上电流程里 */ #include adc_direct.h extern ADC_HandleTypeDef hadc1; void ADC_Direct_Init(void) { /* G4 必须做校准单端模式下传 ADC_SINGLE_ENDED */ if (HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED) ! HAL_OK) { Error_Handler(); /* 校准失败说明时钟或供电没起来别硬跑 */ } }这个函数内部会跑一套自校准流程耗时大概几十微秒返回值必须判。我遇到过供电还没稳就调校准返回HAL_ERROR后面所有读数整体偏低 30 多个 LSB查了半天以为是分压电阻焊错了。校准失败的常见原因是 VDDA 没到位或者 ADC 时钟没配别急着怀疑代码逻辑。还要注意校准结果跟当时的采样时间、单端/差分模式是绑定的。如果你后面在运行期改了SingleDiff或者在初始化之后又调了一次HAL_ADC_ConfigChannel改动采样时间校准的有效性就打折扣了正确的顺序永远是“时钟 → ADC init → 校准 → 配通道 → 启动”。3.2 单通道轮询读数的完整函数轮询是最朴素的写法适合调试和低频读取。这份实现里加了两个细节通道配置每次重配支持运行期切通道以及超时后必须HAL_ADC_Stop。/* adc_direct.c —— 单通道轮询读取返回原始码值 */ uint16_t ADC_Read_Raw(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; /* 如 ADC_CHANNEL_1 */ sConfig.Rank ADC_REGULAR_RANK_1; /* 单通道固定 Rank1 */ sConfig.SamplingTime ADC_SAMPLETIME_47CYCLES_5; /* 高阻抗源要加长 */ sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { return 0xFFFF; /* 0xFFFF 作错误哨兵 */ } HAL_ADC_Start(hadc1); /* 软件触发一次转换 */ if (HAL_ADC_PollForConversion(hadc1, 2) ! HAL_OK) { HAL_ADC_Stop(hadc1); /* 超时不停状态机会变脏 */ return 0xFFFF; } return (uint16_t)HAL_ADC_GetValue(hadc1); }逐段说一下。SamplingTime是核心参数47.5 个周期在 42.5 MHz 下约 1.1 µs 的采样窗口对应源阻抗可以到几十 kΩ 量级如果信号源是 100 kΩ 以上的分压电阻这个值还不够要往上加。PollForConversion的第二个参数是超时毫秒数给 2 ms 已经很宽松正常 1.4 µs 就完成了。超时返回后一定要HAL_ADC_Stop否则 ADC 内部状态机停在“转换中”下一次HAL_ADC_Start会直接返回HAL_BUSY然后你会看到一个永远读不出数的 ADC。0xFFFF这个哨兵值是我个人的习惯12 位 ADC 最大 4095不可能出现 65535调用方判一下就能区分“真读数”和“读失败”。你也可以用返回HAL_StatusTypeDef 指针出参的方式看团队习惯。3.3 中断方式把等待挪到回调里轮询的缺点是 CPU 干等 1.4 µs看着短但主循环里如果每毫秒读 8 个通道累积起来也是可观的开销。中断方式把等待挪进回调启动和取数是分开的。/* adc_direct.c —— 中断方式启动后立即返回结果在回调里取 */ static volatile uint16_t g_adc_irq_val; static volatile uint8_t g_adc_irq_flag; void ADC_Start_IT(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_47CYCLES_5; sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; HAL_ADC_ConfigChannel(hadc1, sConfig); g_adc_irq_flag 0; HAL_ADC_Start_IT(hadc1); /* 转换完成会进 HAL_ADC_ConvCpltCallback */ } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { g_adc_irq_val (uint16_t)HAL_ADC_GetValue(hadc); g_adc_irq_flag 1; /* 只置标志别在这里打印或做浮点 */ } }回调里只做两件事取值、置标志。串口打印、滤波、换算统统放到主循环。原因是中断回调运行在中断上下文printf重定向到串口是阻塞发送一次几十微秒到几毫秒直接把中断频率卡死还会引起主循环里其他中断的抖动。这是新手最容易犯的错串口一打印采样节奏全乱然后怀疑是 ADC 不稳。volatile两个变量都不能省否则编译器优化后主循环里读到的永远是初始值你会看到一个“中断进了但变量没变”的玄学现象。3.4 采样时间与码值换算4095 还是 4096读数拿到手是 04095 的整数换算成电压的时候分母写 4095 还是 4096争论一直有。从原理上说12 位 ADC 的输出码是Vin/Vref × 4096向下取整最大值 4095 对应的是“满量程减一个 LSB”所以严格换算用 4096 更贴近传递函数但从“04095 均匀铺满 0Vref”的实用角度用 4095 让满量程刚好落在 Vref。这份工程用 4095因为它直观误差在 0.024% 量级做工程够用。/* adc_filter.c —— 码值转电压并顺手做一次 VDDA 修正 */ float ADC_Raw_To_Volt(uint16_t raw) { return (float)raw * ADC_VREF / 4095.0f; /* ADC_VREF 默认 3.3f */ } /* 用内部参考通道反推实际 VDDA电池供电时很有用 */ float ADC_GetVDDA(void) { uint16_t raw ADC_Read_Raw(ADC_CHANNEL_VREFINT); /* 需要长采样时间 */ if (raw 0 || raw 0xFFFF) { return 3.3f; /* 读失败退回默认值 */ } return 3.0f * (float)(*VREFINT_CAL_ADDR) / (float)raw; }ADC_GetVDDA的思路是内部参考电压通道的绝对电压是固定的典型 1.212 V但 ADC 是用 VDDA 当基准去量化它所以 VDDA 变了读到的码值会反向变化。出厂校准值VREFINT_CAL_ADDR是在 VDDA 3.0 V 条件下测的用它除以当前码值再乘 3.0 V就能反推出现在的 VDDA。这里有两个必须注意的点一是 VREFINT 通道的采样时间要足够长手册要求最小采样时间通常是微秒级用 2.5 周期会读到明显偏小的值我一般直接给 640.5 周期二是VREFINT_CAL_ADDR的地址跟具体型号相关移植到别的 G4 型号时要去手册的“出厂校准值”章节核对别直接抄。如果板子上 VREF 引脚外挂了基准芯片比如 2.5 V 基准那换算就用那个值别再用 VDDA这时候读数稳定性会明显好一截——G4 内部也有 VREFBUF可以输出 2.048/2.5/2.9 V 给 VREF但驱动能力只有几毫安带外部负载要加运放缓冲。4. DMA 采集链路拆解多通道扫描 循环缓冲4.1 CubeMX 里那几个必须改的勾选项DMA 采集跑不起来九成是 CubeMX 里几个勾没勾对。这份工程采集三路ADC_CHANNEL_1外部电压、ADC_CHANNEL_VREFINT内部参考、ADC_CHANNEL_TEMPSENSOR内部温度配置差异如下。配置项直接采集DMA 扫描采集说明Scan Conversion ModeDisableEnable多通道必须开否则只转 Rank1Continuous Conversion ModeDisableEnable不开就只转一轮DMA 也只搬一轮DMA Continuous Requests—Enable让 ADC 持续发 DMA 请求Number Of Conversion13必须等于 Rank 数量OverrunOverrun preservedOverrun overwrittenDMA 模式下必须覆盖否则溢出即停External TriggerSoftware StartSoftware Start 或 TIM 触发要固定采样率就用定时器触发Overrun这一项特别容易被忽略。默认是preserved意思是转换溢出时保留旧数据、ADC 停止接受新转换结果就是你的 DMA 缓冲里前几个数是对的后面全是陈旧值而且没有任何报错。改成overwritten之后新数据直接覆盖旧数据虽然会丢样本但采集链路不会卡死配合循环 DMA 才是正确姿势。另外 G4 有 DMAMUXDMA 请求源和 DMA 通道是解耦的CubeMX 里看到的“DMA1 Channel1 / Channel2”只是通道号真正映射到哪个请求由 DMAMUX 配。这意味着你改 ADC 实例比如从 ADC1 换到 ADC2时DMA 请求要跟着在 DMAMUX 里改别只改 ADC 那一页。4.2 启动、半传输与缓冲切换DMA 采集的核心是“缓冲怎么切”。这份实现用长度 通道数 × 2 的缓冲配合半传输和传输完成两个回调等于双缓冲前半段在处理时后半段在采集天然避免了读写冲突。/* adc_dma.c —— 循环 DMA 双缓冲 */ #include adc_dma.h #define ADC_CH_NUM 3 #define ADC_SEG_NUM 2 /* 前后两段双缓冲 */ extern ADC_HandleTypeDef hadc1; /* 必须是全局或静态DMA 直接按地址搬不能是栈上局部数组 */ static uint16_t g_adc_buf[ADC_CH_NUM * ADC_SEG_NUM]; static volatile uint8_t g_seg_ready; /* bit0: 前半段就绪, bit1: 后半段 */ void ADC_DMA_Start(void) { HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED); /* 长度单位是“半字数”12 位结果右对齐每个样本占 16 bit */ HAL_ADC_Start_DMA(hadc1, (uint32_t *)g_adc_buf, ADC_CH_NUM * ADC_SEG_NUM); } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { g_seg_ready | 0x01; /* 前半段可读只置标志 */ } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { g_seg_ready | 0x02; /* 后半段可读 */ } } /* 主循环里调用按段取数取完清标志 */ const uint16_t *ADC_GetSegment(uint8_t seg) { uint8_t mask (seg 0) ? 0x01 : 0x02; if ((g_seg_ready mask) 0) { return 0; /* 该段还没采满 */ } g_seg_ready (uint8_t)~mask; return g_adc_buf[seg * ADC_CH_NUM]; }HAL_ADC_Start_DMA的第三个参数是传输长度单位是“数据项个数”不是字节数。因为 ADC 的 DR 寄存器是 16 位、DMA 外设数据宽度配成半字所以长度写 6 就是搬 6 个半字。如果你把 DMA 数据宽度配成字32 位长度含义就变了而且 ADC 只给 16 位有效数据剩余位是脏的——这类问题查起来非常费劲因为数值看着“像那么回事”只是高位随机。回调里同样只置标志。g_adc_buf必须是静态或全局DMA 是按物理地址搬运的放在栈上函数返回后地址就失效了表现是数据写飞、偶尔进 HardFault。按段取数的设计还有一个好处段索引和通道索引是解耦的。ADC_GetSegment(0)返回的是buf[0..2]正好对应 Rank1/2/3 三个通道顺序由 CubeMX 里的 Rank 顺序决定跟缓冲布局无关。这条对应关系要写进头文件注释里否则三个月后没人记得buf[1]到底是参考电压还是温度。4.3 滤波滑动平均 一阶低通怎么选DMA 拿到的是裸数据抖动依然存在——12 位 ADC 的有效位数通常在 1011 位末两位本来就是噪声。滤波不是可选项是必需项。/* adc_filter.c —— 两种滤波按信号特性选 */ #define MOV_WIN 8 /* 滑动平均适合慢变量比如电池电压、温度 */ uint16_t ADC_MovAvg(uint16_t raw) { static uint16_t win[MOV_WIN]; static uint8_t idx, cnt; uint32_t sum 0; win[idx] raw; idx (uint8_t)((idx 1) % MOV_WIN); if (cnt MOV_WIN) cnt; for (uint8_t i 0; i cnt; i) { sum win[i]; } return (uint16_t)(sum / cnt); /* 前几个样本按实际个数平均 */ } /* 一阶低通适合要跟得上变化的量alpha 越小越平滑但越滞后 */ uint16_t ADC_LowPass(uint16_t raw) { static uint32_t y 0; /* 定点避免回调里跑浮点 */ const uint8_t shift 3; /* 等价 alpha 1/8 */ if (y 0) y raw; /* 首次直接赋值避免从 0 爬升 */ y y (((int32_t)raw - (int32_t)y) shift); return (uint16_t)y; }滑动平均的窗口越大越平滑但响应越慢8 点窗口在 1 kHz 采样下相当于 8 ms 延迟做电池监测没问题做电流环就太慢了。一阶低通用移位代替浮点乘shift 3对应时间常数约 8 个采样周期改动 shift 就能调平滑度是中断和主循环里都能用的写法。选择上我的经验是慢变量用滑动平均因为它对阶跃响应是有限长的不会拖尾需要跟随的动态量用一阶低通计算量恒定、内存占用固定。两个都用也不是不行但要知道串联之后相位滞后会叠加做闭环控制时容易把相位裕度吃掉。5. 避坑与排查五个真实翻车现场5.1 ADC 时钟超了 60 MHz读数整体偏移现象所有通道的读数系统性偏大 3%8%换通道、换采样时间都改不了用手摸板子温度升高后偏移量还会变。原因CubeMX 里 ADC12 时钟源选了 PLLP170 MHzCKMODE 忘了分频等于 170 MHz 直接灌进 ADC。超频不会立刻死机而是转换精度崩掉这是最阴的一类问题——代码逻辑全对就是数不对。解决把 CKMODE 设成 4 分频得到 42.5 MHz重新生成代码。验证方法是拿一个已知电压比如 1.000 V打进去看读数是不是稳定在 1241 左右1.0/3.3×4095偏差应该在一个 LSB 以内。5.2 DMA 只进了一次回调之后没动静现象串口只打印了第一批数据之后g_seg_ready一直是 0ADC 还在跑但缓冲不再更新。原因DMA 配成了 Normal 模式或者 ADC 的 Continuous Conversion Mode 没开。Normal 模式下 DMA 搬完设定长度就停了而 ADC 只转了一轮也停了两边都静止。解决DMA 模式改 CircularADC 的 Continuous Conversion 和 DMA Continuous Requests 都使能。判断技巧是看 DMA 的CNDTR寄存器循环模式下它应该一直在递减并回绕如果停在 0 不动就是模式配错了。5.3 多通道顺序错乱温度值跑到电压位现象buf[0]本该是外部电压结果算出来的温度明显不对反过来buf[2]又像电压值。原因两个可能。一是 CubeMX 里 Rank 的顺序和代码里索引的假设不一致CubeMX 里 Rank1 未必是你以为的那个通道二是在运行期用HAL_ADC_ConfigChannel改了某个通道的 Rank但缓冲索引没跟着改。解决在头文件里把索引写死成宏#define IDX_VBAT 0、#define IDX_VREF 1、#define IDX_TEMP 2并在adc.c的初始化代码旁边加注释标明 Rank 顺序。运行期不要改 Rank要切通道就重配整个序列。5.4 高阻抗信号源读数偏低万用表和 ADC 对不上现象万用表量分压点 1.65 VADC 换算出来 1.20 V误差稳定不抖。原因采样时间太短ADC 内部采样电容没充满。G4 的采样电容是几个 pF 量级源阻抗如果是 100 kΩ时间常数就是几百纳秒2.5 个周期约 59 ns根本不够。解决采样时间加到 47.5 或 247.5 周期或者在前端加一级运放跟随器把源阻抗降下来。估算公式是t (R_source R_adc) × C_adc × ln(2^13)按 13 位精度算R_source 100 kΩ 时大概需要 35 µs 的采样窗口。5.5 校准做完了跑一会儿零点又漂了现象上电校准后零点很准运行几分钟后读数整体抬高十来个 LSB重新上电又好了。原因校准之后又调了HAL_ADC_ConfigChannel修改采样时间或单端/差分模式。校准结果是跟这些配置绑定的改了配置等于让校准失效而且不会报错。解决把初始化顺序固定成“时钟 → ADC init → 校准 → 配通道 → 启动”运行期只允许改 Rank 之外的常规配置SingleDiff一旦定了就不动。如果业务确实需要动态切差分模式就在切换后重新校准一次。6. 二次开发改造把采集层抽成接口换芯片只改一个文件跑通之后最容易失控的地方是应用层直接extern ADC_HandleTypeDef hadc1然后到处HAL_ADC_GetValue。等哪天要换成 ADC2、或者换到另一颗 G4 型号改动会蔓延到整个工程。我一般会加一层薄接口把“采集”这件事抽象出来。/* adc_port.h —— 面向接口的采集层应用层只依赖这个头文件 */ typedef struct { void (*init)(void); uint16_t (*read_raw)(uint32_t ch); /* 阻塞式慢速通道用 */ void (*start_scan)(uint16_t *buf, uint16_t len); /* DMA 连续扫描 */ float (*to_volt)(uint16_t raw); } adc_port_t; extern const adc_port_t adc_g431; /* 具体实现放 adc_g431.c */应用层写const adc_port_t *adc adc_g431;之后只调adc-read_raw(...)。换芯片时新增一个adc_xxx.c实现同一套函数指针就行业务代码一行不动。这层抽象的成本只有一个结构体和几个转发函数收益是移植时不用满工程搜HAL_ADC。改造完必须验证不能靠“看着差不多”。我用三步第一步可调直流源打 0.5 V / 1.5 V / 2.5 V 三个点记录码值和换算电压误差超过 ±2 LSB 就回去查采样时间第二步用ADC_GetVDDA读出来的值和万用表量 VDDA 对比差值应该在 30 mV 以内偏差大说明内部参考通道的采样时间不够第三步跑 30 分钟连续采集记录最大值和最小值峰峰值超过 10 LSB 就要考虑加滤波或者查电源纹波。输入电压理论码值实测码值换算电压误差0.500 V6206210.5004 V0.4 mV1.500 V186118601.4989 V-1.1 mV2.500 V310231032.5008 V0.8 mV那张表我一般会贴在工程的README里标定日期和当时的室温都写上。因为有次在冬天标定好的系数夏天拿到现场差了十几个 mV查了半天才发现是分压电阻的温漂跟 ADC 一点关系没有。从那以后我每次动 ADC 配置——哪怕只是把采样时间从 47.5 改成 12.5——都强制走一遍三点标定加 30 分钟漂移测试跑完才允许提交。这套流程看着笨但比在现场对着跳动的串口数字猜要快得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?