☰
STM32G431 ADC直采与DMA:采样时间、多通道扫描和双缓冲避坑
2026/10/25 7:34:40 网站建设 项目流程

简介:面向STM32G431RBT6嵌入式开发者的ADC采集实践包,聚焦模拟信号从直接寄存器读取到DMA自动搬运的两种实现路径,适合已具备C语言与HAL库基础、希望打通多通道采样与低CPU占用数据链路的初中级工程师。包内共1254个文件,以676个C源文件、275个头文件、97个汇编启动文件及若干工程配置、编译输出和说明文本为主,压缩包约29.73MB,覆盖ADC1/ADC2多通道配置、单次与连续转换、DMA通道链接、转换完成中断通知以及采样率与传输长度优化等关键环节。已有1978人学习下载,说明该主题在传感器信号处理、电源监测和音频采集等实时场景中关注度较高。读者可据此对照理解直接读取与DMA搬运的差异,掌握缓冲区设置、中断配合和性能调优思路,并借助现成工程结构快速移植验证,减少从寄存器配置到稳定采集的调试成本。

1. 一块 G431 板子上的 ADC 抖动:直采和 DMA 到底差在哪

STM32G431RBT6 上最容易误判的一件事,是把「采到的数不准」算到取数方式头上。前阵子拆一块用 G431 做的小信号采集板,串口打出来 ADC 读数在 ±8 LSB 之间跳,换成 DMA 搬运,曲线看着顺了,均值纹丝不动。直采和 DMA 解决的是两个不同问题:直采解决「CPU 要不要陪着等」,DMA 解决「连续采样搬不搬得动」,两条路径都不解决模拟前端的偏移和源阻抗失配。

这篇按落地顺序拆:单通道轮询和中断怎么写、多通道扫描的序列怎么排、DMA 循环加半传输怎么拿到不撕裂的数据块、采样时间和源阻抗怎么算、以及我在 G431 的 ADC 上真踩过的五个坑。手上有块 G431 的板子、能点灯能串口打印,就可以跟着走一遍。

2. 直采链路:轮询、中断与扫描模式的落地写法

直采指的是 CPU 亲自去 ADC 数据寄存器里把结果取回来。它最大的价值是简单可控——不占 DMA 通道、不用担心缓冲撕裂、单次测量写三行代码就能跑。代价也直白:从启动转换到读走结果这段时间,CPU 要么死等,要么被中断打断。

我一般这样分:低频、单点、事件驱动的量(按键检测、电池电压巡检、NTC 温度)用直采;连续波形、多通道同步、要对数据做频谱或统计的,直接上 DMA。判断标准不是「哪个更高级」,而是「一秒采多少次」。低于 1 kHz 的采样需求,硬上 DMA 只会给自己加一层调试负担。

2.1 先把 ADC 时基算清楚:时钟源、采样时间与转换周期

G431 的 ADC 内核时钟不是自动跟着主频走的,得单独选源和分频。常见做法是在时钟树里把 ADC12 的输入挂到 AHB 分频或 PLLP 上,170 MHz 主频下取 AHB/4,也就是 42.5 MHz。这个值决定了后面所有时间参数,先定死它。

一次转换的总周期数 = 采样周期 + 转换周期。12 位分辨率下转换周期固定 12.5 个 ADC 时钟,采样周期从下表里挑:

采样时间设置采样周期12 位总周期@42.5 MHz 单次耗时
ADC_SAMPLETIME_2CYCLES_52.515353 ns
ADC_SAMPLETIME_6CYCLES_56.519447 ns
ADC_SAMPLETIME_12CYCLES_512.525588 ns
ADC_SAMPLETIME_24CYCLES_524.537871 ns
ADC_SAMPLETIME_47CYCLES_547.5601.41 µs
ADC_SAMPLETIME_92CYCLES_592.51052.47 µs
ADC_SAMPLETIME_247CYCLES_5247.52606.12 µs

采样时间不是「填大点更保险」这么简单:填太大会把吞吐压下去,填太小则采样电容充不满,读数系统性偏小,而且偏得很有规律,容易被误判成「分压电阻不准」。2.5 个周期只适合源阻抗极低(运放直出、几十欧以内)的场合,分压电阻、热敏电阻这类源,起步给 24.5 个周期。

