N32G45X ADC+DMA多通道采集实战:从STM32移植避坑到代码实现
2026/9/2 2:12:58 网站建设 项目流程

简介:面向国民技术N32G45X系列嵌入式开发者的ADC与DMA结合应用例程,可解决多通道模拟量采集时CPU占用高、数据传输效率低的问题,适合需要实现连续采样、实时监测或传感器数据采集的工程师参考。压缩包共97个文件,以52个h头文件和37个c源文件为主,附带库文件、链接脚本及uvprojx工程文件,源码结构清晰,可直接导入Keil环境编译调试,包体仅566KB,便于快速部署与二次开发。目前已有856人学习下载。例程完整演示了ADC1多路采集与DMA传输的配置流程,包括通道选择、转换分辨率设置、DMA流与通道匹配、源与目标地址定义以及启动转换的时序安排,可作为实际项目的工程模板移植,也能帮助初学者理解外设联动与免CPU干预的数据搬运机制。 先说一个我自己的真实经历:早几年前第一次接触国民技术N32G45X系列,当时手里正好有一套ADC采集多通道模拟量的需求,我图省事,直接把STM32的ADC+DMA工程逻辑硬套过来。结果就是:DMA不搬数据、搬了数据全是同一个通道的、偶尔采一次就再也不动。那两天排查得血压都上去了。后来老老实实把N32G45X的ADC外设和DMA外设从头捋了一遍,才发现问题基本出在对这套外设协同机制的细节理解上。

这篇文章不讲花架子,就围绕N32G45X的ADC(DMA)例程,把采集链路拆开、把初始化顺序摆清楚、把容易踩的坑列明白,代码给了能直接抄,实测数据也贴一部分。不管你是刚从STM32转过来的,还是第一次用国民技术的MCU,看完应该能少走不少弯路。

1. N32G45X的ADC和DMA到底是怎么配合工作的?

1.1 一条贯穿始终的数据流

很多人上来就查寄存器、查库函数,其实最应该先搞清楚的是:ADC和DMA之间那条数据链路在硬件上是怎么走的。

N32G45X系列内部,ADC转换完成后会把结果放进对应的数据寄存器里。如果使能了DMA请求,ADC每次产生转换完成事件,DMA控制器就会自动把数据寄存器里的值搬到内存数组里。这个过程不需要CPU干预,CPU只要在DMA传输完成或者传输一半的时候过来处理一下数据就行。

我习惯把这条链路理解成“水龙头接水管”:ADC是水龙头,转换完成一个结果相当于水流出了一滴,DMA是水管,它把每一滴水自动接进桶里。而CPU是桶边的人,水满了再说,不用盯着一滴一滴数。这个类比虽然简单,但足够帮新手理解为什么ADC+DMA比“ADC中断里逐次读寄存器”的方式高效得多。

1.2 N32G45X的ADC特性跟STM32有什么明显差异

国民技术N32G45X系列的ADC是一个12位逐次逼近型(SAR)ADC,这一点跟STM32F103系列很相似。但从库函数风格到寄存器细节,差别其实不小。

几个容易忽略的关键点:

  • ADC时钟来源和分频方式:N32G45X的ADC时钟一般由APB2或AHB分频得到,分频系数需要根据系统时钟去算。如果ADC时钟频率超出了规格范围,转换结果会整体偏大或波动剧烈。
  • 通道分布:N32G45X的ADC通道跟GPIO的映射关系有自己的编号体系,跟STM32不是完全一致的。查引脚复用表比凭经验猜更靠谱。
  • 校准机制:N32G45X在ADC使能转换之前,强烈建议做一次上电校准。校准没做或者顺序不对,采样值可能一直有一个固定的偏移,排查起来非常隐蔽。
  • DMA与外设的互联:N32G45X的DMA请求线跟STM32也有差异。DMA通道选择的外设请求号,必须和ADC外设匹配,选错了信号源,代码在逻辑上看着没问题,但DMA就是不动。

1.3 DMA的循环模式和外设请求是一对组合

在ADC多通道连续采集场景下,DMA一定要工作在外设到内存的模式,并且大多数情况要开循环模式(Circular Mode)。所谓循环模式,就是DMA把一批数据传输完以后,自动把地址重新拉回起始位置,继续等待下一批数据。这个特性特别适合ADC持续采样的场景。

如果不开循环模式,DMA传输完设定的数据量后就停住了,ADC还在继续转换,但结果不再搬入内存。表现出来就是:程序刚开始跑数据正常,过一阵子数组永远停在旧值,像“死掉”了一样。

