news 2026/10/26 8:43:20

STM32G431 ADC采样:轮询与DMA多通道采集配置实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32G431 ADC采样:轮询与DMA多通道采集配置实战

简介:面向STM32G4系列初学者与嵌入式开发者的ADC采集实践包,围绕STM32G431RBT6展开,重点对比ADC直接读取与DMA搬运两种方式,并覆盖多通道、连续转换、中断配合等常见配置场景。压缩包共1254个文件、约29.73MB,以676个C源文件、275个头文件和97个汇编文件为主体,辅以Keil工程、IAR链接配置、启动文件、数学库及少量说明文本,便于直接编译验证与移植参考。已有1978人学习下载。资源内含ADC1直接采集与ADC2 DMA采集的完整工程线索,涉及HAL库初始化、DMA通道链接、采样时间与分辨率设置、转换完成中断处理,以及HRTIM、数学库等外设与算法支撑文件,适合用来对照理解寄存器与HAL配置差异,排查数据错位、采样抖动和传输效率问题,也可作为传感器信号采集、电源监测等项目的底层模板。

1. 一块 G431 板子上的 ADC 采样:直接读和 DMA 搬,差的不只是一行代码

前阵子帮人看一块 STM32G431RBT6 的板子,现象很典型:单独接电位器测电压,读数纹丝不动;一旦挂上四路电流采样,主循环里的显示刷新就发涩,采样值还偶尔跳一下。拆开看,两处代码都是HAL_ADC_Start加PollForConversion的轮询直采,四路通道挨个排队读,一次扫描把 CPU 切成了四段,谁也别想好好跑。G431 的 ADC1/ADC2 是 12 位逐次逼近型,带硬件过采样和 DMA 请求线,搬运方式选错,精度和实时性会一起塌。直接采集胜在简单、可控、随时能打断点看变量,适合低频单通道和调试期验证;DMA 采集胜在不占 CPU、采样点等间隔,适合多通道扫描和固定采样率。下面按时钟与采样窗口、轮询实现、DMA 配置、现场踩坑的顺序拆,代码都能直接编译进工程。

2. ADC 时钟与采样窗口:12 位读数先立住,再谈搬运

采样值不准,八成不是算法问题,是时钟和采样窗口没配对。G4 这一代 ADC 的输入时钟上限比主频低不少,而采样时间又直接决定采样保持电容能不能被外部信号喂饱。这两件事定不下来,后面无论用轮询还是 DMA,搬回来的都是一堆看着像模像样的错数。

2.1 ADC 时钟只有源选择,没有独立分频器

很多人在 CubeMX 里找 ADC 预分频,找不到就以为漏配了。G4 的 ADC 时钟树里确实没有独立分频器,只有一个源选择开关,位在 RCC 的 ADC12CCIPR 里,可选 SYSCLK、PLLP、HSI16 三路。SYSCLK 跑 170 MHz 时直接选它,ADC 时钟就是 170 MHz,远超 12 位模式允许的上限,转换结果会莫名其妙地漂。所以要么把 PLLP 单独算一个合适频率出来,要么退回 HSI16 图个省事。

PLLP 的频率不是独立设的,它来自 VCO:f_VCO = f_in / PLLM × PLLN,然后f_PLLP = f_VCO / PLLP,PLLP 只能取 2、4、6、8。以 24 MHz 晶振、目标主频 170 MHz 为例,PLLM=6、PLLN=85 得到 340 MHz VCO,PLLR 取 2 得到 170 MHz 主频,PLLP 取 6 得到 56.67 MHz 给 ADC,正好卡在上限以内。

