news 2026/10/26 8:43:32

STM32G431 ADC采集:轮询、中断与DMA配置避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32G431 ADC采集:轮询、中断与DMA配置避坑指南

简介:这份面向STM32嵌入式开发者的实践资料包,围绕STM32G431RBT6的ADC直接采集与DMA传输展开,适合正在学习G4系列片内ADC、需要把模拟信号采集落到工程中的初中级开发者。内容涉及ADC1/ADC2多通道配置、采样时间与分辨率设置、单次与连续转换、DMA通道链接及中断配合,也讨论采样率、转换序列和传输缓冲区等性能优化问题,可对照理解直接读寄存器与DMA自动搬运两条路径的差异。压缩包共1254个文件,以C源码和H头文件为主,辅以汇编启动文件、工程配置、链接脚本与库文件,整体约29.73MB,目录完整便于按模块查阅。已有1978人学习,可作为传感器信号采集、电源监测等实时数据采集场景的工程参考,帮助梳理配置流程、缓冲管理和常见排错思路。

1. 同一路信号两种采法:G431 的 ADC 到底该轮询还是走 DMA

手头一块 STM32G431RBT6 的核心板,要把一路 0~3.3V 的传感器信号读进来,第一反应都是开 ADC。真到 CubeMX 里点配置才发现问题:Continuous Conversion Mode 和 DMA Continuous Requests 勾不勾、缓冲区长度填多少、采样时间给几个周期,生成的工程有时跑得通,有时读数在 0 和 4095 之间乱跳。这套资源把同一路 ADC 的两种采法——CPU 直接读取(轮询或中断取数)和 DMA 搬运——放在一个可对照的工程里,通道数、采样时间、时钟分频、缓冲长度都留了可调的口子。适合已经点过灯、能看懂 HAL 函数名,但还没把 ADC 时序和 DMA 循环模式彻底理清的人。

2. ADC 时钟与采样时间:把 4 Msps 的账先算清楚

G431 的 ADC 标称 12 位下最高 4 Msps,这个数字写在数据手册首页,但它不是"配置一下就有"的。它由三个量咬合决定:ADC 内核时钟频率、12 位转换本身占用的周期数、以及你给的采样时间。这三个量算不明白,后面无论轮询还是 DMA 都只是在搬一堆没有意义的数字。

2.1 ADC 内核时钟从哪来,能开多高

G4 的 ADC 时钟有两个来源,由 ADC_CFGR 里的 CKMODE 位域选择。同步模式下直接取 AHB 时钟 HCLK 的分频(/1、/2、/4),异步模式下取 PLLP 再经过 RCC 里的 ADC 预分频器(/1、/2、/4)。主频 170MHz 时,同步模式实际可用的最大值是 HCLK/4 = 42.5MHz——HCLK/2 就是 85MHz,已经贴在数据手册的上限之外了。

12 位转换本身固定消耗 12.5 个 ADC 周期,加上最小采样时间 2.5 个周期(快速通道),一轮完整的转换至少要 15 个 ADC 周期。所以吞吐上限的算法很直接:

ADC 时钟配置实际频率单次转换周期单次耗时理论吞吐是否在手册范围内
HCLK/1170 MHz1588 ns11.3 Msps否,远超上限
HCLK/285 MHz15176 ns5.6 Msps否,超 60MHz
HCLK/442.5 MHz15353 ns2.83 Msps是
PLLP 60MHz60 MHz15250 ns4.00 Msps是,贴着上限

想让 ADC 跑满 60MHz,得靠 PLLP。但 PLLP 和 SYSCLK 共用同一个 VCO:170MHz 主频意味着 VCO 落在 340MHz,PLLP 的分频系数只能是整数,340/6 = 56.7MHz、340/8 = 42.5MHz,凑不出干净的 60MHz。为了把 ADC 时钟抬到 60MHz 去动整个 PLL,通常会把系统主频、USB 时钟、定时器时基一起带偏,这笔账不划算。

提示:绝大多数采样率需求在 42.5MHz 下已经够用。真要 4Msps 以上的吞吐,先问清楚信号带宽是不是真的需要,而不是先把时钟拉满。

所以这套工程的默认配置是 HCLK/4 = 42.5MHz,用加长采样时间换稳定性,而不是拿时钟精度去赌。这个选择在后面所有代码里都成立。

