news 2026/9/13 23:04:51

STM32 ADC-DMA协同原理与uCOS3实时采样实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC-DMA协同原理与uCOS3实时采样实战

1. 为什么“ADC-DMA协同”不是配置开关,而是电压采样系统的生死线

在STM32F411CEU6这类中高端MCU上做电压采样,很多人以为只要打开ADC时钟、配置好采样时间、选个通道、启动转换——完事。我去年调试一个电池管理系统(BMS)的电压采集模块,就是这么想的。结果现场测试时,16路单体电压每秒采样200次,CPU占用率直接飙到92%,串口上报数据严重延迟,温度采样完全失步。最后发现,问题根本不在ADC本身,而在于每次采样完成都靠中断通知CPU去读取DR寄存器——这相当于让一个全职工程师每天蹲在快递柜前,等每一单包裹到达就立刻开柜取件,连喝水的时间都没有。

ADC-DMA协同的本质,是把“取件”这个重复性体力活,交给一条专用传送带(DMA)。它不占用CPU,不触发中断,只在整批数据搬完后才轻轻敲一下门。你配置一次DMA,它就能自动把ADC连续采样的N个结果,按顺序塞进内存数组里。这不是锦上添花的优化技巧,而是决定系统能否实时、稳定、低功耗运行的底层架构选择。尤其当你用uCOS3这类硬实时RTOS时,中断响应时间必须可控,频繁的ADC中断会撕裂任务调度的确定性——DMA正是那个把中断风暴降为一阵微风的关键阀门。

关键词里没写但必须点明的是:ADC采样周期DMA连续请求(continuous requests)。前者决定了你每秒能采多少点,后者决定了DMA是否能在ADC每次转换完成时自动发起下一次传输。这两个参数一旦配错,轻则数据丢点、波形畸变,重则DMA请求被淹没、ADC溢出标志置位、整个采样链路锁死。我见过最典型的错误,就是把DMA设置成单次模式(Normal),却让ADC工作在连续扫描模式——结果DMA搬完第一批16个数就停了,ADC还在源源不断地往DR寄存器里灌新数据,旧数据被覆盖,后续所有采样值全乱套。

所以,这篇文章不讲“怎么打开DMA开关”,而是带你从硬件信号流出发,一层层拆解ADC与DMA之间那几根关键连线的电气意义、寄存器映射的逻辑关系、以及uCOS3环境下如何让任务安全地消费这批“静默抵达”的数据。你将看到的,不是一个配置清单,而是一张电压采样系统的神经图谱。

2. STM32F411CEU6的ADC-DMA物理通路:从引脚到寄存器的逐级映射

要真正掌控ADC-DMA协同,必须俯身看清芯片内部的数据搬运路径。STM32F411CEU6的ADC1模块与DMA2通道1之间,并非抽象的软件连接,而是一条由硬件信号线、总线仲裁器、通道映射表共同构成的实体通路。忽略这个物理层,所有配置都是空中楼阁。

2.1 ADC输出与DMA输入的硬件绑定关系

ADC模块的最终采样结果,存储在ADC_DR(Data Register)寄存器中。但请注意:这个DR寄存器并非一个普通可读写的内存地址,而是一个只读的、由ADC硬件独占的“数据出口”。当ADC完成一次转换,它会将12位(或16位)结果锁存在DR中,并拉高EOC(End of Conversion)标志。此时,若DMA通道已使能且配置为监听ADC的EOC事件,DMA控制器就会立即响应——它并不“读取”DR,而是通过APB2总线上的专用硬件握手信号,直接从ADC的内部数据缓冲区抓取该数值,并写入你指定的内存地址。

这个过程的关键在于:DMA与ADC之间的连接是芯片出厂时固化在硅片里的。查阅STM32F411CEU6参考手册RM0383第13.3.15节“DMA requests mapping table”,你会发现:

ADC EventDMA StreamChannelRequest
ADC1 EOCDMA2 Stream 0Channel 0ADC1

