先说结论:如果你刚从F103或者F407的老工程迁移到STM32H743上做ADC多通道采样,大概率会在CubeMX配置阶段就踩坑。这芯片的ADC模块和F4完全不是一个思路,多了一个“快速通道/慢速通道”的概念,DMA请求逻辑也更拧巴,再加上网上教程大多停留在“能跑通”的层面,很少有人讲清楚采样周期、扫描顺序、DMA缓冲区排列之间的连带关系。我最近在调一块8路模拟量采集板,把踩过的坑、验证过的配置和最终能稳定运行的代码整理成这篇,给准备上手H743的兄弟一个参考。
这套内容适合谁?正在用STM32H743做数据采集、工业变送器、FOC电流采样、电池监测,反正只要是“多通道ADC+DMA搬运数据”这种标准玩法的人,都可以直接照抄配置思路。我会把CubeMX里那些默认参数是怎么回事、为什么这么配、实际跑起来会遇到什么问题都讲透。
1. 整体方案与设计思路:为什么H743 + 多通道 + DMA是绕不开的组合
1.1 H743的ADC资源比F4“豪横”在哪里
先看一眼H743的ADC硬件资源,这决定了我们后面所有配置的上限。H743内部有3个独立的12位逐次逼近型ADC,编号ADC1/2/3,最典型的特点是支持可编程过采样,可以输出最高16位转换结果。也就是说,你用H743做采集时,如果对速度要求不高、对精度和分辨率有要求,可以不外挂高精度ADC芯片,靠内部过采样把有效分辨率抬上去,这在天平、传感器变送器这类应用里很吃亏——不对,是很占便宜。
另一个是主频带来的优势。H743的ADC时钟最大可以跑到50MHz左右,12位分辨率下,加上最短采样周期2.5个时钟周期,单个ADC的理论转换速率能到3Msps以上。F4系列最高2.4Msps,F1更是只有1Msps级别,所以在采集高速信号时,H743具备直接采中频信号的能力,不需要上外部高速ADC。
H743的ADC还有一个F4没有的重要特性:通道分为快速通道和慢速通道。快速通道的输入等效电容和采样开关阻抗更小,可以用更短的采样时间;慢速通道则需要留出更长的采样时间,否则采样值会偏、会跳。这个细节稍后重点讲,很多人在配置阶段不看通道类型,直接把所有通道设成最低采样周期,结果数据一团糟。
此外,H743内部有一个可选的参考电压缓冲器VREFBUF,可以产生2.5V或2.9V的稳定内部参考,省掉外部基准芯片。ADC的VREF引脚上甚至可以悬空,直接使能内部缓冲就行,这个在精密采集场景里非常实用。
1.2 多通道采样为什么必须用DMA
这里先说一个基础问题:多通道扫描模式采集,DMA到底帮我们干了什么?
当你在ADC1上开启Scan Mode并配置了8个转换通道之后,ADC硬件会按你设定的顺序一个个转换。不开启DMA时,你只能在每次转换完成后,在中断里读取ADCx->DR寄存器。8个通道就是8次中断,CPU全程被打断,而且每次中断响应到读取寄存器的延迟都不确定,这会导致每次采样点之间的时间间隔不均匀,直接影响采样波形的质量,尤其是在分析相位差、计算有功功率这类场景下,中断延迟就是灾难。
使用DMA后,ADC每次转换完成会自动发出DMA请求,DMA控制器把ADCx->DR里的数据搬到内存缓冲区,整个搬运过程不需要CPU干预。CPU只在DMA搬运完一整轮(也就是所有通道都采完一遍)或者搬运到一半时收到一次中断,处理一次数据即可。对于多通道周期采样来说,这是效率最高的方案。
DMA的选择也很简单:H743的ADC1有独立的DMA请求信号,可以接到DMA1或者DMA2的任意空闲数据流上。我习惯用DMA1 Stream0,没有特殊原因,只是工程里统一约定。如果你用了DMA2做串口收发,ADC就接DMA1,避免两条数据流互相抢占带宽。
不过DMA也不是万能的。真正坑人的不是DMA本身,而是多通道扫描模式下DMA缓冲区里的数据排列顺序和连续转换时Buffer数据反复覆盖这两个问题,下面我会专门用两个章节来展开。
2. CubeMX里的关键配置项:每个参数都用在哪里
2.1 时钟树:ADC时钟的分频逻辑与上限
H743的ADC时钟源和F4完全不同,F4的ADC挂载在APB2总线上,时钟上限36MHz;H743的ADC时钟独立了,可以由PLL2P、PLL3P或者系统时钟AHB分频得到,最高支持到50MHz左右。在CubeMX的Clock Configuration页面里,默认工程可能给ADC1/2/3分配了50MHz时钟,这个值能用,但我建议根据实际采样率需求来确定,不用一味追高。
为什么要专门说ADC时钟?因为采样时间和转换时间都是按ADC时钟周期计算的。假设你采样周期配成16.5个周期,12位分辨率固定转换周期12.5个周期,那么一次完整转换需要29个ADC时钟周期。ADC时钟50MHz时,这个转换时间约0.58微秒,3个ADC开起来每秒能采一大片数据。但如果工程里系统时钟只有64MHz,ADC时钟被分得更低,同样的配置下转换时间会成倍拉长。实际项目里如果采样率要求不高,把ADC时钟调到25MHz左右更省功耗、更稳。
配置ADC时钟的方法:在CubeMX的Clock Configuration里,找到ADC12和ADC3的时钟源选项,可选PLL2P、PLL3P、per_ck等。一般选PLL2P,然后直接输入50MHz。如果你想偷懒,也可以直接在CubeMX里双击“ADC12”时钟线,改成AHB分频或者PLL分频,界面会告诉你当前值是否超出上限。
2.2 ADC参数面板逐项解读:Scan、Continuous、DMA请求的关系
CubeMX里打开ADC1,参数面板一堆选项,很多人看得眼晕。实际上最重要的就那几项。
Resolution分辨率。通常选12位,如果对速度有极致要求可以选8位或6位,转换时间会相应缩短。注意14位和16位是通过过采样实现的,不是硬件原生,后面我会单独说过采样配置。
Scan Conversion Mode扫描模式。多通道采集必须开启。开启后ADC会按Rank顺序依次转换所有通道,每转完一轮产生一次"序列转换结束"事件。注意:扫描模式开启后,中断标志位的位置会变,EOC是每转一个通道都置位,而SEQ(序列结束)标志才是整轮结束,用DMA时不用关心中断标志,DMA传输完成就代表一轮结束。
Continuous Conversion Mode连续转换模式。如果只采集一轮,选Disable,配合外部触发或软件触发;如果希望ADC自己不停地从头到尾循环转换,就选Enable。配合DMA Circular模式,可以实现“后台无限采集”,CPU完全不参与。这个模式的缺点是功耗高、数据一直更新,个人建议:优先用Continuous+Circular,然后在DMA回调里做数据处理,这是最省事的结构。
Discontinuous Conversion Mode不连续模式。这个模式容易让人迷惑。通俗讲,它会把Rank序列切成一小段一小段,靠外部触发信号一段段推进。常规采集项目用不到,如果选了反而会导致DMA始终收不齐所有通道的数据,建议保持Disable。
DMA Continuous Requests。这个选项决定ADC每转换完一个通道就发一次DMA请求,还是只在转换完一个序列后发一次请求。如果开了扫描模式+DMA,建议选Enable,这样数据能连续搬运到内存,配合DMA Circular模式形成流水线。如果选Disable,一轮转换只发一次DMA请求,数据量小、传输不连续,实际使用中我基本不会这么配。
Number Of Conversion就是总通道数,比如8个通道就填8。下面的Rank列表里,每个Rank对应一个通道,顺序就是DMA缓冲区里数据的排列顺序,这个顺序后面要拿来跟硬件原理图对一遍,非常重要。
2.3 采样周期的选择:快速通道与慢速通道
采样周期是ADC参数里最容易被忽略、但影响实际数据质量最大的选项。H743的ADC通道分两组:
- 快速通道:输入充电时间需求短,采样周期最低可以配到2.5个ADC时钟周期。
- 慢速通道:输入电容较大,充电时间需求长,采样周期最低8.5个ADC时钟周期。
更准确地说,H743的ADC采样时间配置项一般是:2.5 / 8.5 / 16.5 / 32.5 / 64.5 / 387.5 / 810.5(单位是ADC时钟周期)。如果你选择了2.5,而它实际是一个慢速通道,那么采样电容可能还没充满,采到的电压会比实际值偏低,特别是一些高阻抗信号源,会偏低得离谱。
怎么判断通道是快速还是慢速?CubeMX里鼠标停在通道旁边或查看数据手册,更直接的方法是看引脚号。一般来说,直接连在ADC输入引脚上的大部分是快速通道,部分多路复用脚位会是慢速通道。稳妥的做法:除非你非常确定信号源阻抗极低,否则采样周期至少选16.5以上。温度传感器、分压电阻网络这类常见的模拟信号,建议32.5起步。
采样时间的计算公式如下:
总转换时间 = 采样周期 + 固定转换周期(与分辨率相关)
其中,12位分辨率时固定转换周期为12.5个ADC时钟周期,10位是10.5个,8位是8.5个。
举个例子:ADC时钟50MHz,12位分辨率,采样周期16.5个周期。那么单个通道转换时间 = (16.5 + 12.5) / 50MHz = 0.58微秒,8个通道一轮就是4.64微秒,最大扫描频率约215kHz。如果你打算采集50Hz交流信号做FFT,这个速率绰绰有余。如果你要采集100kHz以上的信号,就得把采样周期压到2.5或8.5,并且用快速通道。
3. 实操过程:从CubeMX到代码的完整落地
3.1 第一步:CubeMX工程配置实例
我用ADC1做8路采集,依次是:PA0、PA1、PA2、PA3、PA4、PA5、PB0、PB1。这几个引脚在H743上全部对应ADC1的通道。打开CubeMX,先把这8个引脚在Pinout视图中设为ADC1_INP0到ADC1_INP7。
然后进入Analog → ADC1,按下表配置:
| 参数项 | 配置值 | 说明 |
|---|---|---|
| Mode: INP0~INP7 | Enabled | 8个通道全部打开 |
| Resolution | 12 bits | 默认即可 |
| Scan Conversion Mode | Enabled | 必须开启 |
| Continuous Conversion Mode | Enabled | 配合DMA循环 |
| Discontinuous Conversion Mode | Disabled | 常规项目用不到 |
| DMA Continuous Requests | Enabled | 连续搬运 |
| Number of Conversion | 8 | 通道数量 |
| Rank 1~8 | 对应INP0~INP7 | 顺序按硬件定义 |
| Sampling Time | 32.5 Cycles | 保守起见 |
| DMA Settings标签页 | Add DMA,Mode Circular | 关键 |
DMA配置里要特别注意三个点:
- Mode设为Circular,这样DMA搬运完一轮后自动重新从头开始,配合ADC的Continuous模式形成无限循环。
- Data Width,外设和内存都设为Half Word(16位),因为ADC的DR寄存器是16位宽度,ADC分辨率最大也只用到16位。
- Memory Increment设为Enable,否则每轮数据都会写到缓冲区同一个位置,后一个数据覆盖前一个数据。
3.2 第二步:DMA缓冲区的排列顺序问题
这是我觉得整个CubeMX配置里最值得单独拎出来讲的地方。你开启8通道扫描后,DMA缓冲区里会怎么排列?
答案:按Rank的顺序排列,不是按引脚号排列。Rank 1对应INP0,Rank 2对应INP1,依此类推。也就是说,一次完整扫描转换结束后,DMA缓冲区从首地址开始依次存放CH0、CH1、CH2……CH7的数据。这个顺序是由CubeMX生成的ADC_ChannelConfTypeDef中Rank字段顺序决定的,你在代码里改动Rank顺序,DMA数据顺序也跟着变。
所以代码里取通道数据的逻辑非常简单:
#define ADC_CH_NUM 8 uint16_t adc_buf[ADC_CH_NUM]; // 假设在DMA传输完成回调中取数据 uint16_t ch0_val = adc_buf[0]; uint16_t ch1_val = adc_buf[1]; // ... 依此类推如果你在CubeMX里调整了通道顺序,比如把INP5放到Rank 1,那么adc_buf[0]就是INP5的数据,而不是INP0。因此强烈建议:配置Rank顺序时,直接让硬件原理图上的物理通路与DMA buffer索引一一对应,并且把每个通道的物理意义写在代码注释里,防止后续接线的人换了一路信号,数据全部错位,排查半天还找不到原因。
3.3 第三步:代码层的正确启动顺序
CubeMX生成工程后,默认已经生成了MX_ADC1_Init()和MX_DMA_Init()函数,但这些函数只是初始化寄存器,不会启动转换。你需要在主程序里按顺序做三件事:校准、启动DMA转换、开始处理数据。
H743的ADC内部有个校准机制,上电后如果不校准就开始转换,结果会有几十个LSB的偏差。校准函数需要在整个ADC处于非转换状态时调用一次:
HAL_ADC_ExCalibration_Start(&hadc1);然后启动DMA传输:
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_CH_NUM);adc_buf是缓冲区,长度是8个uint16_t。ADC_CH_NUM是8,意思是每次采样多少个点由DMA搬运到缓冲区。
如果CubeMX里把Continuous Conversion Mode设成了Enable,并且DMA是Circular模式,那么HAL_ADC_Start_DMA调用一次,之后ADC就会一直在后台转换,DMA会不断把数据刷新到adc_buf。
取电压值的换算公式:
#define VREF_VALUE 3.3f float voltage = (float)adc_buf[0] * VREF_VALUE / 4095.0f;如果使用了内部VREFBUF产生了2.5V参考,那这里的VREF_VALUE就换乘2.5。
在DMA传输完成中断里,系统每次一轮数据搬完会调用一次HAL_ADC_ConvCpltCallback:
volatile uint8_t adc_data_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { for (int i = 0; i < ADC_CH_NUM; i++) processed_buff[i] = adc_buf[i]; // 拷贝一份,防止下次搬运覆盖 adc_data_ready = 1; } }很多人的误区是直接在DMA传输完成中断里做换算或者FFT、滤波。其实这样极不推荐,因为中断里占用时间越长,对实时采集的影响越大。正确做法是在回调里做轻量级拷贝,把数据复制到处理缓冲区,然后置一个标志位,在主循环里根据标志位去处理数据。实测下来这个模式在H743上可以很稳定地跑。
4. 常见问题与排查技巧实录
4.1 数据全是0xFFF或者0x000
我见过最多的情况是第一版工程跑起来,串口打印adc_buf,八个通道全是0xFFF,或者全为0x000。全0xFFF说明每个通道的输入引脚实际悬空或者被拉到高电平,ADC本身没工作,因为H743的ADC引脚没有内部下拉,悬空时读数接近满量程。全0则是引脚被拉到地,或者外部驱动能力不够。
但还有一种情况是没有调用校准函数,并且ADC时钟配置错误导致转换结果异常。先确认CubeMX里ADC时钟是不是有值,之前有个兄弟把ADC1/2时钟源配成了Disabled,结果所有ADC寄存器读出来全是复位值。检查方法很简单:在MX_ADC1_Init里hadc1.Init.ClockPrescaler如果被设成了0(对应禁止),就说明时钟配置没生效。
4.2 DMA收到的数据顺序和物理通道对不上
这个问题几乎每个做多通道采集的人都会经历一次。现象是:明明配置的是PA0、PA1、PA2……结果串口打出来的数据,第0个通道显示的是PA4的电压。
造成这个问题的两个原因:
一是CubeMX生成的Rank顺序和你预期不一致。比如你重新拖拽了引脚映射,CubeMX会自动重新排列Rank列表,如果你在界面看不清,直接去代码里看MX_ADC1_Init中的通道配置顺序。
二是你在代码里手动改了ADC_ChannelConfTypeDef的Channel字段,却没有同步改Rank。记住:
sConfig_ch1.Channel = ADC_CHANNEL_5; sConfig_ch1.Rank = 1;上面的配置虽然把Channel设置成了CH5,但放在Rank 1的位置,意味着DMA第一个数据就是CH5。Rank序号决定DMA排列顺序,Channel字段决定读哪个引脚。很多人只改Channel不改Rank,最后通道对不上,这不是H743的锅,是配置逻辑理解错了。
4.3 DMA数据一直在刷新,我怎么判断读到的数据是“新”的还是“旧”的
这个在使用Continuous + Circular模式时非常典型。特别是主循环处理速度慢、采样速率又高时,DMA搬运可能在你还没处理完上一轮数据时就已经覆盖了缓冲区。
解决思路有三种:
- 用DMA半传输中断和传输完成中断,把缓冲区拆成前后两半。前半轮的数据只在半传输中断里处理,后半轮的数据在传输完成中断里处理,处理时间只要小于半轮转换时间,就不会丢。
- 在DMA回调里做内存拷贝,拷贝完立刻置标志。主循环检测标志后处理拷贝缓冲区。
- 如果采样速率远超CPU处理能力,说明方案设计有问题,应该在DMA之前做硬件抽取或降采样,别让CPU硬扛。
至于判断“新旧”,工程上通常不追求精确到哪一次的样本,而是用HAL_ADC_ConvCpltCallback的频率作为数据刷新率依据。真正高精度的信号采集系统,会使用双缓冲模式,这里先不展开,后文会简单提一下。
4.4 采样数值波动大、上下几百个LSB的跳动
排除外部信号本身噪底大的情况,最常见的原因是参考电压不稳和采样时间不够。
H743内部虽然有过采样功能,但它不能消除参考电压的噪声。如果板上的3.3V是从DC-DC直接出来、纹波几百毫伏,ADC结果跟着一起跳,加再多的软件滤波都是治标不治本。我的做法是模拟供电和数字供电之间加磁珠330欧级别的隔离,VREF引脚接上10uF+100nF的滤波电容,并且尽可能靠近MCU引脚。有条件的话,直接启用H743内部VREFBUF,用内部基准。配置起来很简单:在CubeMX里把VREFBUF设为Enabled,模式选2.5V还是2.9V,ADC的VREF+引脚就不需要外部提供参考了。
采样时间不足的判断方法:把采样周期从32.5改成387.5,如果数据跳动幅度明显变小,说明之前确实是采样时间不够,应该优先提高信号源输出阻抗或增加前级运放缓冲,而不是依赖长采样周期。
4.5 连续转换模式下,怎么把某一路从扫描序列中临时剔除
有次需求是8个通道里某一路传感器偶尔失效,不想每次都采它,但又不想重新生成工程。这时候可以通过HAL_ADC_ConfigChannel动态改Rank列表,但要注意:修改通道前必须先调用HAL_ADC_Stop_DMA停止转换,否则参数写入时可能和正在进行的转换冲突,导致偶发死机或数据混乱。
示例代码:
HAL_ADC_Stop_DMA(&hadc1); sConfig.Channel = ADC_CHANNEL_5; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_32CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_CH_NUM);改完之后,新配置会在下一轮扫描时生效,不用复位。
5. 进阶玩法与实测心得
5.1 双ADC交替采样提升等效采样率
如果单ADC在12位分辨率下满足不了采样率,H743可以把ADC1和ADC2配成双模式,采样同一组信号。CubeMX里在ADC1的Mode中可以直接选择Dual regular simultaneous mode,配置好后两块ADC同时转换同一路信号,DMA请求由主ADC统一发出,缓冲区里会交替存放ADC1和ADC2的结果。
实际采样率可以翻倍,代价是占用两个ADC,不能同时采独立的多通道。这个玩法在FOC电流环采样里用得比较多,H743的3个ADC恰好可以分配成两路电流采样加一路母线电压采样,延时可以低到1微秒以内,很适合跑电机控制。
5.2 过采样滤波的妙用
H743的ADC自带硬件过采样,CubeMX里Oversampling开启后,可以把14位、16位档位打开。硬件过采样本质上是对转换结果做移位平均,等效于低通滤波,能明显降低随机噪声,而且不占用CPU。
不过要理解过采样的代价:过采样率每增加4倍,有效分辨率才提升1位,而转换速率降低为原来的四分之一。比如16位过采样需要64倍采样,那么你实际等效12位采样率直接被除以64。所以过采样只适合低速高精度场景,比如电池电压监测、温度采集,不适合电流波形分析。
我的建议:如果噪声主要来自数字开关导致的毛刺,更推荐在PCB布局上做文章,而非完全依赖软件过采样。软件滤波永远只能补救,不能从根源解决。
5.3 一个让我反复排查了两天的低级坑:DMA中断优先级
还有一个非常隐蔽的坑,在H743上容易出现:DMA的中断优先级设置不当,导致回调函数从不执行或者执行频率异常。
H743的中断优先级分组默认是抢占优先级+子优先级。如果把DMA流的抢占优先级设得比某个高频定时器中断低,而定时器中断里又有一个长时间任务,DMA的中断就会被反复延迟。当时我遇到的现象是:adc_buf数据是正常的,但HAL_ADC_ConvCpltCallback里面的计数变量增长频率不对,仔细查才发现DMA中断被串口中断阻塞了很久。
解决办法:在CubeMX的NVIC设置里,把DMA1_Stream0_IRQn的抢占优先级调成最低,比如0(最高优先级),或者至少高于长时间运行的中断。ADC数据搬运是实时性要求很高的操作,给它最高的中断优先级不丢人。
5.4 数据校验的小技巧:用正弦波信号驱动输入通道自检
说一个我自认为很实用的调试思路:在多通道ADC工程跑通之后,别急着接真实传感器,先拿信号发生器给每个输入通道灌一个1kHz、1Vpp的正弦波,然后用串口上位机或者Python脚本把adc_buf数据打出来画波形。
这一步能非常直观地验证三件事:
- 每个通道的数据顺序是否正确——波形出现的先后必须与Rank顺序一致。
- 采样时间是否足够——如果正弦波幅度看起来明显偏小或者波形有阶梯感,说明采样时间不够。
- 相位差是否正常——同一时刻采多路正弦波,理论上各路之间应该几乎没有相位差(取决于采样循环时间),如果某一路相位明显偏移,检查那个通道的前级电路。
当时我在一个积分为高阻抗输出的传感器上发现了波形钝化的问题,就是靠这个正弦波自检抓出来的。回头想,省下的排查时间远超调那根信号线占用的十分钟。
5.5 关于“相位差”这个搜索热词多说一句
最近看到一些搜“相位差 stm32h743”的人,多半是想用ADC同时采集电压和电流两路信号,然后通过反正切计算相位差,做功率因数计算。用多通道ADC做这事有个天然限制:扫描模式下多路信号不是真正同步采样的,而是有一个微小的转换时间差。如果两路信号频率是50Hz,这个时间差造成的相位误差通常可以忽略;但如果信号频率到了kHz级别,就必须考虑这个误差。
要精准测相位差,方案有两个:一是用ADC1和ADC2的双同步模式,让两路信号严格同拍采样;二是把两个采样点之间的时间差固定下来,在算法里补偿这个固定偏移。第一个方案硬件配置简单,软件补偿的工作量也小,是我推荐的方案。
最后分享一个调H743采集时的小心得:不要一上来就把采样率跑满。先把CubeMX里的采样周期和连续模式调到一个保守值,确认DMA数据通路没问题,再逐步压缩采样周期、提升速率。每次只改一个变量,用示波器或信号发生器验证,这样的调试节奏看起来慢,实际上是踩坑最少的路子,尤其是在H743这种外设丰富、配置自由度高的芯片上。