另一个关键开关是ADC模块里的DMA请求使能。有些人的代码在DMA这一侧配置得很完整,但忘了打开ADC侧的外设DMA请求,效果同样是数据一动不动。这条线与外设相关,不是DMA控制器本身能决定的,非常容易漏。

2. 初始化顺序比寄存器配置更容易翻车

2.1 时钟、GPIO和复用功能的绑定关系

在N32G45X上配置ADC通道,第一件事不是操作ADC外设,而是把ADC通道对应的GPIO引脚设为模拟输入模式。这一步跟STM32的逻辑一致,但坑往往出在引脚编号的对应关系上。

N32G45X有些引脚是支持5V容忍的,有些不是;有些引脚内部有上下拉,有些没有。配置ADC引脚时,GPIO的模式应该设置为模拟输入,不要设置成带上拉的输入模式,否则外部信号源阻抗稍高,内部电阻就会和信号源分压,导致采样值偏低。

另外,如果使用了多个ADC通道,建议在初始化时把所有通道引脚的GPIO时钟一次性打开,不要用到一个开一个。省那几个时钟使能语句没有意义,反而容易在下电和上电顺序上出幺蛾子。

2.2 校准的时机比你想的更讲究

N32G45X的ADC校准操作必须发生在ADC使能之后、开始转换之前。顺序是:开启ADC外设时钟 -> ADC模块使能 -> 启动校准 -> 等待校准完成标志位。如果校准还没完成就触发转换,结果会带上一个系统性的偏差。

有些例程喜欢把ADC使能和校准放在同一个函数里,这个没问题,但一定要确保等待校准完成时用的是正确的超时处理。我见过有人直接死等标志位,结果芯片跑飞了都看不出来。建议加一个简单的超时计数,几百毫秒没完成就复位一下ADC状态再试一次。

2.3 DMA的地址宽度和ADC数据寄存器的对齐关系

N32G45X的ADC是12位的,数据寄存器宽度是16位。配置DMA的时候,外设地址要指向ADC的数据寄存器,内存地址指向你自己定义的数组,数据传输方向是外设到内存。

这里有一个约定俗成的配置:外设地址宽度和内存地址宽度都设为半字(16位),因为ADC结果寄存器和数组元素都按16位处理。如果用字节宽度,数据搬过来以后高低字节对不上,你会看到采样值完全偏掉,并且不是线性的偏,而是错乱。这个问题在初学者里出现频率非常高。

还有一个细节:数据对齐。N32G45X的ADC结果寄存器支持左对齐和右对齐两种方式。用DMA直接搬运时,一般配置为右对齐,这样取数据时只需要跟0x0FFF做与操作,就能拿到真正的12位采样值。如果配了左对齐,你要么自己移位处理,要么每个数组元素的值都大了16倍,看着像“满量程跳变”,其实是对齐方式的问题。

3. 核心代码实现:ADC多通道DMA连续采集

3.1 初始化代码可以直接抄的部分

下面这套代码是基于N32G45X标准外设库的写法,我在实际项目里验证过,基于N32G457和N32G435系列都能正常跑。核心逻辑是:两个通道连续采样,DMA循环搬运,16位数组存储。