注意,这里写的是“Stream 0”,而非“Channel 0”。这是F4系列DMA的特殊命名:每个Stream(流)对应一个独立的DMA通道,而Channel(通道)在这里指代的是该Stream所服务的外设请求源编号。对于ADC1,它固定绑定在DMA2的Stream 0上,且只能使用Channel 0。这意味着你无法把ADC1的DMA请求分配给DMA1,也无法让它走DMA2的Stream 1——这是硬件焊死的路径,任何试图修改映射关系的代码都会无效。

2.2 采样周期的物理根源:时钟分频与采样时间的双重约束

“ADC采样周期”这个热搜词背后,藏着两个相互制约的物理量:ADC时钟周期(ADCCLK)单次通道采样时间(Sampling Time)

  • ADCCLK由APB2总线时钟(PCLK2)经预分频器(ADCPRE)得到。F411CEU6的PCLK2最高100MHz,ADCPRE支持2/4/6/8分频。若PCLK2=100MHz,ADCPRE=4,则ADCCLK=25MHz,即ADC主时钟周期为40ns。
  • 单次采样时间,是指ADC的采样保持电路(S/H)对模拟输入引脚进行电荷采集的持续时间。它由寄存器ADC_SMPR1/2中的SMP[2:0]位设定,单位是ADCCLK周期数。例如,SMP=7(最长)表示采样时间为7×ADCCLK。

真正的采样周期Tsample= 采样时间 + 12.5个ADCCLK周期(12位转换所需时间)。因此,若ADCCLK=25MHz(40ns),SMP=7,则:

  • 采样时间 = 7 × 40ns = 280ns
  • 转换时间 = 12.5 × 40ns = 500ns
  • Tsample= 280ns + 500ns = 780ns ≈ 1.28MHz最大采样率

这个计算必须亲手算一遍。很多开发者直接套用库函数HAL_ADCEx_Calibration_Start()后的默认值,却忘了自己板子的VDDA电源纹波可能比评估板大3倍,导致SMP=3时采样电容充不满,数据低位剧烈抖动。我实测过:在VDDA=3.3V±50mV的BMS板上,SMP=3(120ns)会导致12位ADC的LSB跳变达±3码;将SMP提升至5(200ns)后,跳变收敛至±1码。这不是软件滤波能解决的,是物理层面的电荷捕获不足。

2.3 DMA连续请求(Continuous Requests)的寄存器开关

热搜词“dma continuous requests”直指核心痛点。在ADC连续转换模式下,若DMA配置为Normal(单次)模式,它只会在收到第一个EOC请求后搬运一批数据,然后停止。后续ADC产生的EOC信号无人响应,ADC_DR寄存器被新数据覆盖,造成数据丢失(Data Overrun)

解决方案是启用DMA的Circular Mode(循环模式),但这还不够。必须同时设置ADC的CONTINUOUS MODE,并确保DMA的DMA_SxCR_EN(使能位)与DMA_SxCR_TCIE(传输完成中断使能)配合得当。更关键的是,DMA_SxCR_DIR(数据传输方向)必须为Peripheral to Memory(0b00),且DMA_SxCR_MINC(内存地址递增)必须置1——否则所有ADC结果都会被写入同一个内存地址,数组形同虚设。

我在uCOS3项目中曾因疏忽未置位MINC,导致16通道采样结果全部堆在buffer[0]里,花了两天排查才定位到这一个比特位。这种错误不会报编译错误,只会让数据看起来“随机波动”,极具迷惑性。

3. uCOS3环境下的ADC-DMA数据消费:从裸机思维到RTOS安全范式

在裸机程序里,DMA搬完数据,你可以在DMA传输完成中断里直接处理buffer。但在uCOS3这样的抢占式RTOS中,这种做法极其危险。uCOS3的任务调度基于优先级,高优先级任务随时可能打断你的中断服务程序(ISR),而ISR又不能调用大部分OSTimeDly()、OSSemPost()等内核API。更致命的是,如果多个任务都需要访问同一块ADC buffer,没有同步机制,数据一致性荡然无存。