2.2 单通道直采:轮询版与中断版

先用轮询把通道跑通,确认引脚、时钟、通道号都对,再谈别的。下面这份初始化是我在 G431 上常用的骨架:

/* adc_direct.c —— 单通道、软件触发、轮询读取 */ static ADC_HandleTypeDef hadc1; void adc_direct_init(void) { hadc1.Instance = ADC1; /* 时钟源与分频枚举名不同 HAL 版本略有差异,以工程里 stm32g4xx_hal_adc.h 的定义为准,这里取 AHB/4 = 42.5 MHz */ hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 单通道不扫描 */ hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 每次转换都置 EOC */ hadc1.Init.LowPowerAutoWait = DISABLE; /* 打开会拖慢连续转换 */ hadc1.Init.ContinuousConvMode = DISABLE; /* 单次模式 */ hadc1.Init.NbrOfConversion = 1; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */ hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; hadc1.Init.OversamplingMode = DISABLE; if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); } ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_1; /* PA0 = ADC1_IN1 */ sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_24CYCLES_5; sConfig.SingleDiff = ADC_SINGLE_ENDED; sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) { Error_Handler(); } /* 单端输入必须显式指定 ADC_SINGLE_ENDED,漏了会返回 HAL_ERROR */ if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED) != HAL_OK) { Error_Handler(); } } uint16_t adc_read_once(uint32_t timeout_ms) { uint16_t v = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, timeout_ms) == HAL_OK) { v = (uint16_t)HAL_ADC_GetValue(&hadc1); /* 必须在 Stop 之前取 */ } HAL_ADC_Stop(&hadc1); return v; }

几个参数值得单独说。EOCSelection选ADC_EOC_SINGLE_CONV时,每完成一次转换就置一次 EOC,适合单通道;选ADC_EOC_SEQ_CONV则要等整个序列转换完才置位,序列长度为 1 时两者行为一样,但多通道场景下差别很大。ContinuousConvMode关掉之后必须靠HAL_ADC_Start触发,这是软件触发单次采样的标准姿势。

轮询版单次耗时 = 一次转换时间 + HAL 调用的开销,实际在两三微秒到十几微秒之间(受编译优化和 HAL 内部状态机影响)。做 1 kHz 巡检完全够用,做 100 kHz 波形就别指望了。

中断版把等待交给中断,CPU 主循环该干什么干什么:

volatile uint16_t g_adc_val; volatile uint8_t g_adc_done; /* 单次转换完成回调,序列模式下这个回调每次 EOC 都会进 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { g_adc_val = (uint16_t)HAL_ADC_GetValue(hadc); g_adc_done = 1; } } /* 启动时用带 IT 的版本,转换结束会自动回调 */ void adc_start_it(void) { g_adc_done = 0; HAL_ADC_Start_IT(&hadc1); }

注意中断版必须把ContinuousConvMode和触发方式想清楚:如果开成连续转换 + 单次启动,回调会以最高速率反复进入,几微秒一次,中断开销反而比轮询还大。中断版适合「不定时来一发」的场景,比如按键按下后读一次分压,不是用来做高速采样的。

2.3 多通道扫描:Rank 顺序、缓冲索引与间断模式

多通道靠扫描模式实现:ScanConvMode = ENABLE,NbrOfConversion设成通道数,每个通道用Rank指定转换顺序,Rank 1 最先转。G431 的规则组最多 16 个 Rank,够绝大多数场合用。

/* 三个通道:IN1 / IN2 / IN11,顺序由 Rank 决定 */ static void adc_scan_config(ADC_HandleTypeDef *h) { ADC_ChannelConfTypeDef c = {0}; c.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; c.SingleDiff = ADC_SINGLE_ENDED; c.OffsetNumber = ADC_OFFSET_NONE; c.Channel = ADC_CHANNEL_1; c.Rank = ADC_REGULAR_RANK_1; HAL_ADC_ConfigChannel(h, &c); c.Channel = ADC_CHANNEL_2; c.Rank = ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(h, &c); c.Channel = ADC_CHANNEL_11; c.Rank = ADC_REGULAR_RANK_3; HAL_ADC_ConfigChannel(h, &c); }