2.2 采样时间不是随便选的,它和信号源内阻绑定

采样时间决定了 ADC 内部采样保持电容能不能被外部信号"充满"。G4 的采样电容大约 5pF,内部采样开关等效电阻在 1kΩ 量级,12 位精度要求充满到 1/4096,也就是约 9.7 个时间常数。用一个工程上够用的估算式:

ts ≥ (R_ADC + R_source) × C_ADC × ln(2^(N+2))

N = 12 时 ln(2^14) ≈ 9.7,(R_ADC + R_source) × 5pF × 9.7 ≈ (R_ADC + R_source) × 48.5ns。把这个式子按 42.5MHz 的时钟折算成采样周期数,就得到一张能直接查的表:

采样时间(周期)@42.5MHz 实际时长估算允许的最大源阻抗典型用法
2.559 ns< 200 Ω运放直出、低阻缓冲
6.5153 ns< 2 kΩ板内短走线的分压电路
12.5294 ns< 5 kΩ一般电位器、传感器模块
24.5576 ns< 10 kΩ常见默认值,稳妥
47.51.1 µs< 20 kΩ高阻信号、长线
92.52.2 µs< 40 kΩ需要外部电容配合
247.55.8 µs< 100 kΩ慢变量,如温度、电池电压

表里是估算值,实际还要算上走线电阻、保护电阻、RC 滤波里串的那个电阻。血泪经验是:一个 10kΩ 电位器分压直接进 ADC、采样时间给 6.5 周期,读数会明显偏向中间值且抖动大——不是 ADC 坏了,是采样电容根本没充到位。把采样时间改到 47.5 周期,读数立刻老实。

注意:源阻抗高的场合,除了加长采样时间,更有效的做法是在 ADC 引脚前加一级运放跟随器,把源阻抗压到几十欧,采样时间随便给都稳。

2.3 通道规划与工作模式,先定表再点 CubeMX

配置之前先把这几件事定下来,能省掉后面大量返工:采几个通道、每个通道什么信号、要不要连续转换、由谁触发、数据放到哪块内存。

配置项单通道轮询单通道定时触发多通道连续扫描
Scan Conversion ModeDisableDisableEnable
Continuous ConversionDisableDisableEnable
External TriggerSoftwareTimer TRGOTimer TRGO 或软件
DMA Continuous RequestsDisableDisableEnable
Overrun 处理不涉及不涉及覆盖模式(默认)
数据出口HAL_ADC_GetValue中断回调取数DMA 缓冲区

Scan Conversion Mode 必须和通道数匹配:只采一路却开了 Scan,Rank 里多余的空位会带来不可预期的行为;采三路却关了 Scan,ADC 只会采 Rank 1 配的那一路然后停下。这一条在排查阶段被反复翻出来,先记住。

通道与引脚的对应关系别去背表——不同封装可用的模拟通道不一样,LQFP64 和 QFN48 能引出的 ADC 引脚数量就差着一截。正确姿势是在 CubeMX 的引脚分配视图里勾选 ADC1_INx,能被复用的引脚会自动变绿,选中哪个就从哪个进。工程里只规定通道编号(IN1、IN2、IN3 这种),具体落在哪个 GPIO 交给 CubeMX 决定,换封装时不用改代码。

3. 直接采集落地:轮询、中断与校准三件事的完整代码

"直接采集"这个说法在工程里其实包含两条路径:CPU 亲自等结果(轮询),和转换完成后用中断通知 CPU 来取。两者的时序模型完全不同,适用场景也不一样,但都绕不开初始化之后的第一次校准。

3.1 轮询:最短路径,也是错误处理最容易漏的地方

轮询的模型是"我启动一次转换,然后站在门口等它结束"。CubeMX 里把 Continuous Conversion 和 Scan 都关掉,触发源选 Software Trigger,生成的初始化代码就够用了。真正要写的是取数这一层:

/* adc_poll.c —— 单通道轮询采集,带完整错误出口 */ #include "main.h" extern ADC_HandleTypeDef hadc1; #define ADC_ERR_INVALID 0xFFFFu /* 12 位数据最大 4095,用 0xFFFF 当错误码不会和数据混淆 */ static uint16_t adc_read_once(uint32_t timeout_ms) { uint16_t value = 0; /* 1. 启动一次转换:内部会置位 ADSTART,软件触发一次规则组转换 */ if (HAL_ADC_Start(&hadc1) != HAL_OK) { return ADC_ERR_INVALID; } /* 2. 等转换完成:超时值以 ms 计,靠 HAL_GetTick 推进 */ if (HAL_ADC_PollForConversion(&hadc1, timeout_ms) != HAL_OK) { HAL_ADC_Stop(&hadc1); /* 超时一定要 Stop,否则 ADC 停在 BUSY,下次 Start 直接失败 */ return ADC_ERR_INVALID; } /* 3. 取结果:DR 寄存器里的 12 位右对齐数据 */ value = (uint16_t)HAL_ADC_GetValue(&hadc1); /* 4. 关掉转换,回到干净的初始状态 */ HAL_ADC_Stop(&hadc1); return value; } /* 码值转电压:用实测 VDDA 而不是想当然的 3.3V */ static float adc_to_volt(uint16_t raw, float vdda) { return (float)raw * vdda / 4095.0f; }

逐段说明。HAL_ADC_Start 的返回值必须判:ADC 处于 BUSY 状态时它会直接返回 HAL_BUSY,而很多人习惯写HAL_ADC_Start(&hadc1);然后往下走,一旦上一次转换因为异常没 Stop,后面所有读数都会是同一个旧值——这是"读出来一直不变"最常见的来源之一。HAL_ADC_PollForConversion 的第二个参数是超时毫秒数,不是循环次数;它内部按 HAL_GetTick 判断,因此依赖 SysTick 中断正常运转,如果你在中断里关掉了全局中断再调用它,会一直等到超时。HAL_ADC_Stop 的意义是把 ADSTART 清掉,让下次调用从确定状态开始。

换算里的 vdda 参数不要写死 3.3。USB 供电、LDO 供电、电池供电的板子,VDDA 实测可能是 3.28 或 3.31,按 10 位差异算,满量程就有十几个码值的偏差。真正准的做法在 3.3 节。

轮询的代价是 CPU 全程被占用。单次转换 353ns 看着不长,但 HAL 库函数调用、状态判断加起来通常是微秒级开销,一旦放到 10kHz 以上的循环里就会拖垮其他任务。采一路慢变量(电池电压、温度)用它最省事,采波形就别想了。

3.2 中断:把采样节拍交给定时器,CPU 只负责取数

采样间隔要求稳定时,软件触发就不合适了——触发时刻取决于主循环跑到哪,抖动可能有几十微秒。常见做法是用一个定时器输出 TRGO 事件去触发 ADC,转换完成再进中断取数。

CubeMX 里的关键几步:

  1. 选一个基本定时器(TIM3 之类),在 Trigger Output 里把 TRGO 设为 Update Event。
  2. 按需要的采样率算 ARR 和 PSC。主频 170MHz、要 10kHz 采样,ARR 设为 17000、PSC 设为 0 就得到 10kHz 更新事件。
  3. ADC 的 External Trigger Conversion Source 选对应定时器的 TRGO,边沿选 Rising。
  4. ADC 的 Continuous Conversion 关掉,NVIC 里打开 ADC1_2 中断。