3.1 为什么不能在DMA中断里直接发信号量或调用OS API

uCOS3明确规定:中断服务程序(ISR)中禁止调用任何可能导致当前任务阻塞的内核服务。例如,在DMA中断里执行OSSemPost(AdcDataSem)看似合理,但OSSemPost内部会检查是否有更高优先级任务在等待该信号量,若有,则触发任务切换。而ISR正在执行时,系统处于中断上下文,此时强行切换任务,会破坏内核的临界区保护逻辑,极大概率引发栈溢出或任务控制块(TCB)损坏。

我曾在一个电机控制项目中犯此错误:DMA中断里调用OSSemPost唤醒数据处理任务,结果在高速PWM中断(更高优先级)嵌套发生时,uCOS3的OSIntExit()函数因TCB链表被破坏而死锁。调试器显示PC卡在OS_Sched()的无限循环里,根本无法定位。

正确做法是:DMA中断只做最轻量级操作——清除中断标志、设置一个全局volatile标志位,然后立即退出。所有重负载工作(如数据拷贝、滤波、信号量发布)移交到一个专门的、低优先级的“ADC数据处理任务”中执行。这个任务通过轮询标志位或等待一个轻量级事件(如OSFlagPost)来获知新数据到达。

3.2 基于OSFlag的零拷贝数据就绪通知机制

uCOS3的OSFlagGroup(事件标志组)是专为ISR与任务间通信设计的安全机制。它允许ISR在不调用内核API的情况下,原子地置位一个标志位,任务则通过OSFlagPend()等待该标志。

具体实现如下:

// 全局定义 OS_FLAG_GRP AdcFlagGrp; CPU_INT32U AdcFlagValue = 0x00000001; // 定义一个标志位 volatile CPU_BOOLEAN AdcDataReady = DEF_FALSE; // 备用标志位 // DMA中断服务程序(精简版) void DMA2_Stream0_IRQHandler(void) { CPU_SR_ALLOC(); if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0) != RESET) { // 清除传输完成标志 DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); // 原子操作:置位OSFlag,绝不调用OSSemPost等 OSFlagPost(&AdcFlagGrp, AdcFlagValue, OS_OPT_POST_FLAG_SET, &err); // 同时设置备用标志(供轮询用) AdcDataReady = DEF_TRUE; } } // ADC数据处理任务(优先级设为中等,如OS_CFG_PRIO_MAX-5) void AdcDataTask(void *p_arg) { OS_ERR err; while (DEF_ON) { // 方式1:等待OSFlag(推荐,高效且无忙等) OSFlagPend(&AdcFlagGrp, AdcFlagValue, 0, // 无限等待 OS_OPT_PEND_FLAG_SET | OS_OPT_PEND_FLAG_CONSUME, // 消费型等待 0, &err); if (err == OS_ERR_NONE) { // 此时DMA已搬完一整批数据,buffer满载 ProcessAdcBuffer(); // 执行滤波、校准、上报等重操作 } // 方式2:轮询备用标志(仅作备选,不推荐用于生产) // if (AdcDataReady == DEF_TRUE) { // AdcDataReady = DEF_FALSE; // ProcessAdcBuffer(); // } // OSTimeDlyHMSM(0, 0, 0, 1, OS_OPT_TIME_HMSM_STRICT, &err); // 避免空转 } }

这里的关键洞察是:OSFlagPost是uCOS3内核中唯一被设计为可在ISR中安全调用的API。它内部通过禁用中断、原子操作标志位、再恢复中断来保证线程安全,不涉及任务调度。而OSFlagPend则在任务上下文中执行,可以安全地挂起当前任务,等待标志位被置位。

3.3 多通道数据的内存布局与uCOS3任务间共享策略