/* adc_clock.c —— 单独把 PLLP 留给 ADC,目标 56.67 MHz */ static void adc_clock_init(void) { RCC_OscInitTypeDef osc = {0}; RCC_PeriphCLKInitTypeDef pclk = {0}; /* 24 MHz 晶振:24/6 = 4 MHz 鉴相,×85 = 340 MHz VCO PLLP=6 → 56.67 MHz 给 ADC;PLLR=2 → 170 MHz 给 SYSCLK */ osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLM = 6; osc.PLL.PLLN = 85; osc.PLL.PLLP = RCC_PLLP_DIV6; osc.PLL.PLLQ = RCC_PLLQ_DIV8; osc.PLL.PLLR = RCC_PLLR_DIV2; if (HAL_RCC_OscConfig(&osc) != HAL_OK) { Error_Handler(); } pclk.PeriphClockSelection = RCC_PERIPHCLK_ADC12; pclk.Adc12ClockSelection = RCC_ADC12CLKSOURCE_PLLP; if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { Error_Handler(); } }

这段代码的关键在最后两行:RCC_PERIPHCLK_ADC12告诉 HAL 要改的是 ADC1/ADC2 共用的那路时钟,RCC_ADC12CLKSOURCE_PLLP把源切到 PLLP。PLLM/PLLN/PLLP 三个值必须一起改,只动 PLLP 会连带把主频和 USB 时钟搞乱。改完时钟记得重新调用HAL_ADC_Init,因为 G4 的 HAL 会在初始化时按当前 ADC 时钟频率去写 CR 寄存器里的 BOOST 位,时钟变了 BOOST 没跟着变,高速下转换线性度会明显变差,这个坑后面还会单独说。

不同时钟源对应的转换时间差别很大,选型时直接看表比算半天快:

ADC 时钟源频率最短转换(2.5+12.5 周期)47.5+12.5 周期理论最大采样率
PLLP56.67 MHz265 ns1.06 µs约 944 ksps
SYSCLK 直连170 MHz88 ns353 ns超规格,不可用
HSI1616 MHz938 ns3.75 µs约 267 ksps

表里 SYSCLK 那一行是拿来对照的,别真照着配。HSI16 那一行看着慢,但胜在不用配 PLL,做低速率电池电压监测足够,而且省掉一个晶振依赖,量产板子少一个失效点。

2.2 采样时间:47.5 个周期不是拍脑袋定的

采样时间在 HAL 里体现为ADC_SAMPLETIME_47CYCLES_5这类宏,单位是 ADC 时钟周期。它决定内部采样保持电容的充电时长,电容典型值在 5 pF 量级,外部信号经过源阻抗给它充电,充不满就采样,读数就系统性偏低。用一阶 RC 估算:要保证 13 位精度,误差要压到 1/8192 以下,需要约 9 个时间常数,也就是t_smp ≥ 9 × R_source × C。

反过来推,在 56.67 MHz 下 47.5 个周期是 838 ns,允许的最大源阻抗约838 ns / (9 × 5 pF) ≈ 18 kΩ。如果前端是 100 kΩ 的分压电阻,47.5 周期根本不够,读数会明显偏低,而且偏得很有规律,容易误判成基准电压不准。这时候要么加到 247.5 周期,要么在运放后面加一级跟随器把源阻抗打下来。

源阻抗建议采样时间47.5 周期下的误差量级
< 1 kΩ2.5 ~ 12.5 周期可忽略
1 ~ 10 kΩ24.5 ~ 47.5 周期小于 1 LSB
10 ~ 50 kΩ92.5 ~ 247.5 周期几十 LSB,必须加跟随
> 50 kΩ不建议直连读数不可信

我一般先用 47.5 周期打底,把板子跑起来,拿示波器或逻辑分析仪盯采样点附近的引脚电压,看有没有明显塌陷,塌了就往上加一档。这个动作比反复调校准参数管用得多。

2.3 轮询和 DMA 的分界线在哪

判断标准其实就三条。通道数只有一路、采样间隔在毫秒级、CPU 余量充足,轮询完全够用,代码短到可以手写。多通道扫描,或者采样率乘上单次转换时间占掉的 CPU 比例超过 2% 到 3%,就该考虑 DMA。采样时刻要求等间隔、不能被其他中断打断,那就必须上定时器触发加 DMA,轮询做不到这一点。

