ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 ADC多通道采集:DMA配置与CubeMX实战指南

STM32 ADC多通道采集:DMA配置与CubeMX实战指南 简介STM32结合ADC与DMA实现多通道数据采集是一份面向嵌入式初学者与开发者的完整工程资源。该方案利用STM32内置ADC完成多路模拟信号采样再通过DMA直接传输至内存减少CPU干预适用于环境监控、工业设备、电源管理等高频率实时采集场景。压缩包共219个文件约8.49MB主要包含HAL库源码.c/.h、工程配置.ioc/.uvprojx、编译产物.o/.axf/.hex以及链接映射.map等便于直接打开工程查看初始化与中断处理逻辑。已有1680人学习下载。通过阅读代码可掌握ADC通道序列配置、DMA循环传输、转换完成中断处理等关键环节同时还能学习如何用CubeMX生成基础工程并基于HAL库编写驱动适合希望快速上手STM32多通道采集的开发者作为参考模板。 做单片机开发的朋友多半碰过这种场景一个板子上同时要采好几路模拟量三相电流、两路电池电压、一路温度用STM32的ADC去读本来不难但通道一多、响应一快主循环里就会被各种等待转换的代码塞满。我这次把STM32ADCDMA多通道采集的完整操作讲一遍包括原理、配置、参数计算、代码和调试时踩过的坑给你一份能直接照抄的作业。这里面既有新手容易卡住的CubeMX选项解释也有老手偶尔翻车的对齐和优先级问题做数据采集、仪器仪表、电机控制的朋友都能用得着。1. 多通道采集为什么绕不开DMA1.1 一次ADC多通道转换数据是怎么跑起来的先弄明白ADC本身的工作方式。STM32的ADC模块是逐次逼近型结构内部有个采样保持电容外部模拟电压先通过采样开关给电容充电再靠内部的比较器一位一位逼近出数字结果。多通道采集时我们要把若干个通道放进规则组开启扫描模式后ADC会按照Rank序号挨个把每个通道转换一遍。比如Rank1配的是通道0Rank2配的是通道1一轮扫描结束后四个通道的结果就按顺序依次出现在同一个数据寄存器ADC_DR里。这里有一个非常关键的细节每转换完一个通道ADC_DR里只保留最新结果。如果你没在通道0的结果被通道1覆盖之前把它读走这个数就没了。也就是说没有DMA时CPU必须在转换完成这件事上随叫随到。读取速度跟不上数据就会丢。DMA的作用就是替CPU盯住这个时间点转换完成信号一来DMA控制器直接把ADC_DR的值搬到内存数组全程不用CPU干预。1.2 查询、中断、DMA三种方式的真实成本和取舍查询方式代码最简单但代价也最直观。你永远不知道下一次转换什么时候完成只能用while循环死等等一个通道、读一个通道CPU基本就焊死在ADC这件事上了。只采一路慢速温度信号还能忍四路八路还要跑显示、通信、控制算法查询方式直接拖垮主流程。中断方式比查询好一些每个通道转换完成后触发一次中断在中断里搬运16位数据。但中断频率等于通道数乘以每秒轮数四通道跑10kHz刷新一秒就是四万次中断CPU的时间大量花在进出栈上其他实时任务的响应就会变得不稳定。DMA方式下转换信号直接触发DMA搬运搬完一整轮才产生一次中断开了半传输中断就是半轮一次CPU负载瞬间降下来。我最推荐的是连续转换DMA循环模式这个组合ADC启动后不停扫描四个通道DMA也不停把数据写进数组像一条自动流水线在后台跑。主循环想用数据时直接读数组对应元素就行。CPU只是偶尔来取货不用站在传送带旁边干等。2. 动手之前引脚、时钟和CubeMX工程规划2.1 引脚选择和信号源内阻直接决定采集准不准多通道采集第一步不是写代码而是确认引脚。以F103为例PA0到PA7都能复用为ADC1的输入通道对应通道0到通道7。你要确保这些引脚没有被其他外设占用比如PA9、PA10是USART1和串口冲突的话排线就要乱套。还需要注意同时采多路模拟量时优先用ADC1的通道因为F103的ADC1搭配DMA最顺畅ADC2、ADC3也能用但DMA映射在部分型号上有限制新手阶段先把ADC1玩明白再说。很多人忽略信号源内阻的问题。STM32的ADC内部采样电容不是理想器件采样时间有限如果外部信号源阻抗很高比如几kΩ以上直接用个电位器分压接进来采样瞬间电容来不及充到稳定电压读出来的值就会偏低而且跳动大。实际项目里我习惯在ADC引脚前加一级RC低通R取1kΩ左右C取100nF既滤高频噪声又能保证充电速度。如果你的信号源确实很高阻比如光敏电阻直接分压那就加一个运放跟随器把阻抗降下来精度立刻改善。2.2 CubeMX里的关键选项扫描模式、连续转换和DMA用CubeMX生成工程时ADC1的Parameter Settings里几个选项要格外注意。ScanConvMode必须设为Enabled这是多通道采集的前提否则ADC只会反复转换你指定的那一个通道。ContinuousConvMode建议打开转换完一轮自动开启下一轮配合DMA循环模式实现后台持续采集。DiscontinuousConvMode保持Disabled它是给特殊触发场景用的普通多通道采集用不上。NumberOfConversion填通道数比如4。ExternalTrigConv选Software Start代码里软件启动不依赖定时器。DMA Settings里把ADC1的DMA请求加上Mode选Circular循环模式Data Width选Half Word。F103上ADC1的DMA请求是固定连接到DMA1的Channel1CubeMX会自动配对不用手动选。优先级建议给High避免高负载时ADC的数据搬运被其他DMA请求挤掉。2.3 数组大小、通道数量和Rank顺序怎么对齐DMA搬运的目标是内存数组启动时指定的传输次数就是通道数。四个通道就定义uint16_t adc_buf[4]启动调用HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, 4)。数组下标和通道的对应关系完全由你在ADC里设定的Rank顺序决定Rank1的结果固定存入buf[0]Rank2存入buf[1]依次类推。所以你配Rank顺序时脑子里要清楚buf[0]究竟对应哪个物理通道。这里埋了一个常见坑如果你中途改了CubeMX里Rank顺序但程序里读数据的代码没改数据和引脚对不上是必然的。建议代码里别用裸数字0、1、2、3而是定义成ADC_CH_CURRENT、ADC_CH_VOLTAGE这样的宏哪天调换顺序只动一个地方。3. 核心参数计算采样周期、转换时间与DMA配置3.1 用公式算清楚一轮多通道采集到底要多久ADC单次转换时间由两部分组成采样阶段加转换阶段。12位分辨率下转换阶段固定占12.5个ADC时钟周期采样阶段由SamplingTime决定。F103的ADC时钟来自APB2经过预分频为了不超规格我把APB2设为72MHz、分频6得到12MHz的ADC时钟。公式是单通道转换时间(采样周期12.5)/ADC时钟。采样时间设为55.5周期时单通道就是(55.512.5)/12MHz5.67us四通道一轮约22.7us每秒大概能刷新四万多轮。这个数值已经很充裕了。要采高阻慢变信号时采样时间拉大点让采样电容充得更满数值更稳要采几百kHz的快速信号时就得用短采样时间同时降低源阻抗。下面这个对照表可以直接参考采样时间(周期)单通道转换时间(12MHz ADC时钟)适合场景1.51.17us极低阻信号源追求高刷新率7.51.67us一般低阻传感器13.52.17us普通运放输出28.53.42us稍高阻信号希望稳一点55.55.67us高阻或慢变信号优先保证准确71.57.00us很在意抗干扰时239.521.00us极慢信号速度无所谓3.2 DMA方向、数据宽度、循环模式怎么选DMA方向没有悬念外设到内存DMA_PERIPH_TO_MEMORY。数据宽度里外设侧选Half Word内存侧也选Half Word因为ADC结果就是16位。这里特别注意外设地址增量PeriphInc保持Disable内存地址增量MemInc必须Enable否则每次搬运都写到同一个地址四路数据全堆在buf[0]里后面的数据全被覆盖。Mode选Circular后DMA搬完一轮会自动把内存地址指针指回数组头不需要CPU干预。连续转换模式下数组里的数据永远是最新的一轮结果。启动后只要不调用HAL_ADC_Stop_DMA这套流水线就会一直跑数据始终保持更新状态。3.3 容易翻车的半字对齐和连续请求选项半字对齐这个坑我实际遇到不止一次。有人把外设端设为HalfWord、内存端设为Word看起来只是放宽了宽度结果DMA每次从ADC_DR取16位数据却按32位写进内存buf[1]直接落到数组第三个位置数据全部错位。反过来外设端Word、内存端HalfWord也乱。最稳的做法就是两边一致都用HalfWord。另外F3、F4的部分系列在DMA配置里会有Continuous Requests这类选项作用是让DMA通道持续请求总线不再依赖外设事件。听起来效率高但实际会一直占用总线带宽多通道采集场景没必要开开了之后其他外设的DMA传输反而会被拖慢属于典型的看着厉害用着受罪。4. 完整代码与实测数据验证4.1 F103上的HAL库初始化代码贴一段我实际用过的初始化代码四通道电池电压采集的简化版关键行都加了注释。CubeMX生成工程后你只需要确认这几个函数然后在main里调用启动。static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; // 多通道扫描 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; // 12位右对齐 hadc1.Init.NbrOfConversion 4; // 4个通道 HAL_ADC_Init(hadc1); sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; // 采样时间长一点稳 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_2; sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_3; sConfig.Rank 4; HAL_ADC_ConfigChannel(hadc1, sConfig); }DMA的初始化一般由CubeMX生成在单独文件里核心内容如下。注意__HAL_LINKDMA这行它就是把ADC句柄和DMA句柄绑定在一起的关键少了它HAL_ADC_Start_DMA根本跑不起来。hdma_adc1.Instance DMA1_Channel1; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; 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);启动采集就一行放在main初始化之后之后它就一直跑HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4);这里提醒一下初始化顺序。CubeMX生成的执行顺序一般是GPIO、DMA、ADC这个顺序不要乱调因为ADC初始化时要用到已经注册好的DMA句柄。如果你把MX_DMA_Init放到MX_ADC1_Init后面HAL库在启动时找不到DMA句柄直接跑飞或者报错排查起来还不太容易想到是这个原因。4.2 在主循环里稳定读取多通道数据直接读adc_buf[0]没有问题但有个隐患DMA每时每刻都在写数组如果你在主循环里连续读完四个元素读一半DMA又覆盖了buf[0]这轮数据就是新旧混合的。简单做法是先把四路数据一次性复制到本地数组再处理复制过程虽然也可能被DMA打断但一次复制的时间非常短实际影响可以忽略。要求再高一点可以用DMA传输完成中断加标志位在回调里只置标志真正数据处理放主循环volatile uint8_t adc_ready 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_ready 1; } }主循环里判断到adc_ready为1时才读数组、清标志。中断回调里尽量不做事只置标志避免在中断上下文里耗时过长。4.3 软件滤波和电压换算从原始码值到有意义的结果ADC出来的原始值是码值12位分辨率下范围0到4095。换算电压很简单电压码值×Vref/4095。F103的Vref内部接到VDDA大部分板子上就是3.3V所以3.3V对应4095。滤波这一步直接影响使用体验。我常用的两段式先中值滤波剔毛刺再滑动平均平滑波形。中值滤波的代码很经典九个值排序后取中间那个对脉冲噪声特别有效uint16_t adc_median_filter(uint16_t *arr, uint8_t len) { uint8_t i, j; uint16_t tmp; for (i 0; i len - 1; i) { for (j 0; j len - i - 1; j) { if (arr[j] arr[j 1]) { tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } return arr[len / 2]; }然后对中值结果再做几次滑动平均毛刺基本就被压住了。如果你的信号本身就是缓慢变化多采几轮再平均波动可以控制在±2个LSB以内。这里有一个容易忽略的细节ADC参考电压VDDA必须稳定最好单独加滤波电容VDDA上的一点纹波最后都会变成采样结果里的一串噪声这属于硬件层面的事但往往比软件滤波更能解决问题。4.4 实测数据长这样才算调通了调试时我习惯在四个通道上分别接固定电压通道0接3.3V通道1接地通道2接一个精密电阻分压得到的1.65V通道3接电位器。实测读数应该是通道0稳定在4095附近通道1在0附近通道2在2045到2048之间摆动通道3随电位器拧动线性变化。抖动不超过±3个LSB说明整条链路是通的。如果发现数值差得离谱比如接地通道读出来还有几百先怀疑接线和电源参考而不是怀疑DMA。用串口打印数据观察最直观如果你的串口调试助手数据不稳定先看波特率再看USB转串口驱动。设备管理器里虚拟串口带黄色感叹号通常是驱动没装好把对应厂商的驱动重装一遍就能解决。这类问题和数据采集本身没关系但排查时最容易卡住人。5. 常见问题排查实录5.1 通道数据错位DMA搬回来的值和引脚对不上最典型的症状明明PA0接的3.3V读取的buf[0]却不是4095反而是buf[2]对得上。原因基本是Rank顺序和代码读取顺序不一致或者DMA的内存地址增量没开。解决办法很简单回到CubeMX确认Rank1对应通道0、Rank2对应通道1再检查启动DMA的传输次数是不是4数组元素个数是不是4。还有一个隐蔽原因如果你在CubeMX里改过引脚复用但工程没有重新生成代码里的GPIO配置和目标引脚对不上也会出现这种看起来像DMA错了其实引脚错了的情况。遇到错位先用万用表量引脚电压再对照串口打印的数据能很快缩小范围。5.2 首轮采集值偏小或全为0ADC模块上电后不是立刻稳定的内部参考和采样电容需要一点建立时间。启动后立刻读DMA数组第一轮或者前几轮数据经常偏小甚至读出来一串0。我的习惯是启动DMA后延时个几毫秒再把启动阶段的数据清掉从后面几轮开始用实际数据HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 4); HAL_Delay(2); // 给ADC一点建立时间 memset(adc_buf, 0, sizeof(adc_buf)); // 丢弃启动阶段垃圾数据后面连续采集模式下数据稳定后再去消费就行。另外引脚悬空时读数本来就是随机的别拿悬空通道校验数据也不要因为悬空通道跳得厉害就怀疑DMA配置。5.3 下载时报No STM32 Target Found程序却已经跑了这个报错遇到过太多次包括我第一次也卡过。它通常不是代码逻辑问题而是调试器连接问题。检查顺序大概是SWD两根线SWDIO、SWCLK是否接对地线是否共地芯片供电是否正常VDD和VDDA都要量一下调试器驱动是否装好设备管理器里有没有识别到设备BOOT0是不是被意外拉高拉高的话芯片会进入ISP模式调试器自然连不上程序里如果之前开了低功耗模式也会让内核睡死导致调试器无法连接。老手常用的土办法是按住复位键点下载后立刻松开。很多时候连不上只是时序没赶上这一招能救急。如果是较新的芯片提示和Debug Authentication相关多半是读保护等级被提高或者调试认证配置问题需要用官方工具解除读保护再操作。5.4 ADC和串口DMA同时用时怎么避免互相干扰工程里ADC用DMA串口发送也用DMA是很常见的组合。F103的DMA1有多个ChannelADC1占用Channel1串口可以用其他Channel硬件上不冲突但总线访问会有竞争。ADC的DMA优先级配得比串口高高频采集时串口发送才不会频繁抢占总线导致ADC数据偶发丢失。串口DMA发送还有一个细节必须等上一轮发送完成才能启动下一轮否则数据会错乱或者卡死。最简单的方式是发送前检查状态while (huart1.gState ! HAL_UART_STATE_READY); HAL_UART_Transmit_DMA(huart1, tx_buf, len);实际工程里这个while可以加超时机制避免在异常状态下死等。这个等上一轮发完再发下一轮的思路和前面DMA循环采集其实是一回事DMA只管搬运任务的节奏始终要在应用层控制好。我做过多路数据采集的小项目从四路电池电压采样到八路温度采集底层基本都是这套STM32ADCDMA的组合。这套方案本身已经很成熟难点反而不在初始化代码而在你对数据链路的理解ADC怎么采、DMA怎么搬、应用层怎么消费。把这三点想清楚多通道采集就真的只是配置问题。最后分享一个小技巧如果你用STM32CubeIDE调试可以把adc_buf数组加到Live Expressions窗口实时观察四路数据的值比串口打印直观得多排查数据错位这类问题能省下不少时间。本文还有配套的精品资源点击获取
返回列表