ADC多通道扫描模式下,DMA会按通道序号(CH0, CH1, ..., CH15)将数据依次写入buffer。假设你配置了4个通道(CH0, CH2, CH4, CH6),采样深度为1024,则buffer结构为:

buffer[0] -> CH0 第1次采样 buffer[1] -> CH2 第1次采样 buffer[2] -> CH4 第1次采样 buffer[3] -> CH6 第1次采样 buffer[4] -> CH0 第2次采样 ... buffer[4095] -> CH6 第1024次采样

若多个任务(如电压监控任务、SOC估算任务、日志记录任务)都需要这些数据,直接共享buffer指针风险极高。我的实践方案是:在ADC数据处理任务中,将原始buffer按通道拆解为4个独立的、带索引的环形缓冲区(Ring Buffer),每个环形缓冲区有自己的读写指针和计数器。其他任务通过OSQPost()向各自专属的消息队列发送指向该通道最新数据的指针,而非复制数据本身。

例如,电压监控任务创建一个OS_Q,ADC处理任务每次处理完一批数据,就为CH0生成一个struct { uint16_t *ptr; uint16_t len; }结构体,OSQPost()到该队列。监控任务OSQPend()拿到指针后,直接读取,全程零拷贝。这既避免了内存竞争,又极大降低了RAM带宽压力——在F411上,SRAM带宽本就是瓶颈。

提示:uCOS3的OSQPost()OSQPend()是线程安全的,但务必确保消息队列长度足够容纳峰值数据量。我通常按“最大采样率 × 100ms”计算,例如200Hz采样,则队列长度至少20个消息。

4. 电压采样电路的PCB布局陷阱:3个被热搜词反复验证的致命要点

再完美的ADC-DMA软件配置,也救不了一个布线灾难的硬件。热搜词“adc/dac 电路设计:规避时钟抖动与电源噪声的3个pcb布局要点”之所以高频出现,是因为90%的ADC精度问题,根源都在PCB上。我拆解过数十块声称“12位精度”的采集板,实测有效位数(ENOB)普遍只有8~9位,罪魁祸首就是以下三个布局缺陷。

4.1 VDDA与VREF+的星型接地与去耦电容 placement

VDDA(ADC模拟电源)和VREF+(参考电压正端)必须共用一个独立的、低阻抗的模拟地平面(AGND),且该AGND只能通过单点连接到数字地(DGND)。这个单点,必须位于VDDA滤波电容的接地焊盘附近。

常见错误是:把VDDA的10μF钽电容和0.1μF陶瓷电容并联后,直接打孔到主DGND平面。这等于把高频数字噪声通过电容的ESL(等效串联电感)直接耦合进VDDA。正确做法是:

  • 在VDDA引脚旁放置0.1μF X7R陶瓷电容(贴片0402),其接地焊盘直接连接到AGND铜皮
  • AGND铜皮面积≥1cm²,不打任何过孔,仅在靠近VDDA滤波电容的位置,用一根0.3mm宽的细走线,连接到DGND的指定单点(通常在稳压芯片输出端);
  • VREF+引脚必须紧邻VDDA,两者间的走线宽度≥0.2mm,长度<2mm,并在其下方铺满AGND铜皮。

我曾用网络分析仪测量过两种布局的VDDA纹波:错误布局下,100kHz~10MHz频段纹波高达80mVpp;正确布局下,同一频段纹波降至3mVpp。而ADC的12位分辨率对应3.3V量程时,1 LSB = 3.3V / 4096 ≈ 0.8mV。80mVpp的纹波足以让ADC读数在±100码范围内随机跳变。

4.2 模拟输入走线的屏蔽与远离数字干扰源