拿四通道 10 kHz 举例:每轮转换4 × 1.06 µs ≈ 4.2 µs,一秒 10k 轮就是 42 ms,占 CPU 约 4.2%。单看数字不算高,但它是被切成四段 1 µs 左右的阻塞,夹在中断和主循环之间,抖动会传导到整个系统。同样的活儿交给 DMA,CPU 只在半满和全满回调里出现两次,每次几十微秒处理一批数据,性质完全不同。

3. 轮询直采落地:HAL 阻塞式读数的写法与三个必调参数

轮询写法是我调试新板子的第一段代码,因为它足够笨,笨到出问题时你能一眼看出是哪一步。等 ADC 稳定、通道映射、基准电压这些基础项都验证过了,再换 DMA 才有意义。上来就配 DMA,出了问题你分不清是通道配错、缓冲区错位还是搬运没启动。

3.1 单通道轮询的最小可用代码

CubeMX 里先把 ADC1 的基础参数配好:ContinuousConvMode = DISABLE、DiscontinuousConvMode = DISABLE、ScanConvMode = DISABLE、NbrOfConversion = 1、ExternalTrigConv = ADC_SOFTWARE_START、DataAlign = ADC_DATAALIGN_RIGHT。这几个值决定了每次HAL_ADC_Start只转换一次就停,转换完把 EOC 标志置起来等你去读。

/* adc_poll.c —— 单通道轮询采样,读一次约 1.1 µs,失败返回 0xFFFF */ #include "stm32g4xx_hal.h" extern ADC_HandleTypeDef hadc1; #define ADC_VREF_MV 3300u #define ADC_FULL_SCALE 4095u uint16_t adc_read_once(uint32_t channel) { ADC_ChannelConfTypeDef cfg = {0}; cfg.Channel = channel; cfg.Rank = ADC_REGULAR_RANK_1; cfg.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; /* 47.5 + 12.5 = 60 个 ADC 周期 */ cfg.SingleDiff = ADC_SINGLE_ENDED; cfg.OffsetNumber = ADC_OFFSET_NONE; if (HAL_ADC_ConfigChannel(&hadc1, &cfg) != HAL_OK) { return 0xFFFFu; /* 0xFFFF 是哨兵值,正常 12 位读数到不了 */ } if (HAL_ADC_Start(&hadc1) != HAL_OK) { return 0xFFFFu; } /* 超时给 2 ms,单次转换只要 1.06 µs,留足余量又能兜住异常 */ if (HAL_ADC_PollForConversion(&hadc1, 2u) != HAL_OK) { HAL_ADC_Stop(&hadc1); return 0xFFFFu; } return (uint16_t)HAL_ADC_GetValue(&hadc1); } /* 把原始值换成毫伏,整数运算足够,避免引入浮点库 */ uint32_t adc_to_mv(uint16_t raw) { return ((uint32_t)raw * ADC_VREF_MV) / ADC_FULL_SCALE; }

HAL_ADC_ConfigChannel每次调用都会重写通道相关的几个寄存器,开销在几十个时钟周期,相对 1 µs 的转换时间可以忽略,换来的是调用方不用关心 Rank 顺序。HAL_ADC_Start里有一段等待 ADC 使能稳定的空转,第一次调用会比后续慢几十微秒,这是正常现象,不是代码写错了。HAL_ADC_Stop放在超时分支里是必要的,否则 ADC 停在使能状态,下一次 Start 的行为会依赖 HAL 内部状态机,容易出玄学问题。

参数上有三点值得单独说。Rank在单通道场景固定写 1,多通道扫描时必须从 1 开始递增,写重复了会出现多个通道抢同一个结果寄存器。SamplingTime按 2.2 节的表选,宁可长一点也别短。超时值单位是毫秒,别写 2 想表达 2 微秒,那是两回事,写小了会频繁进超时分支。

3.2 三个参数的取值逻辑

超时值怎么定?用单次转换时间乘 10 再加 1 ms 兜底,是最省心的做法。1.06 µs 的场景给 2 ms,3.75 µs 的场景给 5 ms,都能覆盖掉中断抢占带来的延迟。