顺序一旦定下来,后面所有代码都得按这个顺序解释数据:DMA 缓冲区buf[0]是 Rank 1,buf[1]是 Rank 2。我见过最典型的翻车是把 Rank 写反了,然后在应用层用「第一个是电池电压、第二个是温度」去解读,结果温度曲线跟着电池电压走,排查了半天硬件。Rank 与缓冲区索引的对应关系最好写成宏或者枚举,别靠记忆。

另外一个容易忽略的参数是间断模式(Discontinuous)。打开之后,一次触发只转换 N 个通道然后停下来,要再触发一次才继续。它适合「分组采样」的场景,比如温度不用每次都采、隔几次采一次。多通道 + DMA 连续搬运的组合里,这个开关必须是关的,否则 DMA 每轮只能收到一部分数据,长度对不上。

通道数确定之后,单轮序列的总耗时 = 各通道转换时间之和。三个通道各 47.5 个采样周期,在 42.5 MHz 下就是 3 × 1.41 µs ≈ 4.2 µs,也就是纯扫描的帧率上限约 236 kHz。这个数决定了后面 DMA 缓冲区该开多大、中断该多快进一次。

3. DMA 搬运:让 ADC 自己往内存里写

DMA 这条路的本质是把「读 DR 寄存器」这件重复劳动从 CPU 手上拿走。ADC 每完成一次转换就发一个 DMA 请求,DMA 控制器把 DR 里的半字搬到内存,地址自增,计数减一,全程不惊动内核。对 G431 来说,ADC1 的请求默认挂在 DMA1 的通道 1 上,中间还过一层 DMAMUX,配置工具里一选就行,手写代码时注意别把请求号填错。

真正需要想清楚的是三件事:搬运长度和转换次数怎么对齐、缓冲区会不会在 CPU 读的时候被覆盖、溢出(OVR)之后怎么恢复。

3.1 请求映射与循环模式:一次配置跑到底

最省心的组合是「ADC 连续转换 + DMA 循环模式」。ADC 不停转,DMA 不停搬,指针绕一圈回到起点,CPU 随便什么时候读都是最近的一批数据。

/* adc_dma.c —— 三通道扫描 + DMA 循环搬运 */ #define ADC_CH_NUM 3 #define ADC_BLK 128 /* 每通道每块的点数 */ /* 双半区:前 128*3 个点是一块,后 128*3 个点是另一块 */ static uint16_t adc_dma_buf[ADC_BLK * ADC_CH_NUM * 2]; void adc_dma_start(void) { /* 长度必须是「转换次数」,不是字节数: 3 个通道 = 3 次转换 = 3 个半字 */ if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_BLK * ADC_CH_NUM * 2) != HAL_OK) { Error_Handler(); } }

HAL_ADC_Start_DMA第三个参数是最容易被误解的地方。传的是转换次数,HAL 内部会自己乘上数据宽度。三通道、12 位、右对齐,每次转换一个半字,所以传 3 表示搬 3 个半字。有人按字节数传 6,结果 DMA 搬了两轮才凑够,数据全错位。

循环模式的配置落在 DMA 句柄上:外设地址不自增、内存地址自增、半字宽度、模式 Circular、优先级中等偏高。ADC 侧则要求ContinuousConvMode = ENABLE且Overrun = ADC_OVR_DATA_OVERWRITTEN。溢出策略选覆盖,是因为连续采样场景下丢一个点无伤大雅,但 DMA 停掉就全断了;选ADC_OVR_DATA_PRESERVED的话,一旦溢出 HAL 会进错误回调把 DMA 关掉,看起来就像「采了几百个点之后死了」。

3.2 半传输中断做双缓冲:拿到不撕裂的数据块

循环 DMA 有个天然问题:CPU 去读缓冲区的时候,DMA 可能正写到一半,读出来的块前半段是新的、后半段是旧的。解决办法是把缓冲区切成两半,DMA 写前半区时 CPU 读后半区,靠半传输中断(HT)和传输完成中断(TC)切换。

