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

发布时间:2026/9/13 23:05:02
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_DRData Register寄存器中。但请注意这个DR寄存器并非一个普通可读写的内存地址而是一个只读的、由ADC硬件独占的“数据出口”。当ADC完成一次转换它会将12位或16位结果锁存在DR中并拉高EOCEnd of Conversion标志。此时若DMA通道已使能且配置为监听ADC的EOC事件DMA控制器就会立即响应——它并不“读取”DR而是通过APB2总线上的专用硬件握手信号直接从ADC的内部数据缓冲区抓取该数值并写入你指定的内存地址。这个过程的关键在于DMA与ADC之间的连接是芯片出厂时固化在硅片里的。查阅STM32F411CEU6参考手册RM0383第13.3.15节“DMA requests mapping table”你会发现ADC EventDMA StreamChannelRequestADC1 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最高100MHzADCPRE支持2/4/6/8分频。若PCLK2100MHzADCPRE4则ADCCLK25MHz即ADC主时钟周期为40ns。单次采样时间是指ADC的采样保持电路S/H对模拟输入引脚进行电荷采集的持续时间。它由寄存器ADC_SMPR1/2中的SMP[2:0]位设定单位是ADCCLK周期数。例如SMP7最长表示采样时间为7×ADCCLK。真正的采样周期Tsample 采样时间 12.5个ADCCLK周期12位转换所需时间。因此若ADCCLK25MHz40nsSMP7则采样时间 7 × 40ns 280ns转换时间 12.5 × 40ns 500nsTsample 280ns 500ns 780ns ≈ 1.28MHz最大采样率这个计算必须亲手算一遍。很多开发者直接套用库函数HAL_ADCEx_Calibration_Start()后的默认值却忘了自己板子的VDDA电源纹波可能比评估板大3倍导致SMP3时采样电容充不满数据低位剧烈抖动。我实测过在VDDA3.3V±50mV的BMS板上SMP3120ns会导致12位ADC的LSB跳变达±3码将SMP提升至5200ns后跳变收敛至±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 Memory0b00且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 APIuCOS3明确规定中断服务程序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_QADC处理任务每次处理完一批数据就为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的星型接地与去耦电容 placementVDDAADC模拟电源和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的专用参考电压芯片如ADR45404.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是明显异常值。排查步骤确认硬件无短路万用表量测PA0引脚对地电阻正常示波器观察输入电压平稳。检查DMA配置发现GD32E230的DMA通道映射表与STM32不同——ADC1的DMA请求绑定在DMA0的Channel 1而非Channel 0。原代码配置了Channel 0导致DMA根本未启动buffer全为初始值0。修正DMA通道后紊乱依旧此时DMA已工作但数据仍乱。用逻辑分析仪抓取DMA_REQ和DMA_ACK信号发现GD32E230的ADC在连续模式下EOC信号的脉宽极窄50ns而DMA控制器的采样窗口较宽。若ADC时钟过快DMA可能错过部分EOC脉冲。终极根因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 SWSTART0GD32需显式清零SWSTARTEOC信号脉宽≥100ns50ns若ADCCLK 20MHzGD32需降低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 硬件资源规划与引脚分配MCUSTM32F411CEU6UQFN48封装ADCADC1通道CH0(PA0), CH1(PA1), CH2(PA2), CH3(PA3)DMADMA2 Stream0Channel 0时钟HSE8MHzPLL配置为SYSCLK100MHzAPB2100MHzADCPRE4 → ADCCLK25MHz采样时间SMP5200ns理论采样周期≈1.28MHz满足1kHz需求Bufferuint16_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); // SMP5 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) { // CT1当前使用M1AR切换回M0AR DMA2_Stream0-M0AR (uint32_t)adc_buffer; DMA2_Stream0-NDTR 2000; } else { // CT0当前使用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]