/* adc_it.c —— 定时器触发 + 转换完成中断取数 */ extern ADC_HandleTypeDef hadc1; extern TIM_HandleTypeDef htim3; static volatile uint16_t g_last_raw = 0; static volatile uint32_t g_sample_cnt = 0; void adc_timer_start(void) { /* 先让 ADC 处于就绪状态,再由定时器 TRGO 逐个脉冲驱动转换 */ HAL_ADC_Start_IT(&hadc1); HAL_TIM_Base_Start(&htim3); } /* 每完成一次规则组转换进一次这个回调 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { g_last_raw = (uint16_t)HAL_ADC_GetValue(hadc); g_sample_cnt++; } }

逻辑上要抓住两点。第一,HAL_ADC_Start_IT 只调一次,之后每次 TRGO 上升沿都会自动启动一次新的转换,不需要在回调里反复调用——很多人习惯性地在回调里再 Start_IT 一次,结果转换被重复触发,实际采样率翻倍,FFT 出来全是镜像。第二,回调里只做取数和计数,任何耗时运算(浮点换算、滤波、打印)都挪到主循环,中断里待太久会挤掉下一个触发。

g_last_raw 用 volatile 修饰是必须的:它在中断里写、在主循环里读,编译器不加 volatile 会把它优化进寄存器,主循环永远看到初始值。这个坑在 -O2 优化下才暴露,Debug 版本反而正常,属于典型的"换个编译选项就翻车"。

提示:确认采样率是否真如你所想,最省事的办法是在回调里翻转一个空闲 GPIO,用逻辑分析仪量方波周期。别信代码注释,信波形。

3.3 校准与 VREFINT:让码值能换算成真实电压

G4 的 ADC 出厂时并不保证零点准确,上电后必须做一次自校准。HAL 里对应 HAL_ADCEx_Calibration_Start,注意 G4 和 F1 不同,它多一个差分模式参数:

/* 校准必须在 ADC 使能转换之前做,全程只做一次 */ static void adc_calibrate_once(void) { if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED) != HAL_OK) { Error_Handler(); } } /* 用内部参考电压反推真实 VDDA,比假设 3.3V 准得多 */ static float adc_measure_vdda(void) { uint16_t raw_vref = 0; uint32_t vref_cal = *(uint32_t *)VREFINT_CAL_ADDR; /* HAL 宏,别手抄地址 */ /* 采内部 VREFINT 通道,采样时间必须给足,数据手册要求最短 4µs 量级 */ raw_vref = adc_read_channel(ADC_CHANNEL_VREFINT); if (raw_vref == 0) return 3.3f; /* 防御:读失败时退回标称值 */ /* VREFINT 校准值是在 3.0V 条件下测的,按比例反推当前 VDDA */ return (3.0f * (float)vref_cal) / (float)raw_vref; }

VREFINT_CAL_ADDR 和 VREFINT_CAL_VREF 是 HAL 头文件里给出的宏,指向芯片系统存储区里的出厂校准值,不要从别的型号文档里抄地址过来——不同系列、不同容量放在不同位置,抄错了读出来是一堆随机数。校准函数只在初始化后调一次,转换进行中调用会失败;调用前 HAL 内部会先关掉 ADC,所以别把它塞进采样循环里。

采内部 VREFINT 通道时采样时间要单独放大,大概是最短 4µs 量级,比外部通道苛刻。如果 CubeMX 里所有通道共用一个采样时间,为了 VREFINT 会把整体采样率拉低,通常的做法是给 VREFINT 单独配一个 Rank,或者干脆上电测一次 VDDA 存起来,运行期不再重测。

这一套下来,得到的不是一个"大概 3.3V"的换算系数,而是当前板子真实的 VDDA,码值换算成电压的误差能从百分之一量级压到千分之一量级。

4. DMA 采集落地:单通道循环缓冲与多通道扫描的两套配置

轮询和中断的共同问题是每个样本都要 CPU 出手。要做到几十 kHz 甚至上百 kHz 的连续采样,就得让 DMA 把 ADC 数据寄存器里的值自动搬进内存,CPU 只在缓冲区攒够一批后处理一次。这套资源里两种 DMA 用法各有一套配置,差别比想象中大。

4.1 请求映射与三处必须对齐的配置

G4 系列用 DMAMUX 做请求路由,ADC1 的请求可以映射到多个 DMA 通道上,CubeMX 里选了 ADC1 之后会自动帮你分配一条,一般落在 DMA1 的某个通道。这一点比老型号灵活,但也意味着你不能凭习惯去猜"ADC1 一定是 DMA1_Channel1"。

真正要盯死的是三处对齐:

对齐项外设侧内存侧不一致的后果
数据宽度Half Word(16 位)Half Word用 Word 会一次搬 32 位,两个样本拼成一个数
地址自增外设地址不自增内存地址自增内存不自增则缓冲区只有一个位置被反复覆盖
传输长度——转换次数(不是字节数)填成字节数会多搬一倍,越界写内存

传输长度这一条尤其容易错。HAL_ADC_Start_DMA 的第三个参数是"要传输的转换数据个数",不是字节数,也不是数组长度乘以 2。单通道填 512 就是搬 512 个半字;三通道交错采集填 512 就代表 512 个样本点,其中每 3 个是一帧。