采样时间怎么定?先估源阻抗,查表取一档,跑起来再看数据稳不稳。稳定但不准,大概率是基准或校准问题;不稳且跳变大,才是采样时间不够或者地线噪声。

Rank 什么时候会出问题?多通道扫描但忘了把ScanConvMode打开时,HAL 只会转换 Rank 最低的那一路,其余通道读到的都是同一份数据,现象是四路读数一模一样,很多人第一反应是硬件短路,其实是软件只配了单通道。

3.3 什么时候该果断换掉轮询

轮询撑不住的信号有三个。主循环周期开始抖动,且抖动周期和采样频率对得上;采样值在通道切换后第一拍明显偏差,那是通道切换没留稳定时间;需要固定采样率做频谱或有效值计算,轮询给不了等间隔。出现任意一条,就该换 DMA。

还有一种情况是低功耗场景。轮询意味着 CPU 必须醒着等转换完成,而 DMA 可以让 CPU 在转换期间睡下去,转换完成再唤醒。这一点在电池供电的采集节点上比 CPU 占用率更关键,因为唤醒次数直接决定平均功耗。

4. DMA 搬运:多通道扫描的缓冲区布局与回调拆分

DMA 这套东西,配置本身不难,难在缓冲区布局和回调时机。配置错了顶多不工作,一眼能看出来;布局错了数据是错位的,看着像模像样,算出来的功率、有效值全不对,排查起来能耗掉一整天。

4.1 从 ADC 到内存的请求链路

G431 上 ADC1 的 DMA 请求走 DMA1 的通道 1,中间还过一层 DMAMUX。CubeMX 里在 ADC1 的 DMA Settings 页加一条请求,选 DMA1 Channel 1,方向 Peripheral To Memory,外设地址不自增,内存地址自增,数据宽度 Half Word,模式选 Circular,优先级 Medium 以上。生成的 MSP 代码里会看到外设地址被设成&hadc1.Instance->DR,这个不要手改。

/* stm32g4xx_hal_msp.c 片段,CubeMX 生成,只列关键项 */ hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设地址固定为 DR */ hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; /* 内存地址逐个递增 */ hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; /* 循环模式,采集不停 */ hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

PeriphInc必须是 DISABLE,因为 ADC 只有一个数据寄存器,地址不能动。MemInc必须是 ENABLE,否则所有通道的数据会挤在缓冲区第一个元素上。Mode选 CIRCULAR 是连续采集的前提,选 NORMAL 只会搬一轮就停,这个错误后面避坑章节还会展开。HAL_DMA_Init之后那行__HAL_LINKDMA是把 DMA 句柄挂到 ADC 句柄上,漏了它HAL_ADC_Start_DMA会直接返回错误。

4.2 交错缓冲区的布局与解交错

多通道扫描加 DMA,落到内存里的数据是交错的:[ch0, ch1, ch2, ch3, ch0, ch1, ch2, ch3, ...]。缓冲区长度等于通道数乘以每轮样本数,传给HAL_ADC_Start_DMA的第三个参数是转换次数,不是字节数,这一点 HAL 文档写得含糊,很多人在这里多除了一个 2。

/* adc_dma.c —— 4 通道扫描,每通道缓存 8 个样本 */ #define ADC_CH_NUM 4 #define ADC_CH_BLK 8 #define ADC_BUF_LEN (ADC_CH_NUM * ADC_CH_BLK) static uint16_t adc_buf[ADC_BUF_LEN]; /* DMA 直接写入的交错区 */ static uint16_t adc_ch[ADC_CH_NUM][ADC_CH_BLK]; /* 解交错后的按通道存储 */ static volatile uint32_t adc_ready_blocks = 0; /* 半块 + 整块,共 2 次/轮 */ void adc_dma_start(void) { /* 第三个参数是转换次数,不是 sizeof(adc_buf) */ if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN) != HAL_OK) { Error_Handler(); } } /* 把 [start, start+count) 区间内的交错数据拆到按通道的二维数组 */ static void demux(const uint16_t *src, uint16_t dst[ADC_CH_NUM][ADC_CH_BLK], uint32_t start, uint32_t count) { for (uint32_t i = 0; i < count; i++) { uint32_t idx = start + i; dst[idx % ADC_CH_NUM][idx / ADC_CH_NUM] = src[idx]; } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { demux(adc_buf, adc_ch, 0, ADC_BUF_LEN / 2); /* 前半块 16 个样本 */ adc_ready_blocks++; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { demux(adc_buf, adc_ch, ADC_BUF_LEN / 2, ADC_BUF_LEN / 2); adc_ready_blocks++; } }

