STM32F103C8T6 DMA配置全解析:从原理到实战应用

发布时间:2026/7/31 12:59:30
STM32F103C8T6 DMA配置全解析:从原理到实战应用 1. 从“搬运工”到“甩手掌柜”DMA在STM32中的角色转变如果你玩过STM32F103C8T6或者任何一款STM32肯定对“CPU占用率”这个词不陌生。当你用轮询方式去读一个串口数据或者用ADC连续采集时主循环就像被堵住了一样其他任务根本动不了。中断IT是个好办法它让CPU可以“随叫随到”但每次中断来临CPU都得停下手中的活去保存现场、处理数据、恢复现场对于高速、大批量的数据传输频繁的中断开销依然可观。这时候DMA就该登场了。你可以把它想象成一个极其专业、且不知疲倦的“数据搬运工”。它的全称是直接存储器访问核心思想就一句话让数据在存储器和外设之间“自动”搬家完全不需要CPU的干预。CPU只需要在搬家前告诉DMA搬运工“从A地址搬N个数据到B地址搬完了叫我一声”然后就可以潇洒地去执行其他代码了。等DMA搬完了通过一个中断通知CPU“老板活干完了数据在B地址您查收一下。”对于STM32F103C8T6这颗经典的“蓝桥杯神器”来说理解并用好DMA是项目性能从“能用”到“高效”的关键一步。无论是做四轴飞控需要高速读取IMU传感器数据还是做示波器需要ADC连续采样或是做无线通信需要处理大量串口数据DMA都能大幅解放CPU让你的程序响应更迅速、架构更清晰。接下来我们就深入这个“超级搬运工”的内部看看它是怎么工作的以及如何在C8T6上把它用起来。2. STM32F103C8T6的DMA架构与通道资源剖析STM32F103C8T6内部集成了两个DMA控制器DMA1和DMA2。但需要注意的是C8T6只有DMA1。DMA2是更高端型号如F103xC/D/E才具备的。所以我们的所有讨论都基于DMA1。DMA1拥有7个独立的通道Channel 1~7。每个通道都可以被配置为服务于一个特定的外设请求。这里“外设请求”是关键它意味着DMA的传输是由外设触发的。比如当ADC转换完成、串口收到数据、定时器产生更新事件时这些外设会向DMA控制器发出一个“请求信号”DMA控制器收到后就会指挥对应的通道开始一次数据传输。通道与外设的映射关系是固定的不能随意指派。这是硬件设计时决定的。对于DMA1其通道分配大致如下这是你需要牢记的通道1ADC1, TIM2_CH3, TIM4_CH1通道2SPI1_RX, USART3_TX, TIM1_CH1, TIM2_UP, TIM3_CH3通道3SPI1_TX, USART3_RX, TIM1_CH2, TIM3_CH4, TIM3_UP通道4SPI2_RX, USART1_TX, TIM1_CH4, TIM3_CH1, TIM4_CH2通道5SPI2_TX, USART1_RX, TIM1_UP, TIM4_CH3通道6USART2_RX, TIM1_CH3, TIM3_CH2, TIM4_UP通道7USART2_TX, TIM2_CH1, TIM2_CH2, TIM4_CH4注意这个列表并非完全详尽且不同型号的STM32F1可能会有细微差别。最权威的参考永远是芯片的参考手册Reference Manual中的DMA章节。以USART1为例它的发送TX固定使用DMA1的通道4接收RX固定使用通道5。如果你试图用通道1去服务USART1_TX硬件上是不工作的。为什么通道资源如此重要因为它是稀缺的。C8T6只有7个通道但你可能同时需要ADC多通道DMA采集、串口收发、SPI读写Flash等。这就涉及到通道冲突的问题一个通道在同一时间只能服务于一个外设。例如如果你同时使用了USART1_TX通道4和SPI2_RX也是通道4那就产生了冲突必须分时复用或重新设计数据流。DMA传输的三大要素无论配置哪个通道你都需要明确告诉DMA三件事源地址数据从哪里来是某个外设的数据寄存器如USART1-DR还是内存中的一个数组目标地址数据到哪里去是到外设的数据寄存器还是到内存传输数量要搬多少数据这个数量会随着传输的进行而递减。DMA控制器内部有专门的寄存器来存储这些信息并且在传输过程中自动管理地址的递增或递减。3. DMA工作模式详解单次、循环与存储器到存储器的奥秘理解了通道下一步就是配置DMA的工作模式。这是DMA灵活性的体现主要分为以下几种3.1 外设到存储器 / 存储器到外设模式这是最常用的模式也是DMA设计的初衷。外设到存储器典型应用是ADC多通道DMA采集。ADC转换完成的数据寄存器ADC1-DR是源地址外设你定义在内存中的一个数组adc_buffer[ ]是目标地址。DMA会在每次ADC转换完成后自动把数据搬到数组里。存储器到外设典型应用是串口DMA发送。你要发送的数据存在内存数组tx_buffer[ ]里源地址DMA会逐个搬移到串口的数据寄存器USART1-DR中由串口硬件发送出去。对于发送大量数据如发送一帧图片数据到串口屏特别有用。在这两种模式下地址指针的递增行为需要仔细配置外设地址通常不递增。因为像USART1-DR这种寄存器你总是往同一个地址读写数据。存储器地址通常递增。因为你希望数据依次存放到数组的连续位置或者从数组的连续位置取出。3.2 循环模式 vs 单次模式这是控制DMA传输“轮次”的关键。单次模式DMA在完成指定数量的传输后会自动关闭通道EN位被清零。如果你想再次传输必须由软件重新使能DMA通道。适用于你知道确切传输量的场景比如发送一个固定长度的数据包。循环模式DMA在完成指定数量的传输后会自动将传输计数器和地址指针根据配置是否回绕重新加载为初始值然后开始新一轮传输。这个过程完全由硬件完成无需软件干预。这是实现连续、无间断数据流的利器。ADC连续采集必须使用循环模式。配置一个足够大的缓冲区比如adc_buffer[1000]设置传输数量为1000并使能循环模式。DMA就会在缓冲区里循环填充ADC数据即使CPU很久才来读取一次最新的数据也永远在缓冲区里不会丢失。串口DMA接收不定长数据这是一个经典组合“串口空闲中断 DMA循环接收”。将DMA接收配置为循环模式指向一个环形缓冲区。串口每收到一个字节DMA就自动存到缓冲区并更新指针。当一帧数据结束串口产生空闲中断在中断服务程序里通过计算DMA当前传输剩余值就能推算出这一帧收到了多少数据然后进行处理。这种方法几乎零CPU开销且能处理任意长度的数据帧。3.3 存储器到存储器模式这是DMA1的一个特殊功能允许数据在两个内存区域之间直接搬运比如从一个数组复制到另一个数组。配置时需要将其中一个内存地址配置为“外设地址”虽然它并不是外设。重要限制在STM32F103的DMA1上只有通道1~3支持存储器到存储器模式。通道4~7不支持。如果你需要此功能务必选择正确的通道。使用场景当需要复制大量数据时比如图像缓冲区的复制使用DMA比用CPU的memcpy函数效率高得多因为不占用CPU时间和总线带宽DMA使用自己的总线矩阵。4. 核心配置流程与寄存器/库函数操作指南无论是使用标准外设库SPL还是HAL库配置DMA的思路都是一致的。这里我们以HAL库为例因为它更通用且与STM32CubeMX工具无缝衔接。我们以实现“USART1使用DMA发送不定长数据”和“ADC1多通道循环DMA采集”两个典型场景来讲解。4.1 场景一USART1 DMA发送目标将内存中的一段数据通过USART1发送出去期间CPU不参与数据搬运。CubeMX图形化配置在Connectivity中使能USART1模式选择Asynchronous。在DMA Settings标签页点击Add选择USART1_TX。方向Direction自动为Memory To Peripheral。配置DMA参数Mode:Normal单次模式。如果发送连续流数据可选Circular。Priority:Medium。根据系统实时性要求调整。Increment Address: 对于Memory侧选Enabled地址递增Peripheral侧选Disabled。代码实现关键点// 1. 定义发送缓冲区和长度 uint8_t tx_data[] Hello DMA!\r\n; uint16_t tx_len sizeof(tx_data) - 1; // 去掉字符串结尾的\0 // 2. 启动DMA传输 HAL_UART_Transmit_DMA(huart1, tx_data, tx_len); // 调用此函数后CPU立即返回后续工作由DMA接管 // 3. 可选查询或中断通知发送完成 // 方式A轮询查询 if(HAL_DMA_GetState(hdma_usart1_tx) HAL_DMA_STATE_READY) { // 发送完成 } // 方式B使用发送完成中断需在CubeMX中使能DMA全局中断 // 在stm32f1xx_it.c中找到DMA1_Channel4_IRQHandlerHAL库会自动调用回调函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // USART1 DMA发送完成可以准备下一包数据或进行其他操作 } }避坑经验数据一致性在DMA传输进行中绝对不要修改源数据缓冲区tx_data的内容。因为DMA可能才搬了一半。如果需要修改要么等传输完成回调触发后再操作要么使用双缓冲区Ping-Pong Buffer机制。总线冲突DMA和CPU都要访问内存和总线。如果CPU和DMA频繁访问同一块内存区域尤其是紧挨着的地址可能会导致性能下降。合理安排数据布局可以缓解。中断优先级如果系统中有多个中断如定时器中断、ADC中断等需要合理设置DMA传输完成中断的优先级避免高优先级任务被阻塞。4.2 场景二ADC1多通道循环DMA采集目标连续采集ADC1的通道0PA0和通道1PA1的电压值并存放到数组中。CubeMX图形化配置在Analog中使能ADC1。在Parameter Settings中Scan Conversion Mode:Enabled扫描模式用于多通道。Continuous Conversion Mode:Enabled连续转换模式。DMA Continuous Requests:Enabled使能DMA连续请求这是循环采集的关键。Number Of Conversion: 设置为2。在Rank中配置Channel 0和Channel 1并设置采样时间。在DMA Settings标签页点击Add选择ADC1。方向自动为Peripheral To Memory。配置DMA参数Mode:Circular循环模式。Increment Address:Memory侧EnabledPeripheral侧Disabled。Data Width: 根据ADC分辨率选择Word12位ADC结果用16位存储即可但对齐为半字或字。代码实现关键点// 1. 定义ADC采集缓冲区 // 假设采集2个通道缓冲区深度为100组数据 #define ADC_CHANNEL_NUM 2 #define ADC_BUFFER_DEPTH 100 uint16_t adc_buffer[ADC_BUFFER_DEPTH][ADC_CHANNEL_NUM]; // 二维数组便于理解 // 2. 启动ADC的DMA采集 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_CHANNEL_NUM * ADC_BUFFER_DEPTH); // 第二个参数是目标地址第三个参数是总的数据单元数量通道数*深度 // 3. 此后adc_buffer会被DMA自动、循环地填充 // 例如adc_buffer[0][0]是第0次采集的通道0数据adc_buffer[0][1]是第0次采集的通道1数据 // adc_buffer[1][0]是第1次采集的通道0数据以此类推。 // 4. 在程序任何需要的地方直接读取adc_buffer即可获得最新数据 // 为了知道当前DMA写到了哪个位置可以使用DMA的当前剩余数据寄存器CNDTR来推算 uint32_t remaining __HAL_DMA_GET_COUNTER(hdma_adc1); // 获取剩余传输次数 uint32_t index (ADC_CHANNEL_NUM * ADC_BUFFER_DEPTH - remaining) / ADC_CHANNEL_NUM; // index 指示了当前最新写完的一组数据的索引可能不是最新的因为DMA在循环 // 更稳健的做法是使用双缓冲区或记录“写指针”。高级技巧双缓冲区Ping-Pong Buffer对于实时性要求高的处理如音频处理、数字滤波可以在DMA传输完成一半和全部完成时都触发中断实现双缓冲区交换。这样当DMA在填充缓冲区A时CPU可以处理缓冲区B的数据实现并行处理几乎零等待。HAL库提供了HAL_ADC_ConvHalfCpltCallback半传输完成和HAL_ADC_ConvCpltCallback全传输完成两个回调函数来支持此模式。5. 常见问题排查与实战调试心得即使配置看起来正确DMA也常常会“沉默”不工作。以下是我在项目中总结的排查清单和调试方法。5.1 DMA不启动或数据不传输时钟检查DMA控制器本身需要时钟。在STM32F1中DMA时钟在AHB总线上。使用CubeMX配置时通常会自动开启但如果是手动初始化务必确认RCC_AHBPeriph_DMA1时钟已使能。__HAL_RCC_DMA1_CLK_ENABLE(); // HAL库 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 标准库通道绑定错误这是最常见的问题。反复核对参考手册中DMA请求映射表确认你使用的外设请求如USART1_TX是否真的映射到了你初始化的DMA通道DMA1_Channel4上。用错了通道DMA永远收不到外设的触发信号。外设DMA请求未使能仅仅配置了DMA还不够必须在外设内部开启DMA功能。对于USART需要设置USART_CR3寄存器中的DMAT发送DMA使能或DMAR接收DMA使能位。HAL库中HAL_UART_Transmit_DMA函数内部会做这个操作。对于ADC需要设置ADC_CR2寄存器中的DMA位对于ADC1或DDS位对于DMA连续请求。HAL库中HAL_ADC_Start_DMA会处理。对于SPI/I2C同样有对应的DMA使能位。传输完成中断TCIF未清除如果上一次传输完成后传输完成中断标志没有被清除新的传输请求可能会被忽略。在标准库中需要在中断服务程序里手动清除标志位DMA_ClearITPendingBit。在HAL库中中断处理框架通常会帮你清除但如果你是自己写的底层驱动这一点务必检查。5.2 数据错位、重复或丢失地址递增配置错误这是导致数据错位的元凶。仔细检查DMA_CCRx寄存器中的PINC外设地址递增和MINC存储器地址递增位。对于外设到存储器通常是PINC0MINC1。如果MINC设成了0所有ADC采样值都会堆积在缓冲区的第一个地址覆盖之前的数据。数据宽度不匹配源地址和目标地址的数据宽度必须匹配。例如ADC是12位分辨率结果寄存器是16位低12位有效。如果你配置DMA的数据宽度为Byte8位那么一次传输只能搬走低8位数据就错乱了。通常配置为HalfWord16位即可。缓冲区溢出Overrun在循环模式下如果CPU读取数据的速度跟不上DMA写入的速度新数据就会覆盖还未被处理的老数据。解决方案一是加大缓冲区深度二是提高CPU处理数据的频率例如使用定时器中断定期处理三是采用双缓冲区机制。外设FIFO与DMA的协同问题某些外设如USART有自己的小容量FIFO。当DMA传输速度极快时需要关注外设的FIFO状态避免数据被覆盖。STM32F103的USART FIFO很浅通常不是问题但在更高端的系列或高速通信时需要留意。5.3 调试利器活用调试器查看DMA寄存器在IDE如Keil MDK的调试模式下打开Peripherals - DMA - DMA1窗口可以实时查看每个通道的控制寄存器CCR、数据数量寄存器CNDTR、外设/存储器地址寄存器CPAR/CMAR的值。这是判断DMA是否被正确使能、传输是否在进行、计数器是否在递减的最直接方法。观察内存数据在Memory窗口中输入你定义的DMA缓冲区地址如adc_buffer可以实时看到数据是否被正确写入。结合单步执行可以判断是DMA没写还是CPU误写了。断点与中断在DMA传输完成中断的服务函数里设置断点。如果传输完成但断点从未触发说明中断未正确配置或使能。如果断点触发但内存数据不对问题可能出在数据传输环节。我个人最深刻的教训曾经在一个项目中ADC DMA采集的数据总是偶尔跳变。排查了很久最后发现是电源噪声。模拟部分ADC参考电压、传感器供电的轻微波动被高速采样的ADC捕捉到再经过DMA放大在数据上就表现为随机跳变。所以当DMA硬件层面排查无误后如果数据仍有问题别忘了回头检查你的模拟电路、电源质量和接地。DMA只是一个忠实的搬运工它不生产数据它只是数据的搬运工。如果源头“污染”了搬出来的也只能是“垃圾”。