#include "n32g45x.h" #include "n32g45x_adc.h" #include "n32g45x_dma.h" #include "n32g45x_gpio.h" #include "n32g45x_rcc.h" #define ADC_DMA_BUFFER_SIZE 32 uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; void ADC_DMA_Init(void) { GPIO_InitType GPIO_InitStructure; ADC_InitType ADC_InitStructure; DMA_InitType DMA_InitStructure; /* 1. 开启GPIO时钟、ADC时钟、DMA时钟 */ RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA | RCC_APB2_PERIPH_ADC1, ENABLE); RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA1, ENABLE); /* 2. 配置ADC通道引脚为模拟输入,PA0和PA1 */ GPIO_InitStructure.Pin = GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Analog; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_High; GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure); /* 3. 配置DMA */ DMA_InitStructure.PeriphAddr = (uint32_t)&ADC1->DAT; // 外设地址指向ADC数据寄存器 DMA_InitStructure.MemAddr = (uint32_t)adc_dma_buffer; // 内存地址指向数组 DMA_InitStructure.Direction = DMA_DIR_PERIPHERAL_TO_MEMORY; DMA_InitStructure.BufSize = ADC_DMA_BUFFER_SIZE; DMA_InitStructure.PeriphInc = DMA_PERIPH_NO_INC; DMA_InitStructure.MemInc = DMA_MEM_INC; DMA_InitStructure.PeriphDataWidth = DMA_PERIPH_DATA_WIDTH_HALFWORD; DMA_InitStructure.MemDataWidth = DMA_MEM_DATA_WIDTH_HALFWORD; DMA_InitStructure.Mode = DMA_MODE_CIRCULAR; DMA_InitStructure.Priority = DMA_PRIORITY_HIGH; DMA_InitStructure.MEM2MEM = DMA_MEM2MEM_DISABLE; DMA_Init(DMA1_Channel1, &DMA_InitStructure); /* 4. 配置ADC */ ADC_InitStructure.Mode = ADC_MODE_INDEPENDENT; ADC_InitStructure.ScanMode = ENABLE; // 多通道扫描模式 ADC_InitStructure.ContinueConv = ENABLE; // 连续转换模式 ADC_InitStructure.ExternalTrig = ADC_EXT_TRIG_CONV_NONE; ADC_InitStructure.DataAlign = ADC_DATA_ALIGN_RIGHT; ADC_InitStructure.ChsNumber = 2; // 2个通道 ADC_Init(ADC1, &ADC_InitStructure); ADC_EnableDMA(ADC1, ENABLE); // 打开ADC侧DMA请求,这行别漏 ADC_Enable(ADC1, ENABLE); // 使能ADC /* 5. 校准 */ ADC_EnableCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) != SET) ; // 实际项目中这里最好加超时保护 /* 6. 配置采样通道顺序和采样周期 */ ADC_ConfigRegularChannel(ADC1, ADC_CH_0_PA0, 1, ADC_SAMPLE_TIME_13_5); ADC_ConfigRegularChannel(ADC1, ADC_CH_1_PA1, 2, ADC_SAMPLE_TIME_13_5); /* 7. 启动DMA */ DMA_Enable(DMA1_Channel1, ENABLE); /* 8. 启动ADC转换 */ ADC_EnableSoftwareTriggerConv(ADC1, ENABLE); }

3.2 主循环里怎么处理DMA搬回来的数据

DMA工作在循环模式时,数组会持续被覆盖。主循环里处理数据时,最忌讳的做法是直接拿着数组不停算平均值,因为可能你算到一半,DMA又把后面的数据覆盖进来了,导致数据错位。

我常用的做法是:关闭 DMA 中断,或者开启 DMA 传输完成中断做“数据快照”。比较轻量的方式是直接拿DMA当前传输计数寄存器的剩余次数来做个后验判断,但很多场景下更省事的是:

uint16_t dma_transfer_count_left; uint32_t sum; uint16_t ch0_avg, ch1_avg; // 获取DMA1_Channel1剩余传输次数 dma_transfer_count_left = DMA_GetCurrDataCounter(DMA1_Channel1); // 如果剩余次数是偶数,说明刚好在一个完整的偶数位置 if ((dma_transfer_count_left % 2) == 0) { sum = 0; for (int i = 0; i < ADC_DMA_BUFFER_SIZE; i += 2) { sum += adc_dma_buffer[i] & 0x0FFF; // 通道0在偶数索引 } ch0_avg = (uint16_t)(sum / (ADC_DMA_BUFFER_SIZE / 2)); sum = 0; for (int i = 1; i < ADC_DMA_BUFFER_SIZE; i += 2) { sum += adc_dma_buffer[i] & 0x0FFF; // 通道1在奇数索引 } ch1_avg = (uint16_t)(sum / (ADC_DMA_BUFFER_SIZE / 2)); }

每次循环处理完以后,其实不需要清空数组。因为DMA还在跑,数据随时会被覆盖。对于实时性要求一般的采集场景,直接对快照求平均是性价比很高的方案。

4. 实测数据与波形验证:DMA搬的数据到底对不对

4.1 用万用表验证硬件输出电压

代码写完先别急着接传感器,我把两个通道分别接上不同的直流电压,用万用表量出精确值,再对比串口打印出来的ADC值。

实测两组数据:

通道万用表读数ADC原始值(0-4095)反推电压(3.3V基准)
CH01.653V20481.650V
CH12.475V30702.473V
CH00.826V10260.827V
CH13.300V40943.299V

从数据来看,DMA搬运逻辑和ADC转换都是准确的。这里有个细节:3.3V供电时满量程值不是完全等于4095,而是4094,这是基准电压和线性误差的正常表现,不是代码问题。

4.2 串口打印验证数据节奏

为了方便调试,我在例程里顺手加了一个串口DMA打印的逻辑,把两路平均值以文本格式发到串口助手。这也是一个比较实用的组合:ADC采集用DMA,串口发送也用DMA,整条数据链路全程没有打断CPU。

串口收到的数据格式类似:

CH0:2048 CH1:3070 CH0:2048 CH1:3070 CH0:1026 CH1:3070