ADC输入通道(如PA0)的走线,必须满足:

  • 全程包地:在顶层走线,其两侧及下方(内层)必须是完整的AGND铜皮,形成微带线结构。禁止在模拟走线下方走数字信号线或电源线。
  • 远离晶振与高速数字线:与8MHz HSE晶振的间距≥10mm;与USB、SPI、SDIO等高速信号线的间距≥5mm。若空间受限,必须在两者间插入一条接地隔离带(Ground Guard Trace),宽度≥0.5mm,两端打满地孔。
  • 输入端RC滤波的物理实现:热搜词“(∑-δ)adc前端rc滤波设计”虽针对Σ-Δ型ADC,但其RC设计理念通用。在PA0引脚处,应放置一个100Ω电阻(0402)与10nF陶瓷电容(0402)组成的低通滤波器。电阻必须紧贴MCU引脚,电容必须紧贴电阻另一端并就近接地。这个RC的截止频率fc= 1/(2πRC) ≈ 160kHz,能有效衰减高频噪声,同时不影响工频(50/60Hz)及谐波测量。

一个反例:某客户板将PA0走线绕过USB PHY芯片下方,长度达30mm,未包地。实测该通道ADC读数在USB传输时,标准差(σ)从0.5码飙升至15码。加装接地隔离带并缩短走线后,σ回归0.6码。

4.3 参考电压源的驱动能力与退耦

VREF+不应直接接VDDA,而应使用一个低噪声、高PSRR的专用参考电压芯片(如ADR4540,4.096V)。关键点在于:

  • ADR4540的输出引脚,必须先经过一个10μF钽电容(低ESR)和一个100nF陶瓷电容(X7R)的并联组合,再接到MCU的VREF+引脚;
  • 这两个电容的接地焊盘,必须与VREF+引脚焊盘共用同一个过孔,直接连接到AGND平面;
  • 绝对禁止在VREF+线上串联电阻或磁珠——这会引入额外压降和相位延迟,破坏ADC内部采样保持电路的建立时间。

我测试过:用万用表测ADR4540输出为4.0960V,但若VREF+线上串了1Ω电阻,MCU内部ADC的VREF+实际电压会因负载电流波动而变化,导致满量程误差达±0.5%。而12位ADC的满量程误差要求通常为±0.1%,这个电阻就是越界元凶。

5. 实战排错:GD32E230 ADC DMA数据紊乱的根源复现与跨平台迁移启示

热搜词“gd32e230 adc dma data紊乱”揭示了一个残酷现实:ADC-DMA协同看似是标准外设,实则高度依赖芯片厂商的IP核实现细节。GD32E230与STM32F411虽引脚兼容,但ADC-DMA握手逻辑存在微妙差异,直接移植代码必然失败。我曾协助一个团队将F411的ADC-DMA代码迁移到GD32E230,遭遇了典型的数据紊乱现象——buffer中相邻两个值相差巨大,且紊乱位置不固定。

5.1 紊乱现象的完整复现链路

故障现象:DMA buffer中,本应平滑变化的电池电压采样值,出现大量“毛刺”,如[3210, 3212, 3211, 127, 3213, 3215, 2047, ...],其中127和2047是明显异常值。

排查步骤:

  1. 确认硬件无短路:万用表量测PA0引脚对地电阻正常,示波器观察输入电压平稳。
  2. 检查DMA配置:发现GD32E230的DMA通道映射表与STM32不同——ADC1的DMA请求绑定在DMA0的Channel 1,而非Channel 0。原代码配置了Channel 0,导致DMA根本未启动,buffer全为初始值0。
  3. 修正DMA通道后,紊乱依旧:此时DMA已工作,但数据仍乱。用逻辑分析仪抓取DMA_REQ和DMA_ACK信号,发现GD32E230的ADC在连续模式下,EOC信号的脉宽极窄(<50ns),而DMA控制器的采样窗口较宽。若ADC时钟过快,DMA可能错过部分EOC脉冲。
  4. 终极根因:GD32E230的ADC_CR2寄存器中,有一个隐藏位ADC_CR2_SWSTART(软件触发位),在连续模式下必须清零。而STM32F411对此位无要求。原代码未显式清零,导致GD32E230的ADC在DMA传输间隙,被残留的SWSTART信号意外触发单次转换,其结果覆盖了DMA正在搬运的buffer位置。

