尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32H743 16位ADC三通道DMA并行采样实战与踩坑指南

STM32H743 16位ADC三通道DMA并行采样实战与踩坑指南 这块板子我在几类采集项目里用了有大半年了ADC从12位换到16位之后很多人第一反应是“精度翻四倍那直接把分辨率拉满不就行了”但真正把STM32H743的ADC玩明白之后你会发现16位分辨率只是入场券后续的过采样配置、DMA搬数方式、Cache一致性问题才是真正决定采集质量和系统稳定性的地方。这篇就基于我手头一个三通道并行采样的小项目完整走一遍CubeMX配置、HAL库代码实现、性能调优和踩坑记录给准备用H743做高精度采集的朋友做个参考。内容围绕16bit分辨率、三通道DMA并行采样展开适合正在做数据采集、信号分析、仪器仪表类项目的开发者也适合刚从F1/F4系列转到H7系列的嵌入式工程师。1. 项目整体设计与选型思路1.1 为什么选STM32H743做16位ADC采样STM32H743这颗芯片最吸引人的地方是内置的ADC硬件过采样能力可以把原生12位ADC通过可配置的过采样率拉高到16位输出这在同级别的MCU里并不多见。对比我之前常用的STM32F103和F407F1/F4系列的ADC只有12位想要更高精度要么外挂独立的16位ADC芯片比如ADS1115、ADS1263要么用软件多次采样求平均来“伪造”高分辨率但代价是CPU占用高、实现麻烦而且噪声环境下软件平均的稳定性远不如硬件过采样。H743的16位输出本质上是硬件过采样器在工作内部以12位精度连续采样16次把结果做累加并右移得到16位数据。这个过程完全由ADC外设自己完成不占CPU也不占用DMA带宽之外的额外资源。硬件过采样不仅提升了分辨率还能将量化噪声均摊到更宽的频带内相当于自带了一个低通滤波器对付白噪声类的随机干扰特别有效。选择H743的第二个原因是它有3个独立的ADC模块ADC1、ADC2、ADC3配合DMA可以实现真正的同步采样。比如要做三相电压电流采集、双通道振动信号分析这类需要各通道相位严格一致的场景单ADC扫描模式会有通道间的时间偏差而多ADC同步采样模式可以做到严格同时刻采样。第三点是主频和总线带宽足够。H743跑到480MHzAHB总线和DMA带宽充裕ADC采样率即使拉到几MspsDMA也能轻松搬数CPU基本不怎么参与。1.2 三通道“并行采样”的两种理解标题里“三通道DMA并行采样”这句话其实有点歧义实际项目里通常有两种理解方式第一种是单个ADC模块的扫描采样。一个ADC按顺序扫描三个通道每次转换完一个通道数据由DMA自动搬走三个通道的数据分别放在DMA缓冲区里的不同位置。这种是最常见的用法CubeMX里也最容易配置适合三路信号之间没有严格相位要求的场景比如三路温度、三路压力、三路缓慢变化的模拟量。严格来说这不是“并行”因为三个通道是一个一个轮流转换的。第二种是三ADC同步采样或者叫所谓的“并行采样”。ADC1、ADC2、ADC3各负责一个通道通过硬件触发信号同时启动转换三个通道在同一时刻完成采样做到真正的时间同步。这种方案适合电网信号采集、电机三相电流采集、声学阵列这类对通道间相位一致性有硬性要求的应用。我这次项目需求是三个通道的同步采集但为了把两种方案都讲清楚后面第2部分先讲单ADC三通道扫描DMA的实现这是大部分人的入门配置第4部分再重点讲三ADC同步采样的整个配置和调优过程。两种方案在CubeMX里的配置思路不太一样但底层用到的DMA机制是相通的。1.3 用CubeMX而不是纯手写寄存器的原因H7系列的时钟树比F1/F4复杂太多ADC时钟源可以是PLL2P、PLL3P也可以走AHB分频手动翻参考手册配寄存器非常容易配错而且一旦配错很难查。CubeMX的好处是图形化点击时钟树会自动校验合法范围比如ADC时钟超限时界面直接飘红这能提前拦截大部分低级错误。我项目里用到的具体硬件环境是主控STM32H743ZIT6外部25MHz晶振核心板/Nucleo开发板三个通道分别接了0到3.3V的模拟信号参考电压直接用的VDDA板载3.3V。如果对VREF精度要求更高可以外接精密基准源到VREF引脚或者使用芯片内部的VREFBUF缓冲器。在CubeMX里选好芯片后下一步先把时钟树配置好HSE 25MHzSYSCLK 480MHzADC时钟从PLL2P引出目标频率定在36MHz左右因为16位过采样模式对ADC时钟频率敏感后面实测发现频率越高噪声越大这部分在调优章节展开说。2. CubeMX配置16bit ADC与DMA循环采样的关键点2.1 ADC时钟与分辨率设置进入Analog → ADC1配置界面首先把Resolution设置为ADC_16_BITS。注意CubeMX里选16位分辨率后实际上会自动打开过采样器Oversampling这就是硬件过采样生效的地方不需要你额外再去翻过采样配置。接着设置ADC时钟。CubeMX左侧的Clock Configuration界面里ADC12的时钟源选择PLL2P。我这里把PLL2的P分频设为2PLL2源时钟为72MHz得到36MHz的ADC时钟。为什么不直接拉到50MHz因为16位分辨率下内部过采样器每个输出结果需要16次内部转换ADC时钟越高内部RC充电时间越短反而容易引入失调和增益误差。实测看36MHz下配合恰当的采样时间噪声水平最低后面4.1节有对比数据。采样时间Sampling Time我这里统一设置为64.5 cycles。H743的ADC采样时间选项从1.5周期到810.5周期都有通道输入阻抗高、信号源内阻大时采样时间太短会导致采样电容没充满16位分辨率下高位误差明显。对常规的电压信号源内阻小于1kΩ64.5 cycles是性能和精度的平衡点。如果信号源阻抗很高比如直接接了一个10kΩ电位器分压建议直接把采样时间拉到387.5 cycles别心疼转化速度高精度采集项目里精度优先。关于VREF在CubeMX的ADC配置页有VREF选项如果板上有外部精密基准就选External VREF没有就保持VDDA。我用的Nucleo板直接用的VDDA3.3V参考后面换算公式里按3.3V算。2.2 三通道扫描与转换顺序配置配置三个通道时先在ADC1页面下选择Channel 6对应PC0、Channel 7对应PC1、Channel 8对应PC2。具体哪个引脚对应哪个通道CubeMX会在芯片图上用绿色小方块提示点一下引脚再选功能即可不用死记硬背。我用这三个引脚举例实操时完全可以根据自己的硬件连接换。关键参数有三个Number of Conversion填3。这就是扫描模式下要转换的通道数。Scan Conversion Mode使能Enabled。Continuous Conversion Mode使能Enabled。Enable Continuous Conversion后ADC转换完一轮会立刻开始下一轮配合DMA循环模式就能做到后台持续不断的采样流应用层只要不停从DMA缓冲区取最新数据即可。三个通道的转换顺序默认按Rank1、Rank2、Rank3排列这个顺序非常重要因为DMA缓冲区里数据就按这个顺序排列。我们后面解析数据时buf[0]对应Rank1buf[1]对应Rank2buf[2]对应Rank3。如果要调整通道顺序直接拖Rank顺序即可。2.3 DMA配置与数据对齐DMA这块是很多新人翻车的地方。ADC1有三通道、16位数据DMA配置必须按如下参数来DMA RequestADC1DirectionPeripheral To MemoryModeCircular循环模式Peripheral Address IncrementDisableDMA每次读的都是ADC数据寄存器地址不变Memory Address IncrementEnable数据依次写入内存三个位置Peripheral Data WidthHalf WordMemory Data WidthHalf Word为什么要用Half Word因为16位ADC数据是2字节如果用Word模式搬运H743的ADC数据寄存器会默认左对齐还是右对齐要额外设置容易搞混。Half Word是从左到右最自然、最不容易出错的选择DMA每触发一次搬2字节搬到内存偏移2。还有一个在CubeMX里很容易忽略的选项DMA Continuous Requests在ADC1的DMASettings底部或者Parameter Settings里一定要Enable。这个位控制ADC是否需要外部触发来启动DMA连续请求如果没打开DMA可能只有第一次转换时搬运后面就不再传输了典型现象就是只有buf[0]有数据后面都是0。2.4 过采样配置16位分辨率背后的机制这里单独把过采样拿出来说是因为很多人配完16位后发现数据和12位差不多甚至更差这就是没理解过采样的本质。CubeMX选择16位分辨率后ADC的Oversampling配置会自动设成OSSR1616倍过采样位移数Right Shift会自动设成4也就是累加16次后右移4位等价于除以16把结果映射回16位范围内。这个配置可以手动改Shift越多输出动态范围越小但能换取更高的有效位数。最需要记住的公式是实际输出速率 ADC时钟 / (采样周期 固定转换周期) / 过采样率。比如36MHz ADC时钟、64.5周期采样时间、16位分辨率时一次内部转换大约需要64.58.5个周期约2.03µs再进行16次过采样实际每个16位结果的输出时间大约32.5µs对应约30.7kSPS。如果你觉得这个速率太低可以缩短采样时间或者关掉过采样回到12位但那就不是这个项目的初衷了。所以做设计规划时首先要算清楚16位分辨率下每个通道每秒最多出多少数据。三通道扫描模式下DMA缓冲区每更新一轮的时间是三倍单通道转换时间。对多数传感器采集场景30kSPS的吞吐量完全够用了如果要追求更高速度就只能用8位/10位/12位分辨率。3. 代码实现与数据解析3.1 启动校准16位ADC不可跳过的一步CubeMX生成的代码已经包含了ADC初始化但H7系列的ADC在使能转换前必须做校准否则16位模式下你会看到输出值整体偏移严重时甚至出现固定的跳变。校准代码要在HAL_ADC_Start_DMA之前调用而且每次上电都要执行HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED);注意第二参数是单端模式如果用了差分输入要改成ADC_DIFFERENTIAL_ENDED。校准期间ADC时钟必须保持稳定所以我把校准放在所有外设初始化完成后、开启中断之前的main函数里。校准之后等待一小段时间让内部电压稳定然后就可以启动DMA采样了。启动DMA采样的代码非常简单uint16_t adc_buf[3] {0}; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 3);这里比较容易踩坑的是第三个参数。HAL库定义的是Length在DMA配置成Half Word时它的含义是“要搬运的Half Word数量”不是字节数。我们三通道写3就够了如果这里写成6DMA会搬运6个Half Word缓冲区越界写入可能把其他变量冲掉。3.2 等待转换完成并取数据DMA循环模式下adc_buf会被硬件持续刷新。你可以在主循环里轮询一个标志位也可以注册DMA传输完成中断回调我习惯用回调volatile uint8_t adc_conv_complete 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_conv_complete 1; } }然后在主循环里判断标志位后读取数据。读取时要留意DMA还在后台连续写缓冲区如果主循环读取速度跟不上DMA刷新速度可能出现前半截是新数据、后半截是旧数据的“撕裂”情况。精度要求不高的场合直接读没问题要求高的话用DMA半传输中断把缓冲区拆成前后两半读前半部分时DMA写后半部分这是标准的双缓冲思路。CubeMX的DMA设置里勾选Half Transfer Interrupt和Transfer Complete Interrupt然后在HAL_ADC_ConvHalfCpltCallback里处理即可。3.3 三通道数据顺序与电压换算模拟量输出在0到3.3V之间所以16位ADC量程是0到65535对应0到3.3V。电压换算公式很直接float voltage[3]; for (int i 0; i 3; i) { voltage[i] (float)adc_buf[i] * 3.3f / 65535.0f; }注意这里分母是65535还是65536要看ADC的实际阶数SAR ADC满量程输出是0xFFFF对应VREF所以用65535。如果你用的是厂商手册里明确写了“LSB VREF/2^16”那就用65536不同芯片定义有差异H743这里按65535算实测更贴近万用表的读数。DMA缓冲区里的顺序严格对应你在CubeMX里配置的三个Rank顺序比如我把Rank1配成Channel6Rank2配成Channel7Rank3配成Channel8那buf[0]就是PC0通道的值buf[1]是PC1通道的值buf[2]是PC2通道的值。万一实际读出来发现通道对不上回去检查CubeMX里Rank顺序不要先怀疑硬件。3.4 数据滤波与平滑处理即使有硬件过采样16位ADC在小信号输入时仍然会有±几个LSB的跳动这是正常的。为了进一步压噪声我在应用层加了一级滑动平均滤波窗口大小根据通道特性选择#define FILTER_N 32 uint32_t filter_sum[3] {0}; uint16_t filter_buf[3][FILTER_N]; uint8_t filter_idx[3] {0}; uint16_t moving_average(uint8_t ch, uint16_t new_value) { filter_sum[ch] - filter_buf[ch][filter_idx[ch]]; filter_buf[ch][filter_idx[ch]] new_value; filter_sum[ch] new_value; filter_idx[ch] (filter_idx[ch] 1) % FILTER_N; return (uint16_t)(filter_sum[ch] / FILTER_N); }这个滤波器本质上就是一个FIR滑动平均能有效抑制高频随机噪声。缺点是有相位延迟如果后续要做实时控制或者边沿检测窗口不要设太大。还有一点滤波应该放在电压换算之前做这样在整数域运算速度快不会引入浮点误差累积。4. 性能调优实测记录4.1 采样时钟与采样时间的权衡这块我专门花了两个晚上做了对比实验。条件是同一块板子、同一路输入信号1.2V左右的直流电压分别改变ADC时钟频率和采样时间用串口把连续采样的1000个16位数据传上来算标准差和最大峰峰值。测试结果整理成表贴在下面ADC时钟采样时间输出平均值标准差最大峰峰值50 MHz16.5 cycles236524.8 LSB32 LSB50 MHz64.5 cycles236632.6 LSB18 LSB36 MHz64.5 cycles236711.5 LSB10 LSB36 MHz387.5 cycles236691.2 LSB8 LSB可以看到ADC时钟从50MHz降到36MHz噪声几乎降了一半。这说明对H743这颗ADC来说时钟频率对内部比较器稳定性的影响很明显。长期跑下来我直接把ADC时钟锁定在36MHz采样时间保持64.5 cycles噪声在1.5 LSB左右对于16位采集来说这个表现已经很理想了。如果信号源阻抗高采样时间加长带来的好处比降低时钟频率还要明显。因为采样电容充电需要时间时间不够充不满误差是确定性误差而不是随机噪声表现为读数偏低或通道间串扰。这类问题提高采样时间立竿见影代价是转换速率下降。4.2 真正的并行采样三ADC同步模式前面1.2节提到过严格相位同步要用三ADC模式。CubeMX里这样配把ADC2和ADC3分别使能分辨率、采样时间、通道数量全部和ADC1保持一致。然后回到ADC1的参数设置在Mode里找到Dual Regular Simultaneous Mode或者Multi-ADC模式把ADC1设为MasterADC2和ADC3设为Slave同时找一个触发源一般用TIM1的TRGO或者软件触发。触发设置这里有个关键点三个ADC必须由同一个事件触发才能保证同步。CubeMX里可以把ADC1的External Trigger设为Timer TriggerADC2和ADC3的Trigger Source跟随Master。这样当定时器产生触发信号时三个ADC同时启动转换。DMA的配法和单ADC扫描略有不同。三ADC同步模式下DMA可以从ADC1的CDR寄存器读取组合数据这个寄存器把ADC2和ADC3的转换结果打包在一起一次搬运能同时拿到两个通道的数据加上ADC1的DR寄存器的数据三通道数据一次性全到了。有些情况下也可以配置成使用多个DMA通道分别从三个ADC读取但组合寄存器的方式更简练DMA只需要一条流。同步采样后三个通道数据的时序偏差理论值只取决于三个ADC的内部转换一致性通常在几十ns以内对于电网50Hz信号来说完全够用。实际测试我用同一信号分三路接三个ADC通道读出来的波形几乎完全重合相位差小于示波器能分辨的级别。4.3 缓存一致性优化H7的DCache坑H7内核有L1 CacheCPU读主内存会先把数据缓存到DCache里。DMA是直接写主内存的不会通知Cache于是会出现CPU读到的是Cache里被改之前的旧数据或者DMA覆盖了Cache缓存的行导致数据不同步。这个坑在长时间跑DMA采集时特别隐蔽刚开始启动的几百个数据看起来没问题跑一会儿数据突然变成全0或卡在某个旧值就是Cache一致性出问题了。解决办法有两种。第一种最省事在CubeMX的MPU配置里把DMA缓冲区所在的SRAM区设为Non-cacheable这样所有读写都直接走主内存不经过Cache。缓冲区如果用AXI SRAM0x24000000这块内存属性改一下就能用。第二种是软件维护在读取DMA缓冲区前做一次Cache无效化操作SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));在写入缓冲区比如准备让DMA写入之前执行一次SCB_CleanDCache_by_Addr。这套逻辑在CubeIDE里并不复杂但很多人不知道网上搜H743 DMA数据不对一半以上都是这个问题。我的建议是如果项目里只有这一块DMA缓冲区直接用MPU设成Non-cacheable最简单省得每次读都要手动刷Cache代码也干净。如果整个系统对性能要求极高Cache要保持开启那就把双缓冲和Cache维护配合起来用。4.4 中断频率与CPU占用优化三通道16位DMA循环采样如果每次转换完成都触发中断中断频率就是每秒30k次左右虽然H7跑480MHz能扛住但白白消耗CPU。更合理的做法是DMA传输完成中断或者半传输中断每512次或更多次转换中断一次中断里只置标志位不做事主循环批量处理数据如果想进一步降低CPU占用可以开一个FreeRTOS任务把DMA回调里的标志位作为任务通知处理数据的逻辑放到低优先级任务里避免中断处理时间过长影响实时性。实测对比下来默认每次转换都中断的方案CPU占用约8%到10%改成半传输中断后降到1%以内数据一个不少延迟稍微增加了一点但对多数采集项目来说完全可接受。5. 常见问题与排查技巧5.1 三通道DMA采样问题速查表把我在调试时遇到的典型问题整理成了一张表方便你逐个对照排查现象可能原因排查方向所有通道数据全是0DMA没有触发或ADC没使能检查DMA Continuous Requests是否打开HAL_ADC_Start_DMA是否调用只有第一个通道有数据DMA循环未配置成功确认DMA Mode为CircularMemory Increment为Enable数据值明显偏大或偏小参考电压不对确认VDDA实测值或换用外部基准数据跳动几十LSBADC时钟太高或者采样时间太短降ADC时钟增大采样周期三个通道数据顺序错乱Rank顺序与预期不符回CubeMX检查Channel Rank排列顺序16位分辨率下噪声比12位还大过采样没生效检查Resolution是否真的设成16位DMA宽度是否Half Word跑一段时间数据卡住Cache一致性问题用MPU把缓冲区设成Non-cacheable或手动Invalidate DCache电压换算结果和万用表差太多换算公式中的参考电压不是3.3V用万用表实测VDDA代入实际值5.2 16位数据为什么忽高忽低这个现象我见过很多次排查了半天最后发现是参考电压的噪声把数据带偏了。H743的VDDA如果直接和数字电源混在一起高速翻转的IO、通信接口都会在参考电压上叠加毛刺ADC采到的高位数据就会跟着乱跳。NVIC这里写的是一个典型场景系统里还有SPI、UART在跑尤其SPI时钟线翻转时参考电压毛刺特别明显。解决办法是给VDDA加LC滤波或者用单独的LDO给模拟部分供电。如果板子已经定型改不了硬件就把ADC采样时间拉长让采样时刻尽量错开数字电路同时翻转的瞬间。这个技巧虽说治标不治本但能显著改善短期波动。5.3 采样率计算与DMA长度容易搞错的地方很多人设DMA长度时会习惯性写字节数但HAL库的HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 3)第三个参数在Half Word模式下是半字的个数。三通道就写3不是6。写成6的结果就是缓冲区连续被写6个半字后三个半字写到缓冲区外面去了轻则覆盖别的变量重则直接HardFault。采样率的计算也要仔细三通道扫描模式下一轮数据是三个通道各采一次总时间约等于单通道转换时间乘以3。假设ADC时钟36MHz单通道16位转换时间算下来约2.03µs三轮合计约6.1µs对应三通道总更新速率约164kSPS但每个单独通道的更新速率只有约54.7kSPS。如果需要三通道同步更新且速率更高就得考虑三ADC同步模式因为三个通道是同一时刻转换总吞吐量相当于单个ADC的转换时间不乘通道数。别被这个差别误导实际决定你能跑多块的不是标称转换时间而是信号源的阻抗和精度要求。抗阻太高的情况下硬拉采样率16位分辨率是保不住的。5.4 从F1/F4转过来的隐藏坑GPIO模拟输入配置F1时代ADC引脚不需要特别配置但H7系列对GPIO的模拟输入模式要求更严格。CubeMX里选择了ADC通道后GPIO会自动配置为Analog模式不需要手动改。但如果你是从标准库或者旧工程迁移过来的没把引脚设置成Analog而是保持了GPIO_MODE_ANALOG之外的模式ADC读到的值会明显异常常见现象是输入电压到某个阈值后读数不再上升出现平台期。解决方法是重新生成CubeMX工程确认每个使用到的ADC通道对应的GPIO引脚确实配置成了Analog模式。同时检查GPIO的上下拉电阻如果引脚历史配置里带着下拉或上拉也会改变外部信号的分压影响采集精度。去掉上下拉保持浮空输入让外部电路自己决定电平。这算是一个比较细节的问题但是排查起来可能耽误一晚上。我把这个放在最后说也是想提醒从F1/F4老平台迁移过来的朋友H7的ADC链路整体比老芯片敏感每一环都要仔细确认。最后再分享一个我自己的习惯做ADC采样类项目一定先用一颗稳定的直流电压比如用一个精密电阻分压得到1.000V验证全链路精度再接入真实信号。先用固定值验证能排除掉大部分配置问题。等固定值读数稳定到±2 LSB再上真实波形后面遇到问题就集中在信号调理和算法部分不至于和底层配置混在一起瞎猜。这算是笨办法但确实能省不少力气。
返回列表