DMA 模式必须选 Circular。Normal 模式下 NDTR 减到 0 就停,ADC 还在转,但数据没人搬,表现就是"缓冲区前一半有数、后一半全是初值",或者干脆只更新了第一次。循环模式下 NDTR 归零后自动重装,整个流程不需要 CPU 干预。

还有一个只有 G4 才容易踩的点:CCM SRAM。G431 带一小块内核紧耦合内存,访问快、不占总线,很多人第一反应是把采样缓冲区放进去。但 DMA 控制器访问不到 CCM,缓冲区一旦被链接脚本分配到那个区,DMA 传输会报错误标志或者干脆静默不搬。缓冲区就老老实实放普通 SRAM。

4.2 单通道循环缓冲 + 半满中断做双缓冲

单通道连续采样最常见的结构是"环形缓冲 + 半满/全满两个中断"。DMA 从头写到尾触发一次全满中断,写到一半触发一次半满中断,两个中断把缓冲区切成前后两半,CPU 处理前半段时 DMA 正在写后半段,互不干扰。

/* adc_dma_single.c —— 单通道循环缓冲,半满/全满双缓冲 */ #include "main.h" #define ADC_BUF_LEN 512 #define ADC_HALF_LEN (ADC_BUF_LEN / 2) /* 必须放普通 SRAM,不能进 CCM;aligned(4) 让起始地址满足半字对齐 */ __attribute__((aligned(4))) static volatile uint16_t adc_buf[ADC_BUF_LEN]; static volatile uint8_t half_flag = 0; static volatile uint8_t full_flag = 0; void adc_dma_start(void) { half_flag = 0; full_flag = 0; /* 长度参数 = 转换数据个数,循环模式下自动重装 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { half_flag = 1; /* 前半段 [0, 256) 已就绪 */ } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { full_flag = 1; /* 后半段 [256, 512) 已就绪 */ } } /* 主循环里消费:每次只处理已经就绪的那一半 */ void adc_process(void) { uint32_t sum = 0; uint16_t i; if (half_flag) { half_flag = 0; for (i = 0; i < ADC_HALF_LEN; i++) sum += adc_buf[i]; g_avg_raw = sum / ADC_HALF_LEN; } if (full_flag) { full_flag = 0; sum = 0; for (i = ADC_HALF_LEN; i < ADC_BUF_LEN; i++) sum += adc_buf[i]; g_avg_raw = sum / ADC_HALF_LEN; } }

回调里的判断hadc->Instance == ADC1不能省。G431 有两个 ADC,如果工程里 ADC2 也在跑,两个都会进同一个回调,不区分就会把 ADC2 的数据当成 ADC1 的。

缓冲区长度和半满阈值的关系要理解清楚:DMA 的传输计数从 512 递减,减到 256 时触发半满,减到 0 时触发全满并重装回 512。所以半满中断对应的是"前半段写完了",此时 CPU 去读后半段是安全的,反之亦然。这个对应关系搞反了,读到的就是正在被覆盖的数据,波形上表现为周期性毛刺——这种毛刺非常像信号本身的问题,能排查很久。

处理函数里的求和要卡在中断之外。512 个半字的累加在 170MHz 的 M4 上大概几个微秒,放到中断里会拖长中断响应,高采样率下直接导致 DMA 覆盖未处理的数据。用标志位加主循环消费的结构,把耗时工作挪出去,是这类采集的标准骨架。

4.3 多通道扫描:序列、对齐与数据解包

多通道的配置和单通道差别不大,但数据布局变了:ADC 按 Rank 顺序依次转换,DMA 按转换顺序依次搬运,所以内存里是一个"帧"接一个"帧"地交错排列。三通道、每帧三样本,缓冲区里就是 CH1、CH2、CH3、CH1、CH2、CH3……