static volatile uint16_t *g_ready_ptr; /* 指向「刚填满、可以读」的半区 */ static volatile uint8_t g_ready_flag; /* 前半区写满:此时 DMA 正在写后半区 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { g_ready_ptr = &adc_dma_buf[0]; g_ready_flag = 1; } /* 后半区写满:此时 DMA 已经绕回前半区 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { g_ready_ptr = &adc_dma_buf[ADC_BLK * ADC_CH_NUM]; g_ready_flag = 1; } /* 主循环里处理,注意处理时间必须小于半区填满的时间 */ void app_poll(void) { if (!g_ready_flag) return; g_ready_flag = 0; const uint16_t *p = (const uint16_t *)g_ready_ptr; /* 先取指针快照 */ uint32_t sum[ADC_CH_NUM] = {0}; for (uint32_t i = 0; i < ADC_BLK; i++) { for (uint32_t ch = 0; ch < ADC_CH_NUM; ch++) { sum[ch] += p[i * ADC_CH_NUM + ch]; } } /* sum[] 就是这一块的累加值,除以 ADC_BLK 得均值 */ }

中断里只做一件事:记指针、置标志。千万别在回调里做memcpy或者浮点运算,半传输中断的频率是「采样率 / (2 × 半区点数)」,100 kHz 采样、128 点一块的情况下是 390 Hz,看着不高,但一旦你在里面做几百次乘加,中断执行时间就会吃掉主循环,反过来拖慢其它任务。

g_ready_ptr要加volatile,因为它会在中断里被改写、在主循环里被读取,编译器不知道这件事,不加优化等级一高就把判断优化没了——这是嵌入式里最常见的一类玄学问题。

还有一个边界要留意:主循环的处理时间必须小于半区填满的时间。100 kHz 采样率、128 点一块、3 通道,半区填满需要 128 × 3 / 100k = 3.84 ms,处理代码必须在这个时间内跑完,否则指针会被下一次中断覆盖,读到的数据块就串了。

3.3 采样率与吞吐估算:先把账算在纸上

DMA 不会凭空提高 ADC 的转换速度,它只是让 CPU 不参与搬运。真实的上限仍然是转换时间,按 2.1 节的表,@42.5 MHz 下单通道的极限是这样:

采样时间总周期单次耗时单通道极限速率3 通道扫描帧率
2.515353 ns2.83 Msps943 kfps
6.519447 ns2.24 Msps746 kfps
12.525588 ns1.70 Msps566 kfps
24.537871 ns1.15 Msps382 kfps
47.5601.41 µs708 ksps236 kfps

注意最后两列不是「一定能达到」的,还得看 DMA 带宽和内存访问。100 kHz × 3 通道 × 2 字节 = 600 kB/s,对 AHB 总线来说毫无压力,但如果同一总线上还挂着别的 DMA 搬大量数据,就会有总线仲裁开销,表现为偶发的数据延迟而非丢数。

如果采样率要求是「精确等间隔」而不是「尽量快」,那就别用连续转换,改成定时器触发:

/* TIM3 更新事件 → TRGO → ADC1 外部触发,采样率 = 170MHz / (PSC+1) / (ARR+1) */ void adc_timer_trigger_init(void) { htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1699; /* 170 MHz / 1700 = 100 kHz */ htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(&htim3); TIM_MasterConfigTypeDef mc = {0}; mc.MasterOutputTrigger = TIM_TRGO_UPDATE; mc.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim3, &mc); /* ADC 侧改成外部触发,连续转换关掉,让定时器决定节奏 */ hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; HAL_ADC_Init(&hadc1); HAL_TIM_Base_Start(&htim3); }

定时器触发的价值在于抖动极小。软件触发的采样间隔受中断延迟、主循环调度影响,做 FFT 时频谱会糊;硬件触发的间隔由定时器硬件保证,抖动在纳秒级。做音频、振动、电流环这类需要频谱分析的场合,定时器触发是标配,不是可选项。

顺带提一句双 ADC。G431 有两路 ADC,可以在双规则同步模式下让两路同时采样同一个时刻的信号,再配合 DMA 把两个结果拼成一个 32 位字搬走。它解决的是「两路信号之间要有严格相位关系」的问题,普通多通道扫描做不到这一点,因为扫描是分时进行的,通道之间天然差着几个微秒。

4. 避坑与排查:G431 的 ADC 上最容易翻车的五件事

