资讯动态

N32G45X ADC+DMA多通道采集实战:从STM32移植避坑到代码实现

发布时间:2026/9/2 2:13:04 来源:尧图企业网站定制
简介面向国民技术N32G45X系列嵌入式开发者的ADC与DMA结合应用例程可解决多通道模拟量采集时CPU占用高、数据传输效率低的问题适合需要实现连续采样、实时监测或传感器数据采集的工程师参考。压缩包共97个文件以52个h头文件和37个c源文件为主附带库文件、链接脚本及uvprojx工程文件源码结构清晰可直接导入Keil环境编译调试包体仅566KB便于快速部署与二次开发。目前已有856人学习下载。例程完整演示了ADC1多路采集与DMA传输的配置流程包括通道选择、转换分辨率设置、DMA流与通道匹配、源与目标地址定义以及启动转换的时序安排可作为实际项目的工程模板移植也能帮助初学者理解外设联动与免CPU干预的数据搬运机制。 先说一个我自己的真实经历早几年前第一次接触国民技术N32G45X系列当时手里正好有一套ADC采集多通道模拟量的需求我图省事直接把STM32的ADCDMA工程逻辑硬套过来。结果就是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是桶边的人水满了再说不用盯着一滴一滴数。这个类比虽然简单但足够帮新手理解为什么ADCDMA比“ADC中断里逐次读寄存器”的方式高效得多。1.2 N32G45X的ADC特性跟STM32有什么明显差异国民技术N32G45X系列的ADC是一个12位逐次逼近型SARADC这一点跟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.650VCH12.475V30702.473VCH00.826V10260.827VCH13.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完全不搬数据这是最让人抓狂的问题代码看着都对数组就是不变。我的排查链路是有固定顺序的先查ADC侧DMA请求有没有使能。N32G45X的ADC外设里有一个DMA使能位没打开的话ADC转换完成事件根本送不到DMA控制器。再查DMA外设地址。是不是指向了ADC的数据寄存器。很多人从STM32拷代码过来外设地址还是写的ADC1-DRN32G45X库里的寄存器名可能是ADC1-DAT编译不报错但地址如果偏了就搬不了数据。最后查中断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再开启有时候会残留上一次的状态反而不如全部重新配置来得干净。ADCDMA这套组合在N32G45X上跑通了以后你会发现后续做多通道数据采集、FFT预处理、传感器信号监测都顺了很多。说白了DMA就是一个搬运工你把搬运工的路线设定清楚把ADC的水龙头打开剩下的事就交给硬件去跑就行了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价