/* adc_dma_scan.c —— 三通道扫描,交错帧的解包 */ #include "main.h" #define ADC_CH_NUM 3 #define ADC_FRAME_NUM 64 #define ADC_SCAN_LEN (ADC_CH_NUM * ADC_FRAME_NUM) /* 总转换个数 = 192 */ static volatile uint16_t scan_buf[ADC_SCAN_LEN]; static volatile uint8_t scan_ready = 0; void adc_scan_start(void) { /* Rank1=IN1, Rank2=IN2, Rank3=IN3,顺序必须与下面的下标对应 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)scan_buf, ADC_SCAN_LEN); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { scan_ready = 1; } } /* 按通道拆出各自的平均值 */ void adc_scan_unpack(uint16_t *out_ch1, uint16_t *out_ch2, uint16_t *out_ch3) { uint32_t s1 = 0, s2 = 0, s3 = 0; uint16_t f; if (!scan_ready) return; scan_ready = 0; for (f = 0; f < ADC_FRAME_NUM; f++) { s1 += scan_buf[f * ADC_CH_NUM + 0]; /* 下标 0 对应 Rank1 */ s2 += scan_buf[f * ADC_CH_NUM + 1]; /* 下标 1 对应 Rank2 */ s3 += scan_buf[f * ADC_CH_NUM + 2]; /* 下标 2 对应 Rank3 */ } *out_ch1 = (uint16_t)(s1 / ADC_FRAME_NUM); *out_ch2 = (uint16_t)(s2 / ADC_FRAME_NUM); *out_ch3 = (uint16_t)(s3 / ADC_FRAME_NUM); }

解包函数里的下标和 CubeMX 里的 Rank 是一一绑定的,这是多通道错位问题的总根源。Rank 1 配了 IN3、Rank 2 配了 IN1,但代码里按 IN1、IN2、IN3 去解,得到的就是三路互相串台的数据,而且因为很多传感器输出范围相近,串台后数值看着"像那么回事",不容易第一时间发现。

判断有没有错位,最直接的办法是给三路分别接 GND、3.3V、1.65V 分压,跑一遍看三个通道的值是不是分别贴着 0、4095、2048。这个动作花两分钟,能省掉半天。

长度参数 192 也是按"总转换个数"填的,不是 64 帧、也不是 384 字节。填错的表现是缓冲区只填了一部分或者越界,前者看起来像采样率不够,后者可能悄悄改掉相邻变量。

5. 避坑现场:五个把 ADC 数据搞脏的具体原因

配置看着都对、代码也能编译,读数就是不对,这类问题在 ADC 上出现频率极高。下面五条是按"现象 → 原因 → 解决"整理的,基本都是我在调试中真实蹲过的。

5.1 读数在 0 和 4095 之间乱跳

现象:输入接的是一个稳定的分压信号,采样值却在两端反复横跳,偶尔还夹着几个中间值。

原因:绝大多数是采样时间不足,采样保持电容没充满就被读走,剩下的电荷量取决于上一次转换残留,于是读数在量程边缘摆。其次是地线问题——传感器和 MCU 没有共地,或者共地点在电源入口而不是 ADC 引脚附近,回流路径上那点压降正好落在模拟地上。

解决:先把采样时间从 6.5 周期提到 47.5 周期再测一遍,如果立刻稳定,就是源阻抗匹配问题,按 2.2 节的表重新选值;如果换了采样时间还是跳,用万用表量传感器地和 MCU 的 AGND 之间的电压,超过几个毫伏就得改接地。板上模拟地和数字地单点连接的位置,也要确认没被大电流路径穿过。

5.2 DMA 只搬了一次就停

现象:缓冲区最前面十几个数是正常的,后面全是 0 或初值,重新复位又能跑一次。

原因:三种可能。DMA 模式选了 Normal 而不是 Circular,NDTR 减到零就停;或者 HAL_ADC_Start_DMA 之前 ADC 已处于 BUSY 状态,启动直接失败但返回值没判;或者缓冲区被链接脚本分到了 CCM SRAM,DMA 根本访问不到那块内存。

解决:先看 CubeMX 里 DMA 的 Mode 是不是 Circular,这一条覆盖大半情况。然后检查 HAL_ADC_Start_DMA 的返回值,非 HAL_OK 就说明启动失败了,顺着往前找是不是有地方调了 HAL_ADC_Start 没 Stop。最后打开 map 文件确认缓冲区地址落在普通 SRAM 区间,而不是 0x10000000 开头的 CCM 区。

5.3 多通道数据错位

现象:三路信号分别接不同电压,读出来三路数值都"合理"但对不上号,或者某一路始终等于另一路。

原因:代码里解包的下标顺序和 CubeMX 里 Rank 的配置顺序不一致。另一种是 Scan Conversion Mode 没开,ADC 只转 Rank 1 就结束,但 DMA 按三个通道的长度在搬,后两个位置一直是陈旧数据。