下面这五条是我在 G431 上实际踩过的,按「现象 → 原因 → 解决」写,遇到问题时可以直接对号入座。

4.1 读数整体偏小且越换大阻值越明显:采样时间不够

现象:分压电阻测电池电压,万用表量出来 12.4 V,ADC 换算出来 11.9 V,误差稳定在 4% 左右,换成阻值更大的分压网络误差更大。

原因:采样时间设成了 2.5 个周期,ADC 内部采样电容(G4 手册给的 C_ADC 在 5 pF 量级)没有充足。采样保持电路本质是个 RC 充电过程,源阻抗越大,时间常数越大,固定的采样窗口内就充不到目标电压。

解决:按源阻抗重新算采样时间,或者加一级电压跟随器把源阻抗压到几十欧。分压电阻在几十 kΩ 量级的场合,采样时间直接给到 47.5 甚至 92.5 个周期,误差能回到 1 LSB 以内。加运放跟随是最彻底的办法,代价是 BOM 多一颗料和一点失调。

4.2 DMA 只搬了第一轮就再也不更新:模式或溢出把通道关了

现象:串口打印adc_dma_buf的前几个点,数值正常,但过了几秒再看还是同一批数,一点没变。

原因:两个高发点。一是 DMA 配成了 Normal 模式,一轮搬完就停了,而 ADC 还在连续转换;二是Overrun设成了ADC_OVR_DATA_PRESERVED,某次 CPU 处理时间偏长导致 OVR 置位,HAL 的错误回调把 DMA 给关了。

解决:DMA 模式必须配 Circular;溢出策略改成ADC_OVR_DATA_OVERWRITTEN,让新数据覆盖旧数据而不是停机。同时在HAL_ADC_ErrorCallback里打一个计数器,排查阶段能立刻看出是不是溢出导致的。

4.3 多通道数据串位:长度和 Rank 没对齐

现象:三通道采样,温度通道的曲线跟着电池电压一起波动,明显是数据错位。

原因:NbrOfConversion设成了 3,但HAL_ADC_Start_DMA的长度传成了 6(按字节算的),DMA 每轮要搬 6 个半字才凑满一块,于是第二块的第一个点实际上是第一块的第 7 个点,整体错位一路传下去。另一种情况是 Rank 顺序和代码里的索引映射写反了。

解决:长度统一按「转换次数」给;Rank 与缓冲区索引的对应关系写成宏或者枚举,别在应用层硬编码数字。排查时先把所有通道接到同一个电压(比如都接 VDDA/2 的分压),看缓冲区里是不是三个一样的值,这样可以立刻区分是顺序错还是数据错。

4.4 校准返回错误或者校准后反而漂:顺序和状态没管好

现象:HAL_ADCEx_Calibration_Start返回HAL_ERROR,或者校准之后读数比校准前还偏。

原因:校准必须在 ADC 使能(ADEN 置位、内部稳压器稳定)之后、启动任何转换之前执行。如果先HAL_ADC_Start再校准,或者中途HAL_ADC_Stop之后没有重新初始化就再次校准,都会失败。另外单端输入必须显式传ADC_SINGLE_ENDED,传错成差分模式也会返回错误。

解决:把校准放在HAL_ADC_Init之后、第一次HAL_ADC_Start*之前,顺序固定;需要重新校准时先HAL_ADC_Stop,再HAL_ADC_Init,再校准。校准时序错乱这类问题没有报错提示,属于典型的黑匣子,建议在初始化函数里加一句串口打印,确认每次上电都走到校准那一步。

4.5 直采读到的永远是上一次的值:取数时机错了

现象:轮询单次采样,输入电压改了,串口打出来的值要下一次才变,等于永远慢一拍。

原因:HAL_ADC_GetValue在HAL_ADC_Stop之后调用,或者EOCSelection配成了ADC_EOC_SEQ_CONV而序列里只有一个通道,导致PollForConversion提前返回、DR 里还是旧数据。

解决:GetValue必须在Stop之前取;单通道用ADC_EOC_SINGLE_CONV。更底层的写法是直接读ADC1->DR,读操作本身会清 EOC,比 HAL 的层层判断快,也少一层状态机。调试阶段可以用 GPIO 翻转配合示波器看「启动 → 转换完成 → 取值」三个时刻的间隔,比在串口上猜快得多。

