详解DMA配置全流程

微信视频号:sph0RgSyDYV47z6
快手号:4874645212
抖音号:dy0so323fq2w
小红书号:95619019828
在嵌入式开发里,数据传输效率就像系统的"血液循环"——顺畅了,性能才能真正跑起来。STM32MP1系列凭" Cortex-A7+Cortex-M4 "的双核架构,加上超强的DMA子系统,成了工业控制、智能设备的香饽饽。今天就扒透它的DMA配置精髓,让数据搬运效率直接翻倍!
 
 
添加图片注释,不超过 140 字(可选)
一、STM32MP1 DMA架构:三引擎各司其职 STM32MP1藏着三大DMA控制器,层级分明,各管一摊高速传输: • 高速主DMA(MDMA):系统级"数据高铁",通过AXI总线连所有主设备,专做内存到内存的超高速传输。M4和A7双核能共享,峰值带宽飙到4.3GB/s(@533MHz),传大文件全靠它。 • 双端口DMA(DMA1/DMA2):外设的"专属快递员",每个控制器带8个独立数据流(共16个),每路能接116个通道请求,还有四级FIFO缓冲(32位宽),更支持硬件双缓冲——外设数据传输全靠这俩。 划重点:MDMA是双核共享资源,但DMA1/DMA2只能A7或M4单独用。分配资源时得先想清楚谁用啥,别抢。 二、DMA配置全流程:十步搭出高效通道 以M4核用DMA2为例,一步步教你配出能用的高效通道,每步都标了关键注意点。 步骤1:先开时钟,给DMA"通电" 任何外设都得先开时钟,DMA也一样,还得等时钟稳了再干活: __HAL_RCC_DMA2_CLK_ENABLE(); // 启用DMA2时钟 HAL_Delay(1); // 等1ms,确保时钟稳定 步骤2:选通道,先复位清状态 用之前先复位寄存器,避免旧配置捣乱: DMA_HandleTypeDef hdma; hdma.Instance = DMA2_Stream3; // 选DMA2的Stream3(根据需求换) HAL_DMA_DeInit(&hdma); // 复位通道,清空旧配置 步骤3:填核心参数,定传输规则 这步最关键,传输方向、地址增不增、数据宽度都在这设: hdma.Init.Direction = DMA_MEMORY_TO_PERIPH; // 方向:内存→外设(按需换) hdma.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址固定(寄存器地址不变) hdma.Init.MemInc = DMA_MINC_ENABLE; // 内存地址自增(数据存在连续地址) hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设数据宽度:32位(字) hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 内存数据宽度:32位(跟外设对应) hdma.Init.Mode = DMA_CIRCULAR; // 循环模式(持续传数据用,单次传换NORMAL) hdma.Init.Priority = DMA_PRIORITY_HIGH; // 优先级设高(关键任务用VERYHIGH) hdma.Init.FIFOMode = DMA_FIFOMODE_ENABLE; // 开FIFO,减少总线冲突 hdma.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; // FIFO满了再传(按需调) 小提醒:FIFO模式一定要开,尤其数据宽度不一样时(比如外设8位、内存32位),能避免数据乱序。 步骤4:绑地址,告诉DMA"从哪取、往哪放" 外设地址是寄存器地址,内存地址是自己定义的缓冲区: hdma.Init.PeriphBaseAddr = (uint32_t)&ADC1->DR; // 外设地址:ADC数据寄存器(按需换) hdma.Init.Mem0BaseAddr = (uint32_t)adc_buffer; // 内存地址:自定义缓冲区 步骤5:设传输量,说清"要传多少" 用NDTR寄存器设数据项数量,注意是"项"不是"字节"(跟数据宽度对应): #define BUF_SIZE 1024 // 比如传1024个32位数据 hdma.Init.NDTR = BUF_SIZE; // 传输数据项数量 步骤6:选请求源,让外设"喊DMA干活" 通过DMAMUX绑定外设请求,比如ADC1要传数据,就告诉DMA"听ADC1的": HAL_DMAEx_ConfigMuxRequest(&hdma, DMA_REQUEST_ADC1); // 绑定ADC1请求(按需换外设) 步骤7:初始化,把DMA和外设"连起来" 配置完参数,初始化DMA,再跟外设关联,让它们协同工作: HAL_DMA_Init(&hdma); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma); // 把DMA和ADC1绑一起(hadc1是ADC句柄) 步骤8:开中断(可选),知道"传完没" 需要监控传输状态就开中断,比如传完了要切换缓冲区: // 设中断优先级并使能 HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 0, 0); // 优先级设高,别被打断 HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn); // 使能传输完成中断(还能开半传完、错误中断) __HAL_DMA_ENABLE_IT(&hdma, DMA_IT_TC); 步骤9:启动DMA,让它"跑起来" 最后一步,启动传输,之后DMA就自己干活了,不用CPU管: HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, BUF_SIZE); // 启动ADC+DMA 步骤10:查状态,知道"传得怎么样" 需要时看看DMA忙不忙,还剩多少没传: HAL_DMA_StateTypeDef state = HAL_DMA_GetState(&hdma); if(state == HAL_DMA_STATE_BUSY) { // 如果正在传 uint32_t remaining = __HAL_DMA_GET_COUNTER(&hdma); // 查剩余数据量 printf("还剩 %lu 个数据没传\n", remaining); } 三、踩坑指南:4个常见问题怎么解 配DMA时总遇到奇奇怪怪的问题?这几个高频坑给你填好: 1. 缓冲区数据不更新?先查这3点 • 时钟漏开了:不光DMA时钟要开,外设时钟也得开(比如ADC时钟),少一个都不行。 • 外设没发请求:确认外设设了"DMA请求使能"(比如ADC的DMAContinuousRequests要设ENABLE),不然DMA收不到"干活信号"。 • 地址没对齐:32位传输时,缓冲区地址得是4的倍数(比如0x20000000行,0x20000001不行),对齐错了传不了。 2. 通道冲突?两招解决 • 调优先级:把关键任务(比如实时采集)的DMA设成DMA_PRIORITY_VERYHIGH,让它先抢总线。 • 换通道:用DMAMUX重新分配请求源,比如原来ADC1用Stream3,冲突了就换Stream4,只要DMAMUX里重新绑一下就行。 3. 数据错位?看位宽和FIFO • 位宽不匹配:外设是8位、内存用32位时,一定要开FIFO,再把FIFO阈值设成"4个8位数据满"(比如DMA_FIFO_THRESHOLD_QUARTER),让FIFO凑够32位再传。 • 地址增错了:内存没开MemInc,数据会全堆在一个地址;外设开了PeriphInc,会乱读寄存器——按实际需求检查这俩参数。 4. 中断不触发?查标志和NVIC • 没清标志:中断服务程序(ISR)里先调用HAL_DMA_IRQHandler(&hdma),让库函数清标志,不然会一直触发。 • NVIC没使能:确认HAL_NVIC_EnableIRQ调用了,中断号也没写错(比如DMA2_Stream3_IRQn别写成Stream2的)。 四、性能优化:4个技巧让DMA更快 配通了还想再快?这几个技巧能榨干DMA性能: 1. 开双缓冲,消除传输间隔 循环模式下用双缓冲,DMA传缓冲区1时,CPU填缓冲区2,传完直接切,中间不耽误: // 配置突发传输(一次传4个数据,提升效率) hdma.Init.MemBurst = DMA_MBURST_INC4; hdma.Init.PeriphBurst = DMA_PBURST_INC4; // 启动双缓冲传输 HAL_DMAEx_MultiBufferStart(&hdma, src, dst1, dst2, length); 2. 设突发传输,占满总线带宽 把MemBurst和PeriphBurst设成DMA_MBURST_INC4(4拍突发),一次传4个数据,比单个传快得多——尤其传大数据块时,带宽能涨一大截。 3. 调FIFO阈值,适配外设速度 外设慢(比如串口)就把阈值设小(比如半满),别让FIFO等太久;外设快(比如SPI)就设大(比如满),减少传输次数——按外设速度灵活调。 4. MDMA级联,传超大文件 传几MB的大数据时,用"双口DMA+MDMA"级联:让DMA1先把外设数据传到临时缓冲区,满了就触发MDMA,MDMA再把临时数据传到大内存——全程不用CPU插手,超大文件也能高效传。 五、实战案例:ADC三通道DMA循环采集 用上面的配置,搞个三通道ADC采集示例,全程DMA自动传,CPU只管算数据: // 定义缓冲区(三通道,每通道1024个数据) uint16_t adc_buf[3 * 1024]; void ADC_Init_DMA(void) { ADC_HandleTypeDef hadc; DMA_HandleTypeDef hdma; // 1. 配DMA hdma.Instance = DMA2_Stream0; hdma.Init.Request = DMA_REQUEST_ADC1; // 绑ADC1请求 hdma.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设→内存 hdma.Init.PeriphInc = DMA_PINC_DISABLE; // ADC寄存器地址固定 hdma.Init.MemInc = DMA_MINC_ENABLE; // 内存地址自增 hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // ADC是16位数据 hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma.Init.Mode = DMA_CIRCULAR; // 循环模式,一直采 hdma.Init.Priority = DMA_PRIORITY_HIGH; hdma.Init.FIFOMode = DMA_FIFOMODE_ENABLE; // 开FIFO HAL_DMA_Init(&hdma); // 2. 绑ADC和DMA __HAL_LINKDMA(&hadc, DMA_Handle, hdma); // 3. 启动采集 HAL_ADC_Start_DMA(&hadc, (uint32_t*)adc_buf, 3 * 1024); } // 数据处理(DMA传的时候CPU就能算) void Process_ADC_Data() { uint32_t ch1_sum = 0; for(int i = 0; i < 1024; i++) { ch1_sum += adc_buf[i*3]; // 通道1数据(每隔3个取一个) // 同理算通道2(i*3+1)、通道3(i*3+2) } // 转成电压(3.3V供电,12位ADC) float ch1_avg = ch1_sum / 1024.0f * 3.3f / 4096.0f; printf("通道1平均电压:%.2fV\n", ch1_avg); } 这个例子里,ADC采完数据自动进缓冲区,CPU不用管传输,专心算平均值——这就是DMA的优势:解放CPU,让它干更重要的事。 结语:懂DMA,才懂STM32MP1的真本事 STM32MP1的DMA不是简单的"数据搬运工",是双核架构的"性能粘合剂"——让M4核专心管实时控制(用DMA1/DMA2传外设数据),A7核管应用逻辑(用MDMA传大文件),各司其职才够高效。 就像高手用工具,懂DMA怎么配、怎么优化,才能让STM32MP1真正跑起来。
 
 
微信视频号:sph0RgSyDYV47z6
快手号:4874645212
抖音号:dy0so323fq2w
小红书号:95619019828
B站:UID:3546863642871878
参考文献链接
posted @ 2025-08-15 08:46  吴建明wujianming  阅读(32)  评论(0)    收藏  举报