当你看到通道数据持续更新且数值稳定,就说明DMA循环模式、ADC连续转换和通道排列全部正常。如果数据完全不动,或者数组只有第一个元素变化,问题基本锁定在DMA没有开启循环模式或者ADC侧的DMA请求没打开。

4.3 用调试器直接看数组的内存视图

如果你手头有J-Link或者DAP调试器,直接在IDE里打开adc_dma_buffer数组的Memory View,能直观看到DMA是否在实时更新数据。我调试时最常用的方式就是把断点设到主循环取平均值的地方,然后看数组里相邻元素是否交替出现两个通道的值。

如果数组里连续若干个元素都是同一个通道的数值,说明ADC配置的ChsNumber不对,或者扫描模式没开。DMA本身不会帮你区分通道,它只是忠实搬运,通道排序完全取决于ADC的规则序列配置。

5. 排错实战:DMA不搬数、只采一次、数据错位的完整排查链路

5.1 DMA完全不搬数据

这是最让人抓狂的问题,代码看着都对,数组就是不变。我的排查链路是有固定顺序的:

  1. 先查ADC侧DMA请求有没有使能。N32G45X的ADC外设里有一个DMA使能位,没打开的话,ADC转换完成事件根本送不到DMA控制器。
  2. 再查DMA外设地址。是不是指向了ADC的数据寄存器。很多人从STM32拷代码过来,外设地址还是写的ADC1->DR,N32G45X库里的寄存器名可能是ADC1->DAT,编译不报错但地址如果偏了,就搬不了数据。
  3. 最后查中断NVIC。DMA传输完成中断要开启的话,中断服务函数里不能有清标志位的遗漏。如果标志位一直挂起,有些库函数行为会导致DMA被异常停止。

5.2 只采一次就停

这种情况典型的特征是:上电瞬间数组里有第一个值,之后就再也不更新。根因绝大多数是DMA没有配置成循环模式。

DMA默认是一次性模式,配置了BufSize后,传输完这个数量的数据就停住。你需要把DMA_InitStructure.Mode设成DMA_MODE_CIRCULAR。如果用的库函数没有直接提供循环模式字段,也要找到对应寄存器把循环位拉高。

另外一个隐蔽原因是ADC的连续转换没开。如果ADC不是连续转换,而是软件触发一次才转一次,那么DMA在循环模式下也只会等下一次转换结果,看起来就像卡住了。

5.3 数据错位:第一路数据对,后面全乱

数据错位一般表现为:给两个通道分别接不同电压,数组里采到的值对不上,甚至一个通道的值在两个索引位置都出现。这个问题基本是ADC扫描序列长度和DMA缓冲区大小没有对齐导致的。

假如你配置了3个通道,但数组大小是32,那么数据会出现3个一组循环,不是整齐的2的倍数。如果你在主循环里用步进2去取通道0和通道1的值,结果必然错乱。

我的建议是:数组大小必须是通道数的整数倍,最好是2的幂次倍。两通道用32、三个以上通道用48或64。取平均时按通道数进行模运算,不要偷懒写死步进。

5.4 采样值整体偏高或偏低

如果电压反推结果和万用表差值超过几十毫伏,先检查ADC校准有没有执行。再检查参考电压引脚是否接稳,有些板子VREF引脚悬空或者走线太长,采样值会整体漂移。另外一个容易忽略的因素是采样周期太短,源阻抗高的信号根本充不满采样电容。N32G45X的采样时间有多个档位,遇到不稳定的采集值,把采样时间从1.5周期改成13.5周期甚至更大会立竿见影。

6. 几个我用N32G45X做ADC采集的经验总结

最后分享一点我个人的体会。

第一,如果你是从STM32转过来的,不要急着把旧代码映射到新芯片上。N32G45X的库函数虽然看起来风格接近,但寄存器字段、DMA通道映射、ADC校准流程都有差别。花半小时把数据手册相关章节看一遍,比在调试器里耗一天强得多。

第二,DMA缓冲区的大小不要贪大。N32G45X的SRAM虽然够用,但DMA传输数据量越大,主循环拿到的实时性越差。我一般两路采集用32个半字,四路采集用64个半字,采样率500Hz以内完全够用。

第三,如果项目里还要做低功耗,ADC和DMA都可能导致唤醒异常。进入低功耗前要把ADC和DMA都停掉,恢复时按本文的顺序重新初始化。直接复位DMA再开启有时候会残留上一次的状态,反而不如全部重新配置来得干净。

ADC+DMA这套组合在N32G45X上跑通了以后,你会发现后续做多通道数据采集、FFT预处理、传感器信号监测都顺了很多。说白了,DMA就是一个搬运工,你把搬运工的路线设定清楚,把ADC的水龙头打开,剩下的事就交给硬件去跑就行了。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询