5. 把有效位数榨出来:校准、过采样与模拟前端

12 位是分辨率,不是精度。G431 的 ADC 在良好前端下能跑到 11 位左右的 ENOB,前端没做好可能只有 8 位。这一章讲三个能实际提精度的动作。

5.1 用出厂校准值反推 VDDA,顺手把温度读出来

芯片内部有个参考电压 VREFINT,出厂时在 3.0 V 供电、25 ℃ 条件下测过并存进电子签名区。读它的原始码值,配合出厂校准值,就能反推当前实际的 VDDA——这比假设「VDDA 一定是 3.3 V」靠谱得多,尤其在用电池或 USB 供电、电压本来就在飘的场合。

/* 地址以数据手册 "Device electronic signature" 表为准 */ #define VREFINT_CAL_ADDR ((const uint16_t *)0x1FFF75AAUL) #define VREFINT_CAL_VREF 3000U /* 出厂校准时的 VDDA,单位 mV */ /* VREFINT 通道采样时间必须 >= 4 us,否则读数偏低 */ static uint16_t adc_read_vrefint_raw(void) { ADC_ChannelConfTypeDef c = {0}; c.Channel = ADC_CHANNEL_VREFINT; c.Rank = ADC_REGULAR_RANK_1; c.SamplingTime = ADC_SAMPLETIME_247CYCLES_5; /* 6.1 us @42.5MHz */ c.SingleDiff = ADC_SINGLE_ENDED; c.OffsetNumber = ADC_OFFSET_NONE; HAL_ADC_ConfigChannel(&hadc1, &c); return adc_read_once(10); } float vdda_mv(void) { uint32_t raw = adc_read_vrefint_raw(); if (raw == 0) return 0.0f; /* 防止除零,也顺便暴露配置错 */ return (float)VREFINT_CAL_VREF * (float)(*VREFINT_CAL_ADDR) / (float)raw; }

拿到 VDDA 之后,所有通道的换算公式都从raw * 3.3f / 4095换成raw * vdda_mv() / 1000.0f / 4095。室温下这一改,测量的一致性会明显好转,尤其是 USB 供电、线损压降大的板子。

温度通道同理,用TS_CAL1/TS_CAL2两个出厂点和对应的温度值做线性插值。G4 的手册给的校准点是 30 ℃ 和 130 ℃,采样时间同样要求不小于 4 µs。片内温度传感器测的是结温,不是环境温度,别拿它当室温计用。

5.2 硬件过采样:用 16 倍换 2 位,代价是什么

G431 的 ADC 自带硬件过采样,一次结果由多次转换累加后再右移得到,全程不占 CPU。配置上只有四个参数:

hadc1.Init.OversamplingMode = ENABLE; hadc1.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_16; /* 16 次累加 */ hadc1.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_2; /* 右移 2 位 */ hadc1.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; hadc1.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_CONTINUED_MODE;

有效位数 = 12 + log2(16) - 2 = 14 位。这个式子值得记住:累加 N 次能多出 log2(N) 位,右移多少位就还回去多少位。想要满 16 位,比例给 16、右移 0;想要 14 位且数值不溢出uint16_t,就是上面这组。缓冲区类型保持不变(半字)即可,不用改成 32 位。

代价有两层。第一是吞吐:16 次转换才出一个结果,采样率直接降到 1/16,100 kHz 变 6.25 kHz。第二是效果有前提——过采样压的是白噪声,如果干扰是 50 Hz 工频或者开关电源的固定频率纹波,过采样对它几乎无效,该用工频抑制和屏蔽还是得用。

实际用下来,过采样在「源阻抗低、信号带内噪声随机」的场合效果最明显,比如用运放缓冲过的电桥信号,能把有效位数从 10.5 提到 12 以上。反过来,高阻分压加长线引入的干扰,过采样只会把噪声平摊,读数该跳还是跳。

5.3 模拟前端:算一下源阻抗的上限

采样保持本质是 RC 充电,要求 k 个时间常数内充到 0.5 LSB 以内,k 与位数有关:

k = ln( <p> <a href="https://download.csdn.net/download/qq_62725929/88837658" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询