demux里用取模和除法定位,编译器在通道数是 2 的幂时会自动优化成移位和掩码,不用手写。回调里只做搬移和计数,浮点运算、通信、显示全部放到主循环,中断里待的时间越短越好。adc_ready_blocks用volatile修饰,主循环靠它判断有没有新数据,读一次减一次,读到 0 就跳过,别在中断里做复杂同步。

注意:G4 是单核 Cortex-M4,没有 D-Cache,DMA 写进来的数据 CPU 直接可见,不需要 invalidate 操作。从 H7 或者 M7 平台迁过来的代码如果保留了 cache 维护函数,在 G4 上不会出错但纯属多余,反而容易被后人误以为有缓存一致性问题。

4.3 定时器 TRGO 触发:把采样率钉死

软件触发的问题是不准。HAL_ADC_Start_DMA发一次软件触发只转换一轮,要连续采样就得在回调里再触发,间隔取决于中断响应时间,抖动轻松上微秒。真要做等间隔采样,用定时器的更新事件作为 TRGO 输出,接到 ADC 的外部触发源上。

/* tim_trgo.c —— TIM3 更新事件做 TRGO,10 kHz 触发 ADC1 */ static void tim3_trgo_init(void) { TIM_HandleTypeDef htim3 = {0}; TIM_MasterConfigTypeDef master = {0}; __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = 169; /* 170 MHz / 170 = 1 MHz 计数频率 */ htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 99; /* 1 MHz / 100 = 10 kHz */ htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(&htim3); master.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 更新事件输出到 TRGO */ master.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim3, &master); HAL_TIM_Base_Start(&htim3); /* 只开计数,不开中断,触发由硬件完成 */ }

预分频值和周期值要按实际主频算,代码里的 169 和 99 对应 170 MHz 主频。ADC 侧把ExternalTrigConv改成ADC_EXTERNALTRIGCONV_T3_TRGO,触发边沿选上升沿。这样每次定时器溢出,硬件自动发起一轮扫描,DMA 自动搬走,全程不经过 CPU,采样间隔的抖动只剩定时器本身的分辨率,几十纳秒量级。

4.4 半满和全满双缓冲的取舍

缓冲区开成两半,是为了让 DMA 写前半块的时候 CPU 处理后半块,避免读写同一片内存。样本数要满足两个条件:总长度是通道数的整数倍,否则解交错会错位;半块长度要够主循环处理完,太短了频繁进中断,太长了数据延迟大。我一般按主循环周期的两到三倍来定,10 kHz 采样、主循环 1 ms 一轮的话,半块放 16 到 32 个样本比较合适。

如果主循环里还有耗时不确定的操作,比如 Flash 擦写或者串口阻塞发送,双缓冲也救不了,得考虑三缓冲或者把处理搬到 DMA 中断之外的低优先级任务里。这时候数据一致性靠的是块计数,主循环处理完一块才把计数减一,中断只管加,配合临界区保护,比关全局中断简单。

5. 排查与避坑:五条 ADC 采样的现场翻车记录

下面这五条都是我在 G431 上真实遇到过的,按现象、原因、解决三段写,遇到类似症状可以直接对号入座。

5.1 读数整体偏低且很稳定:采样窗口没喂饱采样电容

现象:输入 1.65 V,读数换算出来只有 1.52 V,换了三块板子偏差几乎一样,改用万用表量引脚电压是对的。

