ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32三重ADC+DMA双缓冲同步采样方案详解与踩坑记录

STM32三重ADC+DMA双缓冲同步采样方案详解与踩坑记录 做多路同步采样的时候我第一版方案直接翻车了用一块STM32F4的普通ADC扫描三个通道看起来能采可一上示波器就发现三相电压的相位关系全是歪的。问题根源不是代码逻辑而是单ADC多通道本身就是轮流转换通道之间存在时间差。后来换成了三重ADC DMA双缓冲这套组合用CubeMX重新配置才算是把同一时刻采三路这件事真正落地。这篇文章就把这套方案的原理、CubeMX配置、代码实现和踩坑点一次讲透给同样要做同步采样的朋友一个可以直接抄作业的参考。1. 先想清楚三重ADC DMA双缓冲到底解决什么问题1.1 单ADC轮流采样的致命时间差很多人在做多通道采集时第一反应是用ADC1的扫描模式Scan Mode把三个通道排进一个规则序列里让ADC自己一个个转。这个方案最大的问题在于ADC的规则组虽然可以连续转换多个通道但物理上只有一个采样保持电容、一个比较器通道之间永远是串行的。以F407为例如果ADCCLK是21MHz12位分辨率采样周期取3个时钟周期一个通道的转换时间大概是 (12 3) / 21MHz ≈ 0.71微秒。三个通道扫描一轮从第一个通道开始采样到第三个通道拿到结果中间大约差了1.4微秒以上。如果你采集的是50Hz工频信号1.4微秒对应的相位误差可能只有0.02度左右看起来不大但如果采集的是电机驱动里的相电流、高频振动信号、或者做声源定位这个时间差会直接变成不可接受的相位误差和幅值偏差。更麻烦的是单ADC扫描模式如果遇到外部触发频率很高触发信号来了之后ADC正在转前面的通道新触发还要排队时序完全不可控。所以同步采样这四个字用单ADC的扫描模式是做不到的必须在硬件层面让多个ADC同时开始转换。1.2 STM32F4三重ADC的工作方式STM32F4这一代芯片内部集成了三个独立的逐次逼近型ADC模块分别是ADC1、ADC2、ADC3。它们可以完全独立使用也可以组合成双重模式或三重模式。所谓三重ADC模式核心就是三路ADC共享同一个触发源在同一时刻对各自的通道进行转换。具体到寄存器层面外部触发信号只需配置在ADC1上触发信号到来时ADC2和ADC3会跟着ADC1同步启动。转换完成后每个ADC的结果放在各自的DR寄存器里。如果使能了DMA请求数据会按ADC1、ADC2、ADC3的顺序被搬到内存缓冲区。这就是硬件同步不是靠中断里做尽量同时读而是真正的同一拍开始采样。有个地方必须提醒三重模式下每个ADC在规则组里一次只能处理一个当前转换。也就是说如果你有三个物理信号需要同步采样正确做法是ADC1的Rank1放一路信号、ADC2的Rank1放第二路、ADC3的Rank1放第三路这样一次触发就能拿到三路同时刻的样本。想用ADC1自己扫两个通道、ADC2扫两个通道、ADC3扫两个通道在规则同步模式下会让时序变得混乱不建议新手这样配。1.3 为什么这里必须用DMA而不是中断读数据三路ADC一旦跑起来数据率会迅速超过CPU用中断处理的舒适区。假设ADCCLK 21MHz每个ADC的转换时间是0.71微秒那么三个ADC并行转换每个周期能产生3个16位结果。如果每秒触发100kHz每秒要处理300k个数据点折算下来大约每3.3微秒就要进一次中断读一次数据。中断处理不是不行但有几个致命问题第一中断响应本身有延迟无法保证在一个固定时刻点读取DR数据时序会有抖动第二高频率中断会占用大量CPU时间片导致主循环里做FFT、PID、通信等任务变得卡顿第三如果中断里再去做数据拷贝或者计算很容易发生下一个转换结果已经出来、上一个还没处理完的数据覆盖。DMA的作用就是让数据搬运这件事不经过CPU。ADC转换完成之后硬件DMA控制器自动把DR里面的数据搬到指定的内存数组搬运完成再产生一个中断信号通知CPU。这样CPU只管处理数组里的数据不用管每一个采样点是什么时候进内存的。对于三重ADC这种多路高速采集场景DMA基本是必选项。1.4 “双缓冲”是在双哪两个缓冲区双缓冲解决的是数据覆盖问题。DMA在循环模式下会不停地往同一个内存数组写数据如果CPU正在处理第N帧数据的时候DMA已经把第N1帧数据写进了同一个数组CPU拿到的数据就是新旧混杂的结果全错。双缓冲的思路是把缓冲区拆成两个独立的区域DMA写A区的时候CPU处理B区DMA写满A区后自动切到B区同时通知CPU去处理A区。两个区交替使用DMA和CPU永远不碰同一块内存区域数据覆盖问题就从这个根上解决了。但这里有个小坑STM32F4的HAL库在CubeMX生成的ADC多模式DMA启动函数里默认使用的是普通循环DMA并不是硬件DMA双缓冲模式。很多人在CubeMX里找了半天Double Buffer选项发现ADC的DMA设置里根本不给配。这是因为在HAL库框架下更常见的做法是循环DMA 半传输中断 传输完成中断来模拟双缓冲。这个我会在第三章详细展开。2. CubeMX一步一步配出三重ADC同步采样2.1 时钟树和ADC分频怎么定先用CubeMX新建一个基于STM32F407VET6的工程时钟树里让APB2时钟PCLK2跑到84MHz。之所以强调APB2是因为ADC1、ADC2、ADC3都挂在APB2总线上它们的时钟源来自APB2分频后的ADCCLK。F4的ADC最高输入时钟是36MHz而PCLK2已经达到84MHz所以必须给ADC加预分频。CubeMX里在ADC配置页可以找到ADC Prescaler选择 /4这样ADCCLK 84 / 4 21MHz安全且典型。如果你把APB2超频到更高的值一定要重新核算ADC分频绝不能超过36MHz上限否则采样结果非线性和噪声会明显变差。采样周期怎么选在ADC Parameter Settings的Rank列表里有Sampling Time可选3、15、28、56、84、112、144、480个周期。追求速度就选3Cycles追求精度就选84或144Cycles。同步采样三路信号时建议把三个ADC对应Rank的采样时间配成一致避免因为采样保持时间不同造成通道间的幅值差异。2.2 ADC1/2/3的关键参数配置CubeMX里先把三路ADC都使能进入对应的Parameter Settings。关键参数如下参数ADC1ADC2ADC3ModeTriple regular simultaneous modeIndependent modeIndependent modeScan Conversion ModeEnabledEnabledEnabledContinuous Conversion ModeEnabledEnabledEnabledDiscontinuous Conversion ModeDisabledDisabledDisabledResolution12 bit12 bit12 bitData AlignmentRight alignmentRight alignmentRight alignmentDMA Continuous RequestsEnabled无需设置无需设置External TriggerSoftware TriggerSoftware TriggerSoftware Trigger这里最容易搞混的就是Mode这个参数。三重模式的主开关只在ADC1上ADC2和ADC3保持Independent mode即可。如果你把ADC2或者ADC3也选成Triple模式要么编译报错要么生成的代码里模式配置互相冲突。通道选择上三个ADC要分别选一个物理引脚。F4系列的ADC2和ADC3没有完全独立的引脚集合不少引脚是ADC1/2或者ADC1/3共享的CubeMX里配置引脚时能看到类似ADC12_IN3这样的标注。实际使用中你只要保证三个ADC选的物理引脚对应了三路不同的外部信号即可系统会自动处理共享引脚关系。转换序列Rank里三个ADC都只用Rank1。F407上ADC1和ADC2都有16个外部通道ADC3的外部通道数量和分布略有不同具体要看芯片型号的数据手册。做完这部分CubeMX就能生成一个三段式的ADC初始化代码。2.3 三重模式下DMA到底挂在哪个ADC上这是三重ADC配置和普通单ADC配置最大的区别DMA请求只配置在ADC1上ADC2和ADC3不要单独添加DMA设置。因为三重模式的DMA请求由主ADC统一产生数据顺序由硬件内部状态机按ADC1、ADC2、ADC3排列从DMA的角度看它只知道自己是从ADC1的DR地址读数据。在CubeMX的DMA Settings里给ADC1添加一条DMA请求具体选择DMA2 Stream0 Channel0F407上常见其他F4型号会有差异以IDE提示为准。参数设置如下DirectionPeripheralToMemoryPriorityHighModeCircularIncrement AddressPeripheralDisableIncrement AddressMemoryEnableData WidthPeripheralHalf WordData WidthMemoryHalf Word外设地址不递增、内存地址递增这是标准ADC采集DMA配置。数据宽度用Half Word是因为ADC的DR是16位寄存器。很多项目里三重ADC数据错乱排查到最后都是数据宽度不小心配成了Word导致读取的数据一会儿对一会儿不对。2.4 别忘了开启DMA Continuous Requests在ADC1的Parameter Settings界面里有一个参数叫DMA Continuous Requests默认是Disabled。这个参数对应到寄存器就是ADC_CR2的DDS位。它到底什么意思ADC的DMA请求有两种产生方式一种是每次规则组转换完成后只产生一次DMA请求叫单次请求另一种是只要DMA使能且ADC还在转换就持续不断地产生请求叫连续请求。如果你用Circula模式的DMA并且希望ADC自动持续采、不靠手动重新触发就必须把DMA Continuous Requests设为Enabled。否则你会看到DMA只搬运了一帧数据然后整个采集流程就停在那里不动了。很多第一次接触三重ADC的人在DMA循环模式配好的情况下仍然只能采到一波数据十有八九就是漏了这个参数。同理Continuous Conversion Mode也要打开这样ADC本身在转换完一帧后会自动启动下一帧不需要软件反复触发属于硬件自持运行。3. 代码实现从启动到双缓冲再到数据解析3.1 启动三重模式DMA采样的唯一入口CubeMX生成代码后工程里会有MX_ADC1_Init、MX_ADC2_Init、MX_ADC3_Init三个初始化函数。初始化只管配置寄存器和GPIO真正让三重ADC跑起来要调用HAL库的多模式启动专用接口#define ADC_BUF_LEN 1024 /* 半字个数必须是3的整数倍 */ __IO uint16_t adc_buf[ADC_BUF_LEN] {0}; HAL_ADCEx_MultiModeStart_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);这个函数的三个参数分别是主ADC句柄、DMA内存缓冲区指针、数据长度。注意第三参数是半字个数不是帧数也不是字节数。ADC1、ADC2、ADC3每同步完成一次转换会产生3个16位数据所以这里填的1024实际能装下1024 / 3 ≈ 341帧数据。启动之前建议先做一次ADC校准HAL_ADCEx_Calibration_Start(hadc1); HAL_ADCEx_Calibration_Start(hadc2); HAL_ADCEx_Calibration_Start(hadc3);校准会让ADC内部的失调误差被修正对绝对电压精度有影响。我第一次调的时候忽略了校准后来发现同一电压两个ADC读数差了十几个LSB校准之后才拉齐。3.2 用“半满中断 满中断”来模拟双缓冲前面说了CubeMX生成的HAL库不会自动帮你开启硬件双缓冲但我们可以用循环DMA配合两个中断回调来实现同样的效果。原理很简单把一个长度为2N的DMA缓冲区在逻辑上切成两半前半段和后半段各自作为一个缓冲块。DMA循环写满整块缓冲区时会产生传输完成中断写到一半时会产生半传输中断。于是CPU在DMA正在写后半段的时间窗口内处理前半段在DMA正在写前半段的时间窗口内处理后半段天然错开了读写区域。代码实现如下#define HALF_LEN 512 #define ADC_BUF_LEN (HALF_LEN * 2) __IO uint16_t adc_buf[ADC_BUF_LEN] {0}; volatile uint8_t half_flag 0; void HAL_ADCEx_MultiModeConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { half_flag 1; /* 前半段可处理 */ } } void HAL_ADCEx_MultiModeConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { half_flag 2; /* 后半段可处理 */ } }主循环里按标志位处理数据while (1) { if (half_flag 1) { half_flag 0; process_frame((uint16_t *)adc_buf, HALF_LEN); } else if (half_flag 2) { half_flag 0; process_frame((uint16_t *)adc_buf[HALF_LEN], HALF_LEN); } }这样做的好处是回调函数非常短只置一个标志位不占用DMA时间真正的数据处理放在主循环里可以配合实时操作系统或者其他任务调度。处理半帧数据的时间必须小于另一帧DMA写入的时间否则下一轮中断来的时候上一轮还没处理完数据仍然会被覆盖。如果处理逻辑太重就得把处理好的数据做快速拷贝或者改用四块乒乓缓冲、再或者上真正的硬件DMA双缓冲。3.3 解析三重ADC的数据三个结果在一帧里的顺序数据到了adc_buf里如何知道哪个是ADC1、哪个是ADC2、哪个是ADC3规则同步模式下DMA搬运顺序是固定的每一次同步转换结果先搬ADC1的DR再搬ADC2的DR最后搬ADC3的DR。体现在缓冲区里就是一个三元素循环void process_frame(uint16_t *buf, uint16_t len) { /* len 512是半字个数不是帧数 */ for (uint16_t i 0; i len; i 3) { uint16_t v_adc1 buf[i]; uint16_t v_adc2 buf[i 1]; uint16_t v_adc3 buf[i 2]; /* 这里就是同一时刻采到的三路值去做你要的处理 */ } }网上有些资料会提到通用规则数据寄存器ADC_CDR说三重模式下DMA应该从CDR读取合并数据。以我现在用的HAL库版本和F407芯片实测DMA外设地址走的是ADC1的DR地址硬件会把三个ADC的结果按顺序轮流放在DMA总线上所以用半字宽度、按3取模就可以了。如果你的CubeMX配置自动生成的外设地址指向了ADC_CDR那就需要改成32位宽度并且按每2个32位字为一组去解析具体情况打开生成的代码看一眼DMA外设地址就知道不用死记。另一个细节如果你在规则组里给某个ADC配了多个Rank比如ADC1 Rank1是PA0、Rank2是PA1那数据顺序会变成ADC1_Rank1、ADC2_Rank1、ADC3_Rank1、ADC1_Rank2、ADC2_Rank2、ADC3_Rank2。解析代码需要按照这个帧序来写。新手建议先用每人一个Rank的配置跑通再考虑多通道扩展。3.4 真正使用采集数据时的注意事项处理同步采集数据时最容易踩的坑是数据对齐问题。由于DMA搬运和ADC转换之间是流水线式的你拿到的第i帧数据并不一定对应触发信号的同一个位置。如果外部触发频率不高这个误差一般可以忽略但如果要拿三路数据做FFT、谐波分析建议在每一帧数据里加一个硬件标志位或者在触发信号上升沿打一个时间戳确保帧与帧之间没有掉落或者重复。内存方面adc_buf要用__IO修饰因为DMA是硬件外设编译器不一定会知道这个数组会被后台改写不加volatile可能被优化到寄存器里导致主循环永远看到旧值。数组长度方面F4的DMA传输位数如果是半字长度限制在65535以内普通采集需求都够用。还有就是DMA的优先级。F4的DMA2比DMA1快一些ADC采集建议挂在DMA2上。同一个DMA控制器下如果还有其他外设比如串口TX、SPI等争带宽要把ADC采集的DMA优先级适当调高避免高频率下出现搬运不及时、ADC那边数据覆盖。4. 常见问题、坑位和调试手段实录4.1 采出来的数据只有第一帧是好的这个现象我在帮一个群友排查时遇到过表现是程序下载后第一次数据完全正确后面全部变成0或者固定值。检查下来有三个高频原因第一DMA Continuous Requests没开ADC只产生了一次DMA请求后续数据根本没搬第二DMA模式错配成了Normal而不是Circular搬完一遍就停第三缓冲区地址没有按Word对齐F4某些DMA配置会对地址对齐敏感。处理思路是按顺序排查先看ADC_CR2的DDS位有没有置1再看DMA_SxCR的CIRC位是否为1最后用调试工具查看DMA_CNDTR寄存器的实时值如果这个值一直在递减变化说明DMA还在继续工作。4.2 双缓冲区的数据还是会覆盖循环DMA 半满中断这套方案有一个硬前提CPU处理一块数据的时间必须小于DMA写满另一块的时间。如果你的处理函数里做了浮点运算、FFT、串口打印处理时间很容易超过这个窗口。解决办法有三种把处理函数改短只做数据拷贝或者标定把重负荷计算放到更慢的任务里。把双缓冲区升级成三缓冲。/三块轮流使用DMA写C区的时候CPU可能还在处理A区或B区容错空间大很多。使用STM32F4的硬件DMA双缓冲模式让DMA在当前缓冲区写满后自动切换内存目标不需要等CPU处理完。这三种方案里最简单且稳妥的是第一种。做实时采集时我会让中断回调里只复制数据不参与任何计算。4.3 想用硬件DMA双缓冲模式该怎么做如果你确实想用F4的DMA双缓冲特性首先要知道它和HAL库的ADC多模式启动函数不能同时用。HAL_ADCEx_MultiModeStart_DMA内部已经调用了HAL_DMA_Start_IT去初始化DMA你再调用HAL_DMAEx_MultiBufferStart会覆盖掉DMA配置两个函数互相打架现象千奇百怪。我的做法是绕过HAL的多模式启动手动配置#define HALF_BUF_LEN 512 uint16_t dma_buf_a[HALF_BUF_LEN]; uint16_t dma_buf_b[HALF_BUF_LEN]; HAL_StatusTypeDef ret; ret HAL_DMAEx_MultiBufferStart(hdma_adc1, (uint32_t)hadc1.Instance-DR, (uint32_t)dma_buf_a, HALF_BUF_LEN); HAL_DMAEx_ChangeMemory(hdma_adc1, (uint32_t)dma_buf_b, DMA_MEMORY_1);然后在DMA传输完成回调里切换下一次内存目标void XferCpltCallback(DMA_HandleTypeDef *hdma) { if (hdma-Instance DMA2_Stream0) { HAL_DMAEx_ChangeMemory(hdma, (uint32_t)dma_buf_b, DMA_MEMORY_0); } }注意HAL的ADC多模式启动接口内部注册了它自己的回调两个方案不要混着启动否则会互相覆盖回调函数。我实际项目中大部分情况下用循环DMA方案就够了只有在采样率极高、CPU完全无法及时处理时才切换到硬件双缓冲。4.4 三个ADC结果对不上拍的排查思路如果三个ADC各自单独采数据都是正确的合在一起后ADC2或者ADC3的数据比ADC1慢了一拍问题一定出在三重模式的配置上。首先要确认ADC1的Mode字段确实是Triple regular simultaneous mode而不是某个ADC自己开了独立模式其次检查ADC2、ADC3的触发源规则同步模式下统一走ADC1的外部触发路径它们的触发源要保持一致。还有一个隐藏坑扫描模式。CubeMX在多重模式下会自动开启Scan Conversion Mode但如果某个ADC只配了一个Rank扫描模式的表现和普通模式差异不明显。如果你改了Rank数量一定要回到CubeMX里重新确认扫描模式没有被意外关掉。另外如果三个ADC采样同一个直流电压数值理论上应该非常接近。如果AD值相差很多先查参考电压是否都一致、采样时间是否相同、校准是否都做过。硬件上三路信号的输入阻抗不同也会造成读取差异建议信号源都经过运放缓冲再进ADC引脚。4.5 调试三重ADC时的几个小技巧第一片上调试器里打开Live Expressions直接观察adc_buf[0]、adc_buf[1]、adc_buf[2]的值。给三路输入分别接不同的直流电压如果数组里看到三个稳定且不同的值说明基本链路是通的。不要一上来就用正弦波做FFT验证变量太多不好定位。第二在HalfCplt和Cplt回调里翻转一个GPIO用示波器测这个GPIO的周期。这个周期就是半块缓冲区的DMA写入时间可以直接验证你的采样率是否符合预期也能判断CPU处理窗口到底有多紧张。第三遇到数据偶尔跳变优先怀疑DMA优先级和总线竞争。把ADC的DMA设为最高优先级并把不需要的DMA流暂时关闭做对照可以很快确认是不是DMA带宽不够。第四如果使用的是我之前说的软件触发 Continuous模式系统上电后要先稳定几个毫秒再开始处理数据因为ADC第一次转换时可能会受到上电带来的基准电压建立过程影响。做测试时直接丢弃前几十帧是成本最低的稳定手段。这套三重ADC DMA双缓冲的配置我后来在多个项目里复用包括三路电流同步采样、双通道振动信号采集加一路转速信号等场景。配置流程基本一致只是每次根据信号频率去调整采样时间、缓冲长度和触发源。做同步采样稳定压倒一切配置上少一点高级感多一点可预见的确定性反而是最省心的。最后分享一个小习惯每次新建工程时我会先在CubeMX里把ADC1的DMA设置截图留档然后把启动函数的关键参数写进代码注释里。三重ADC的模式配置牵扯的寄存器关联比普通ADC多一旦隔了几个月再回来维护光靠记忆很容易漏掉DMA Continuous Requests这种细节。有据可查比临时翻参考手册快得多。
返回列表