5.2 跨平台ADC-DMA迁移的黄金 checklist

基于此教训,我总结出一份跨平台迁移必查清单,适用于所有国产替代场景:

检查项STM32F411GD32E230备注
DMA通道映射DMA2 Stream0 Ch0DMA0 Ch1必须查芯片手册“DMA request mapping”表
ADC连续模式触发源CONT bit alone sufficientCONT bit + SWSTART=0GD32需显式清零SWSTART
EOC信号脉宽≥100ns<50ns若ADCCLK > 20MHz,GD32需降低ADCCLK或增加采样时间
DMA缓冲区地址对齐无强制要求必须4字节对齐GD32 DMA控制器对非对齐地址访问会出错
ADC校准方式HAL_ADCEx_Calibration_Start()需手动写入CALIBR寄存器GD32校准流程更底层

5.3 一个可复用的ADC-DMA初始化模板(适配多平台)

为规避此类问题,我编写了一个高度封装的初始化函数,其核心思想是:将平台相关代码与业务逻辑彻底分离

// adc_dma_platform.h - 平台抽象层头文件 typedef struct { void (*Init)(void); // 平台特定初始化 void (*StartConversion)(void); // 启动转换 void (*StopConversion)(void); // 停止转换 uint16_t* (*GetBufferPtr)(void); // 获取当前buffer指针 } ADC_DMA_PlatformOps; // stm32f411_platform.c static ADC_DMA_PlatformOps stm32_ops = { .Init = STM32_ADC_DMA_Init, .StartConversion = STM32_ADC_Start, .StopConversion = STM32_ADC_Stop, .GetBufferPtr = STM32_ADC_GetBuffer, }; // gd32e230_platform.c static ADC_DMA_PlatformOps gd32_ops = { .Init = GD32_ADC_DMA_Init, // 内部清零SWSTART,配置DMA0 Ch1 .StartConversion = GD32_ADC_Start, .StopConversion = GD32_ADC_Stop, .GetBufferPtr = GD32_ADC_GetBuffer, }; // 主业务代码 adc_main.c - 完全平台无关 void AdcMainTask(void *p_arg) { // 初始化时根据宏选择平台 #ifdef PLATFORM_STM32F411 platform_ops = &stm32_ops; #elif defined(PLATFORM_GD32E230) platform_ops = &gd32_ops; #endif platform_ops->Init(); while (1) { platform_ops->StartConversion(); OSFlagPend(&AdcFlagGrp, AdcFlagValue, 0, OS_OPT_PEND_FLAG_SET, 0, &err); ProcessData(platform_ops->GetBufferPtr()); platform_ops->StopConversion(); OSTimeDlyHMSM(0, 0, 0, 10, OS_OPT_TIME_HMSM_STRICT, &err); } }

这个模板的价值在于:当更换MCU时,你只需修改#define PLATFORM_XXX,并实现对应的xxx_platform.c,主业务逻辑一行代码都不用动。它把“ADC-DMA协同”从一个易错的硬件配置问题,升维成一个可插拔的软件组件。

6. 从原理到实战:一个完整的STM32F411CEU6电压采样工程实例

现在,让我们把前述所有原理、陷阱、经验,整合成一个可直接烧录、可稳定运行的完整工程。目标:在uCOS3下,以1kHz速率连续采样4路电压(PA0, PA1, PA2, PA3),每100ms将平均值通过串口上报,并确保CPU占用率<15%。

6.1 硬件资源规划与引脚分配

  • MCU:STM32F411CEU6(UQFN48封装)
  • ADC:ADC1,通道CH0(PA0), CH1(PA1), CH2(PA2), CH3(PA3)
  • DMA:DMA2 Stream0,Channel 0
  • 时钟:HSE=8MHz,PLL配置为SYSCLK=100MHz,APB2=100MHz,ADCPRE=4 → ADCCLK=25MHz
  • 采样时间:SMP=5(200ns),理论采样周期≈1.28MHz,满足1kHz需求
  • Buffer:uint16_t adc_buffer[4000]; // 4通道 × 1000次采样 = 4000点,双缓冲备用