原因:前端是 47 kΩ 分压电阻,采样时间只给了 12.5 个周期,采样保持电容根本没充到目标电压,转换结果的偏差跟源阻抗和采样时间强相关,所以重复性极好,看着不像噪声问题。

解决:采样时间加到 247.5 周期,偏差立刻收敛到 1 LSB 以内。更彻底的做法是在分压后面加一级运放跟随,把源阻抗降到 1 kΩ 以下,采样时间就能退回 12.5 周期,顺带把采样率提上去。

5.2 DMA 只进一次回调,之后数据不再更新

现象:串口打印第一帧数据正常,后续缓冲区里全是同一个值,HAL_ADC_ConvCpltCallback只触发了一次。

原因:CubeMX 里 DMA 模式选成了 Normal,搬完设定的转换次数就停了,ADC 还在转换,但没人搬,数据堆在 DR 寄存器里被覆盖。

解决:改成 Circular 模式。改完记得确认HAL_ADC_Start_DMA的第三个参数和缓冲区长度一致,两者不匹配会触发 DMA 传输错误中断,现象和 Normal 模式停摆很像。

5.3 四路通道读数完全一样:Rank 全是 1,扫描没开

现象:四路电流采样,串口打出来四个数一模一样,误差都在 ±2 LSB 以内,硬件上四路输入电压明显不同。

原因:HAL_ADC_ConfigChannel里四个通道的Rank都填了ADC_REGULAR_RANK_1,后配的覆盖先配的,加上ScanConvMode没使能,ADC 只转换 Rank 最低的那一路,DMA 把同一个值搬了四遍。

解决:Rank 按 1、2、3、4 递增,ScanConvMode设为 ENABLE,NbrOfConversion改成 4。改完先用轮询逐个通道验证一遍,确认硬件通路没问题,再开 DMA。

5.4 偶发满量程 4095:GPIO 没切模拟模式

现象:采样值大部分正常,每隔几百毫秒蹦出一个 4095 或者 0,位置随机,跟采样率没关系。

原因:CubeMX 里只配了 ADC 通道,引脚模式还停留在默认的输入浮空。数字输入缓冲器挂在模拟通路上,引脚悬空时输入级会在阈值附近来回翻转,把噪声耦合进采样网络。

解决:所有接 ADC 的引脚,GPIO 模式必须设成GPIO_MODE_ANALOG,顺带把上下拉全部关掉。CubeMX 在配 ADC 通道时通常会自动改,但如果引脚是先手工配过再挂 ADC,就容易漏。

5.5 校准失败或越校越飘:时机和时钟没盯住

现象:调用HAL_ADCEx_Calibration_Start返回 HAL_ERROR,或者返回 OK 但校准后偏差反而变大。

原因:两种情况。一是校准放在HAL_ADC_Start之后调用,ADC 已经在转换,校准流程要求 ADC 处于关闭状态。二是改了 ADC 时钟源却没重新HAL_ADC_Init,CR 里的 BOOST 位还停留在旧频率对应的档位,校准系数是在错误的工作点下算出来的。

解决:校准固定放在MX_ADC1_Init之后、任何 Start 之前,顺序是时钟配置、GPIO 配置、HAL_ADC_Init、校准、再启动。每次改时钟或改分辨率,把校准重新走一遍。校准本身只要几十微秒,上电做一次的成本可以忽略。

提示:ADC 使能之后有一段稳定时间,HAL_ADC_Start内部会空转等待,首次转换的结果往往比后续偏一点。要求高的场合,启动后先丢弃前两轮数据再用。

6. 校准与过采样:把读数逼进可信区间的三个动作

前面几章解决的是"能不能读到",这一章解决"读得准不准"。G431 的 ADC 出厂时并没有逐片校准,上电后的单端校准只是修掉内部失调,真正影响绝对精度的是 VDDA 的实际值,而它往往不等于你原理图上写的 3.3 V。

6.1 用 VREFINT 反推真实 VDDA