解决:CubeMX 里把 Rank 的通道编号抄下来,和代码里的下标排成一列对照,一个一个核。用 GND / 3.3V / 一半分压三路已知输入做一次验证,比对着信号源猜快得多。Scan 模式这一项在配置完通道数之后回头再看一眼,很容易在改别的选项时被顺手关掉。

5.4 采样值随负载变化漂移

现象:空载时读数正常,一开外设(比如串口、SPI 或电机驱动)读数整体偏移,重新上电又恢复。

原因:VDDA 被拉低了。码值换算时用了固定的 3.3V,实际 VDDA 掉到 3.2V,所有读数按同一个比例偏大。另一种是 ADC 时钟取自 HCLK,程序里为了降功耗调过主频,ADC 时钟跟着变,采样时间对应的实际时长缩短,有效位数下降。

解决:把 VDDA 的实测值接到换算公式里,或者干脆用 3.3 节的 VREFINT 方案每次上电测一次。时钟方面,改动系统主频时顺手检查 ADC 的 CKMODE 配置,重要采样别依赖动态调频。

5.5 加了校准之后精度反而变差

现象:不校准的时候读数还算稳定,加了 HAL_ADCEx_Calibration_Start 之后零点偏了几个码值。

原因:一是在 ADC 转换进行中调用了校准函数,HAL 内部会关掉 ADC,校准值可能没真正写进去;二是校准只应在初始化阶段做一次,有人把它塞进循环里每次采样前都调;三是校准过程中 VDDA 不稳定,比如刚上电 LDO 还没建立好就校准了。

解决:校准放在初始化完成之后、第一次 Start 之前,全工程只调用一次。上电后延时几十毫秒等电源稳定再校准。校准完立刻采一次内部 VREFINT 通道,用读数反推 VDDA 并与万用表实测对照,偏差超过 1% 就说明校准流程有问题,别急着怀疑代码逻辑。

6. 从原始码值到可用电压:硬件过采样与一次上电自检

DMA 把数据搬进缓冲区只是把"采"这一步做完了。真正决定这套采集能不能用在产品里的,是后面两步:怎么在不牺牲采样率的前提下把有效位数提上去,以及怎么在两分钟内判断今天这块板子的模拟前端是不是正常的。

先说过采样。G4 的 ADC 自带硬件过采样单元,在 ADC_CFGR2 里配置,能把 N 次转换结果累加再右移,全程不占 CPU。等效位数提升是 log2(N)/2 位,16 倍过采样配 4 位右移,有效位数提升约 2 位;256 倍提升约 4 位,但吞吐量掉到 1/256。CubeMX 里的配置对应关系如下:

参数推荐值说明
Oversampling ModeEnabled打开硬件累加器
Oversampling Ratio16x累加 16 次,吞吐降到 1/16
Right Bit Shift416 = 2^4,右移 4 位还原量程
Oversampling Trigger ModeRegular每个触发脉冲完成一整组累加
Regular OversamplingEnabled规则组参与过采样

42.5MHz 时钟、47.5 周期采样时间下单次转换约 1.4µs,16 倍过采样后每 22µs 出一个结果,约 45kHz 有效输出——对绝大多数传感器信号绰绰有余。硬件过采样的好处是累加在模拟域完成,比软件连采 16 次再平均少了 15 次 CPU 搬运和中断开销。

超过 16 倍时,硬件过采样的收益开始被 ADC 本身的噪声底限制住,不如换成软件侧的中值加滑动平均组合。我在工程里常用的结构是:DMA 缓冲区每帧先经过一次 5 点中值滤掉脉冲干扰,再做 16 点滑动平均压随机噪声。中值那一步是关键,滑动平均对单个野值的抑制能力很弱,一个尖峰能把 16 个样本的平均值拉偏好几个码。