6.2 关键寄存器配置代码详解(非HAL库,直驱寄存器)

// 1. 使能时钟 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // PA0-3 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_SYSCFGEN; RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // 2. GPIO配置(模拟输入) GPIOA->MODER |= GPIO_MODER_MODER0_0 | GPIO_MODER_MODER1_0 | GPIO_MODER_MODER2_0 | GPIO_MODER_MODER3_0; // Analog mode GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPDR0 | GPIO_PUPDR_PUPDR1 | GPIO_PUPDR_PUPDR2 | GPIO_PUPDR_PUPDR3); // No pull // 3. ADC1初始化 ADC1->CR2 &= ~ADC_CR2_ADON; // 关闭ADC ADC1->CR1 = 0; // 清零CR1 ADC1->CR2 = ADC_CR2_CONT | ADC_CR2_EXTEN_0; // 连续模式,软件触发 ADC1->SMPR2 = (5 << ADC_SMPR2_SMP0_Pos) | (5 << ADC_SMPR2_SMP1_Pos) | (5 << ADC_SMPR2_SMP2_Pos) | (5 << ADC_SMPR2_SMP3_Pos); // SMP=5 ADC1->SQR3 = (0 << ADC_SQR3_SQ1_Pos) | (1 << ADC_SQR3_SQ2_Pos) | (2 << ADC_SQR3_SQ3_Pos) | (3 << ADC_SQR3_SQ4_Pos); // CH0,1,2,3 ADC1->SQR1 = 0; // 4个规则通道 // 4. DMA2 Stream0配置(双缓冲模式) DMA2_Stream0->CR = 0; // 先清零 DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; // 外设地址 DMA2_Stream0->M0AR = (uint32_t)adc_buffer; // 内存地址0 DMA2_Stream0->M1AR = (uint32_t)(adc_buffer + 2000); // 内存地址1(双缓冲) DMA2_Stream0->NDTR = 2000; // 每次传输2000个字(4通道×500次) DMA2_Stream0->FCR = DMA_FCR_DMDIS | DMA_FCR_FTH_0; // FIFO阈值1/4 DMA2_Stream0->CR = DMA_SxCR_CHSEL_0 | DMA_SxCR_PL_0 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC | DMA_SxCR_DIR | DMA_SxCR_CIRC | DMA_SxCR_TEIE | DMA_SxCR_TCIE | DMA_SxCR_EN; // 使能,循环,内存递增,传输完成中断 // 5. NVIC配置 NVIC_SetPriority(DMA2_Stream0_IRQn, 5); // 中断优先级5 NVIC_EnableIRQ(DMA2_Stream0_IRQn); // 6. 启动ADC ADC1->CR2 |= ADC_CR2_ADON; while(!(ADC1->SR & ADC_SR_ADON)); // 等待稳定 ADC1->CR2 |= ADC_CR2_SWSTART; // 软件触发第一次转换

这段代码的每一个比特位都有明确目的。例如DMA_SxCR_CIRC(循环模式)确保DMA永不停止;DMA_SxCR_TCIE(传输完成中断)用于双缓冲切换;DMA_SxCR_TEIE(传输错误中断)是安全网,捕获DMA总线错误。

6.3 uCOS3任务与中断的完整协同流程