芯片内部有一个基准通道 VREFINT,出厂时在 3.0 V 供电、常温下测过一个校准值,存在系统存储区的固定地址。用它反推当前 VDDA,比拿万用表量更贴近 ADC 实际看到的基准。

/* vdda_cal.c —— 用内部基准反推 VDDA,做比例式换算 */ #define VREFINT_CAL_ADDR ((uint16_t *)0x1FFF75AAu) /* 地址以所用型号数据手册为准 */ static uint32_t vdda_mv; void vdda_estimate(void) { uint16_t raw = adc_read_once(ADC_CHANNEL_VREFINT); /* 采样时间给足,至少 247.5 周期 */ if (raw == 0u || raw == 0xFFFFu) { vdda_mv = 3300u; /* 读失败退回标称值,保证后续换算不除零 */ return; } /* 出厂值按 3.0 V 记录,raw 与电压成反比 */ vdda_mv = (uint32_t)((3000ull * (*VREFINT_CAL_ADDR)) / raw); } uint32_t adc_to_mv_calibrated(uint16_t raw) { return ((uint32_t)raw * vdda_mv) / 4095u; }

VREFINT 通道的源阻抗很高,采样时间必须给到 247.5 周期以上,否则反推出来的 VDDA 本身就偏。算出来的值一般落在 3200 到 3400 mV 之间,跟万用表量的差值能压到 10 mV 以内。这一步做完,所有通道的换算

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

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

高阶调制PAPR实战分析:128QAM、64APSK与QPDK的功放适配决策

简介&#xff1a;本资源聚焦无线通信系统中高阶调制方式的峰均功率比&#xff08;PAPR&#xff09;对比分析&#xff0c;面向通信工程专业学生、射频与信号处理方向研究人员及5G/卫星通信系统设计工程师&#xff0c;解决高阶调制选型时PAPR评估缺乏实测数据支撑的实践痛点。压缩…

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

AI搜索优化:提升意图识别与排序算法的实战指南

1. 项目背景与核心价值 在信息爆炸的时代&#xff0c;搜索体验的质量直接决定了用户获取信息的效率。最近我在优化"思阳GEO"项目时发现&#xff0c;传统搜索模式存在三个典型痛点&#xff1a;结果相关性低、排序逻辑不透明、个性化程度不足。这导致用户需要反复调整关…

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

动态上下文与实时RAG技术解析:MCP协议与工程实践

1. 动态上下文与实时RAG的核心价值 在传统RAG&#xff08;检索增强生成&#xff09;系统中&#xff0c;知识库通常是静态的&#xff0c;这意味着当新数据产生时&#xff0c;系统无法立即将其纳入检索范围。这种延迟在需要实时决策的场景中会成为致命短板。动态上下文技术通过MC…

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

离线PDF处理工具的核心功能与选型指南

1. 为什么我们需要一款离线的PDF处理工具&#xff1f; 在日常工作和学习中&#xff0c;PDF文档已经成为我们最常接触的文件格式之一。无论是合同签署、论文提交还是电子书阅读&#xff0c;PDF的跨平台稳定性使其成为文档交换的首选格式。但随之而来的问题是&#xff1a;大多数P…

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

T3 Stack全栈类型闭环:TypeScript、tRPC与Prisma实战指南

如果只看“t3code”这个名字&#xff0c;你可能以为它是某个极客圈子的暗号&#xff0c;或者是一把压缩过的快捷键。事实上&#xff0c;这是我给自己负责的一个全栈项目起的内部代号&#xff1a;T3 指的是现在 React 生态里那套热度居高不下的技术组合拳——TypeScript、Tailwi…

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

AI测试用例生成:三大核心提示原则与实战技巧

1. 为什么你的AI测试用例生成效果不理想&#xff1f; 我最近在技术社区看到不少同行抱怨&#xff1a;"用AI生成的测试用例根本没法用&#xff01;"、"写出来的全是废话&#xff01;"、"覆盖的场景完全不靠谱&#xff01;"。作为一个在测试领域摸…

作者头像 李华