/* 中值 + 滑动平均:先剔野值,再压噪声 */ static uint16_t filter_step(uint16_t new_sample) { static uint16_t win[5] = {0}; static uint8_t idx = 0; uint16_t tmp[5], i, j, t; uint32_t sum = 0; win[idx] = new_sample; idx = (idx + 1) % 5; for (i = 0; i < 5; i++) tmp[i] = win[i]; for (i = 0; i < 4; i++) { /* 5 个元素,插入排序足够 */ for (j = i + 1; j < 5; j++) { if (tmp[j] < tmp[i]) { t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } for (i = 1; i < 4; i++) sum += tmp[i]; /* 去掉最小和最大,中间三个求平均 */ return (uint16_t)(sum / 3); }

再说自检。这套工程我给它加了一个上电自检流程,跑完才允许业务逻辑启动,具体三步:把 ADC 输入引脚短接到 GND,连续采 256 次,全部落在 0~3 码值内算通过;短接到 VREF 对应的电源轨,全部落在 4092~4095 内算通过;接一个 1.65V 分压,平均值落在 2040~2056 之间算通过。三步里任何一步不过,就在串口打印具体是哪一步、偏差多少,而不是丢一个笼统的"初始化失败"。

这三步能揪出的问题比想象中多:短接 GND 读数偏高,通常是地回流或者采样电容漏电;接电源轨读数不满量程,可能是采样时间不够或者内部采样开关有问题;中间点偏差大而两端正常,一般是分压电阻精度或者参考电压漂移。花两分钟走一遍,比后面在整机联调时怀疑算法强得多。

从那以后我每次开一块新的 G431 板子,都强制先把这三步自检跑一遍再往下写业务代码,哪怕前一天刚在另一块同型号板子上验证过。模拟前端的个体差异比数字部分大得多,同一批板子里总有一两块的分压电阻是 5% 精度的,靠"上次没问题"来推断这次也没问题,翻车概率不低。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 8:43:05

t3code实战:Next.js+tRPC+Prisma构建类型安全全栈应用

第一次注意到 t3code 这个词&#xff0c;是我在某次刷推的时候看到别人发的项目截图&#xff0c;代码目录里全是src/server/api/routers、prisma/schema.prisma这样的结构。当时第一反应是&#xff1a;又是什么新脚手架&#xff1f;点进去仔细看了一圈才意识到&#xff0c;这其…

作者头像 李华
网站建设 2026/10/26 8:42:23

T3 Stack全栈实践:从零构建类型安全的t3code项目

去年做一个内部工具项目时&#xff0c;我选型调研了一圈&#xff0c;最后还是回到了一套组合拳&#xff1a;TypeScript Next.js tRPC Tailwind&#xff0c;也就是常说的T3 Stack。当时照着社区里比较火的一个开源模板改造&#xff0c;发现这套东西最大的优势不是“快”&…

作者头像 李华
网站建设 2026/10/26 8:42:18

视频实时分析技术:架构设计与性能优化实践

1. 项目背景与行业需求视频内容分析技术正在经历从"有没有"到"好不好"的质变过程。作为某视频平台的技术负责人&#xff0c;我亲历了从早期简单的内容识别到现在实时流分析的完整技术演进。当前行业面临的核心痛点在于&#xff1a;传统批处理式分析无法满足…

作者头像 李华
网站建设 2026/10/26 8:41:58

新型电力系统规划与关键技术解析

1. 新型电力系统规划背景与战略意义2023年启动的"十五五"电网规划编制工作&#xff0c;标志着我国电力行业进入新的发展阶段。这次规划的核心任务是构建适应新能源占比逐渐提高的新型电力系统&#xff0c;其战略意义主要体现在三个维度&#xff1a;首先&#xff0c;在…

作者头像 李华
网站建设 2026/10/26 8:40:32

T3Code:统一前端工程化模板、规范与代码生成

从第一次带团队做前端项目规范化开始&#xff0c;我就一直在和“代码风格不统一、配置散落、新人上手慢”这类问题较劲。早期用脚手架生成了项目&#xff0c;但每个脚手架生成的 lint 规则、目录结构和提交规范都不一样&#xff1b;后续再手动补 ESLint、Prettier、Husky&#…

作者头像 李华
网站建设 2026/10/26 8:39:45

专利文档XML智能转换与核稿系统架构解析

1. 专利文档处理行业的痛点与变革需求在知识产权服务领域&#xff0c;专利文档的标准化处理一直是困扰从业者的核心难题。传统专利提交流程中&#xff0c;XML转档与核稿环节往往分散在不同部门&#xff0c;导致效率低下且错误率高。根据国际专利组织统计&#xff0c;约37%的专利…

作者头像 李华