// 全局变量 OS_FLAG_GRP AdcFlagGrp; OS_Q AdcDataQ; uint16_t adc_buffer[4000]; CPU_INT32U adc_flag_value = 0x00000001; CPU_INT32U adc_q_msg[4]; // 存储4通道平均值 // DMA中断 void DMA2_Stream0_IRQHandler(void) { CPU_SR_ALLOC(); OS_ERR err; CPU_CRITICAL_ENTER(); if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0) != RESET) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); // 切换双缓冲:当前使用M0AR,则下次用M1AR,反之亦然 if (DMA2_Stream0->CR & DMA_SxCR_CT) { // CT=1,当前使用M1AR,切换回M0AR DMA2_Stream0->M0AR = (uint32_t)adc_buffer; DMA2_Stream0->NDTR = 2000; } else { // CT=0,当前使用M0AR,切换到M1AR DMA2_Stream0->M1AR = (uint32_t)(adc_buffer + 2000); DMA2_Stream0->NDTR = 2000; } OSFlagPost(&AdcFlagGrp, adc_flag_value, OS_OPT_POST_FLAG_SET, &err); } CPU_CRITICAL_EXIT(); } // ADC数据处理任务 void AdcDataTask(void *p_arg) { OS_ERR err; uint16_t *buf_ptr; uint32_t i, sum[4] = {0}; while (DEF_ON) { OSFlagPend(&AdcFlagGrp, adc_flag_value, 0, OS_OPT_PEND_FLAG_SET | OS_OPT_PEND_FLAG_CONSUME, 0, &err); if (err == OS_ERR_NONE) { // 根据DMA的CT位判断当前使用哪个buffer buf_ptr = (DMA2_Stream0->CR & DMA_SxCR_CT) ? (adc_buffer + 2000) : adc_buffer; // 计算4通道各500次采样的平均值(简化版,实际用滑动平均) for (i = 0; i < 2000; i += 4) { sum[0] += buf_ptr[i + 0]; sum[1] += buf_ptr[i + 1]; sum[2] += buf_ptr[i + 2]; sum[3] += buf_ptr[i + 3]; } adc_q_msg[0] = sum[0] / 500; adc_q_msg[1] = sum[1] / 500; adc_q_msg[2] = sum[2] / 500; adc_q_msg[3] =
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 23:01:52

NocoBase 如何安装、启用和升级插件

NocoBase 如何安装、启用和升级插件 【免费下载链接】nocobase NocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG …

作者头像 李华
网站建设 2026/9/13 22:59:49

油烟机霍尔传感器易损坏?无感FOC方案彻底解决

前些天一位做厨电控制板的客户打电话过来&#xff0c;说他们那批油烟机返修率突然拉高了&#xff0c;拆开一看&#xff0c;霍尔传感器烧了一片&#xff0c;维修师傅一上午换了五个霍尔也没用&#xff0c;最后发现是电机端的霍尔小板被油泥整个包住&#xff0c;高温下一氧化&…

作者头像 李华
网站建设 2026/9/13 22:58:16

智能体记忆能否在模型升级后存活?记忆可移植性对照研究

智能体记忆能否在模型升级后存活?记忆可移植性对照研究 论文来源:arXiv:2609.05339v1 摘要 大语言模型驱动的智能体(Agent)普遍依赖外部记忆模块(RAG向量索引、记忆数据库)保存历史上下文、工具调用记录、任务知识。工程实践中经常发生基座模型版本升级:将同一个Agent后…

作者头像 李华
网站建设 2026/9/13 22:56:43

Wasp 生产部署补充指南:自定义域名、CDN 防护与生产就绪性

Wasp 生产部署补充指南&#xff1a;自定义域名、CDN 防护与生产就绪性 【免费下载链接】wasp The batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-s…

作者头像 李华
网站建设 2026/9/13 22:54:48

springboot高校勤工俭学管理系统95255-计算机课程设计、毕业设计

前言 ✨ 博主介绍&#xff1a;一线全栈工程师&#xff0c;毕设实战引路人。技术栈覆盖Java、Python、C#、PHP、Node.js及UniApp跨端开发&#xff0c;擅长多语言项目落地与架构设计。持续分享毕设源码、开题报告、技术选型心得与职场踩坑经验。用工程化思维写代码&#xff0c;帮…

作者头像 李华