ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32串口DMA配置详解:从原理到实战避坑指南

STM32串口DMA配置详解:从原理到实战避坑指南 1. 从“轮询”到“DMA”为什么串口通信需要解放CPU如果你用过STM32的串口大概率是从HAL库的HAL_UART_Transmit和HAL_UART_Receive这两个函数开始的。它们简单直接一个循环把数据一个个字节地塞进数据寄存器或者从寄存器里一个个读出来。在调试初期或者数据量不大的时候这完全没问题。但当你需要连续发送一长串数据比如通过串口发送一张图片的像素信息或者以115200的波特率持续接收来自传感器的数据流时问题就来了。CPU会像个尽职但低效的邮差被死死地绑在“搬数据”这件重复劳动上。它要不断地检查状态寄存器“上一个字节发完了吗发完了好我塞下一个。” 或者“有数据来了吗来了好我读出来。” 这个过程我们称之为“轮询”或“阻塞式”通信。在此期间CPU几乎无法分身去做其他更有价值的工作比如处理算法、响应按键、更新显示等。系统的实时性和效率大打折扣。DMADirect Memory Access直接存储器访问就是为了解决这个问题而生的。你可以把它想象成一个独立的、专业的“数据搬运工”。你只需要告诉DMA三件事数据从哪里来源地址、要搬到哪里去目标地址、要搬多少数据量。然后你启动它它就会在后台悄无声息地完成所有搬运工作完全不需要CPU介入。对于串口发送DMA负责把内存里的一块数据自动搬运到串口的数据发送寄存器TDR对于接收则是把接收寄存器RDR里的数据自动搬运到内存中你指定的位置。那么使用DMA模式到底能带来什么好处最核心的好处是解放CPU。在DMA搬运数据的整个过程中CPU是被释放的它可以去执行其他任务代码。只有当DMA完成了一次完整的传输比如发完了100个字节它才会通过一个中断来通知CPU“老板活儿干完了。” 此时CPU再来处理后续事宜比如准备下一批数据或者解析刚刚接收到的完整数据包。这种“异步”的工作方式极大地提高了系统的整体吞吐量和响应能力。其次降低了数据丢失的风险。在高速数据流接收场景下使用轮询方式很容易因为CPU忙于其他任务而错过数据寄存器中新的字节导致数据被覆盖而丢失。DMA的硬件触发机制响应速度极快几乎在数据到达寄存器的瞬间就会被搬走可靠性高得多。最后它简化了编程模型。你不再需要编写繁琐的、可能产生阻塞的轮询代码而是设置好DMA然后基于“传输完成中断”来构建你的应用逻辑代码结构更清晰更易于维护。在STM32的生态里ST提供的STM32CubeMX工具和HAL库让DMA的配置变得前所未有的简单。它通过图形化界面帮你完成了底层寄存器配置的绝大部分工作你只需要关注业务逻辑。接下来我们就手把手地从CubeMX的工程创建开始完成一个完整的串口DMA发送与接收配置。2. CubeMX工程创建与基础外设配置我们以常见的STM32F103C8T6BluePill核心板为例目标是配置USART1使用DMA进行发送和接收。其他系列型号的操作逻辑基本一致。2.1 新建工程与芯片选择打开STM32CubeMX点击“New Project”。在芯片选择器中输入“STM32F103C8”在结果列表中选择“STM32F103C8Tx”。注意型号后缀的“Tx”代表LQFP48封装这是BluePill板常用的封装。双击芯片图片或点击“Start Project”进入配置界面。2.2 系统核心SYS与时钟RCC配置在左侧分类视图的“System Core”中先配置SYS。Debug: 根据你的调试器选择。如果你使用ST-LINK进行SWD调试这里选择“Serial Wire”。这会影响PA13(SWDIO)和PA14(SWCLK)两个引脚的功能。如果选择不当可能导致后续无法下载或调试程序。然后配置RCC复位与时钟控制。High Speed Clock (HSE): 选择“Crystal/Ceramic Resonator”。BluePill板外部通常有一个8MHz的晶振。这一步是为系统提供准确的外部时钟源是保证串口波特率精确的基础。2.3 串口USART1引脚与参数配置在左侧分类视图的“Connectivity”中找到USART1。模式选择: 点击USART1在右侧的“Mode”中选择“Asynchronous”异步通信模式。这会使能USART1功能并自动占用PA9TX和PA10RX引脚。你可以在下方的芯片引脚图上看到这两个引脚变成了绿色。参数配置: 在下方出现的“Configuration”选项卡中点击“Parameter Settings”。Baud Rate: 设置为115200这是一个非常通用的波特率。Word Length: 8 Bits一个字节。Parity: None无校验位。Stop Bits: 11个停止位。Over Sampling: 16 Samples默认抗干扰能力较好。其他参数保持默认即可。至此一个最基础的串口功能就配置好了。但这只是“轮询模式”的配置。接下来才是DMA配置的核心。3. DMA通道的添加与关键参数详解DMA配置是本文的核心也是容易出错的地方。STM32的DMA资源是以“流”StreamF1/F4系列或“通道”ChannelF0/F1/F3等系列来管理的。对于STM32F103我们使用的是DMA的“通道”。3.1 添加DMA发送请求在左侧分类视图的“Connectivity”中再次点击USART1。在右侧“DMA Settings”选项卡中点击“Add”。在弹出的“DMA Request”选择框中你会看到USART1_TX和USART1_RX。我们先添加发送请求。选择USART1_TX然后点击“OK”。这会为USART1的发送功能分配一个DMA通道。3.2 配置DMA发送通道参数添加成功后列表中会出现一行我们需要对其参数进行详细配置。每一个参数的选择都有其背后的硬件原理。Direction:Memory To Peripheral。这定义了数据流向从内存我们定义的数组到外设USART1的数据发送寄存器。这是发送操作的正确方向。Priority:Medium。当多个DMA通道同时请求时优先级高的先被服务。对于单一串口发送Low或Medium均可。如果系统中有更紧急的数据搬运如ADC连续采样可以将其设为High。Mode:Normal。这是最常用的模式。在Normal模式下DMA在传输完指定的数据量比如100个字节后就会自动停止并产生传输完成中断。与之相对的是Circular循环模式在该模式下DMA传输完指定数据量后会自动重置计数器并重新开始传输形成一个循环缓冲区常用于需要持续不断收发数据的场景如音频流。初次使用强烈建议先从Normal模式开始理解。Increment Address: 这里有两项。Memory必须勾选Enable。因为我们发送的数据通常存储在内存的一个连续数组中DMA每搬运完一个字节或半字、字内存地址需要自动增加以指向下一个待发送的数据。Peripheral必须保持Disable。因为外设USART1的TDR寄存器的地址是固定的我们始终是把数据搬到同一个寄存器里。Data Width: 这里也有两项。Memory和Peripheral通常都选择Byte8位。因为串口通信的基本单位是字节8位。这确保了DMA每次搬运的数据大小与串口数据寄存器的大小匹配。这是一个关键点如果这里配置为Half Word16位或Word32位而你的串口是8位数据那么DMA一次会搬运2个或4个字节到TDR寄存器这会导致数据错乱。除非你使用了串口的9位数据模式或硬件流控制否则保持Byte。3.3 添加并配置DMA接收请求重复“添加”步骤这次选择USART1_RX。Direction:Peripheral To Memory。数据从外设USART1的RDR寄存器到内存。Priority:Medium或High。接收的优先级通常建议设置得比发送高因为接收不及时会导致数据丢失而发送晚一点通常可以接受。Mode: 对于接收Normal和Circular的选择更具策略性。如果你接收的是固定长度的数据包比如每包20字节并且你需要在收完一包后立即处理那么用Normal模式在传输完成中断里处理数据。如果你接收的是连续不断的数据流如GPS模块的NMEA语句那么Circular模式是更好的选择。它会在内存中创建一个环形缓冲区新数据会覆盖旧数据你只需要定期去缓冲区里读取有效数据即可避免了数据覆盖丢失的风险。本文后续以Normal模式为例进行代码编写。Increment Address:Memory:Enable。原因同发送我们需要把接收到的数据依次存放到数组的不同位置。Peripheral:Disable。原因同发送源地址RDR寄存器固定。Data Width: 同样都设置为Byte。注意DMA通道冲突。STM32的DMA控制器通道资源是有限的且一个通道在同一时间只能服务于一个外设请求。CubeMX通常会智能地分配空闲通道。但如果你的项目复杂使用了多个外设的DMA如ADC、SPI、另一个串口等可能会遇到通道冲突。如果CubeMX报错你需要手动调整确保每个外设请求使用的通道是唯一的。4. 中断与NVIC配置让CPU知道DMA干完活了配置好DMA的搬运工我们还需要告诉CPU什么时候该来“验收”工作成果。这就是中断的作用。4.1 使能DMA传输完成中断在刚才的DMA Settings列表中每一行即每个DMA请求的右侧都有一个“NVIC Settings”选项卡有时需要点击通道行最右侧的齿轮图标进入详细设置。点击它。对于USART1_TX通道找到并勾选Transfer Complete interrupt传输完成中断。这样当DMA发送完所有指定数据后就会产生一个中断。对于USART1_RX通道同样勾选Transfer Complete interrupt。当DMA接收满指定数量的数据后产生中断。为什么还需要串口自身的中断在某些高级场景下你可能还需要串口本身的USART1 global interrupt。例如你想在DMA接收的同时还能处理串口的其他事件如帧错误、溢出错误等。或者你想使用“空闲中断”Idle Interrupt来检测一帧数据的结束当串口总线空闲超过一个字符传输时间时触发这在接收不定长数据时非常有用。但为了简化初学模型本文暂不开启串口全局中断仅依赖DMA传输完成中断。4.2 配置NVIC优先级可选但重要在左侧分类视图的“System Core”中点击NVIC。 在这里你可以看到所有已使能的中断列表包括我们刚刚使能的DMA1 Channel4 global interrupt对应USART1_TX和DMA1 Channel5 global interrupt对应USART1_RX。Preemption Priority抢占优先级数值越小优先级越高。当两个中断同时发生时优先级高的先执行并且可以打断正在执行的优先级低的中断。Sub Priority子优先级当两个中断的抢占优先级相同时比较子优先级数值小的先执行但它们不能互相打断。 对于简单的应用你可以不修改使用默认优先级。但在复杂的实时系统中合理设置中断优先级至关重要。例如确保接收DMA中断的优先级高于发送或者高于某些非紧急的定时器中断。配置完成后你的DMA和中断设置应该大致如下图所示具体通道号可能因型号而异USART1_TX - DMA1 Channel4, Direction: M2P, Priority: Medium, Mode: Normal, MemInc: Enable, ... USART1_RX - DMA1 Channel5, Direction: P2M, Priority: Medium, Mode: Normal, MemInc: Enable, ... NVIC: DMA1 Channel4/5 Interrupt Enabled.5. 生成代码与HAL库关键函数解析点击CubeMX右上角的“GENERATE CODE”选择你的IDE如Keil MDK-ARM、IAR或STM32CubeIDE并指定工程路径。CubeMX会生成完整的初始化代码。打开生成的主文件通常是main.c在/* USER CODE BEGIN PV */和/* USER CODE END PV */之间我们定义发送和接收缓冲区。/* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ uint8_t uart_tx_buffer[100]; // 发送缓冲区 uint8_t uart_rx_buffer[50]; // 接收缓冲区 /* USER CODE END PV */在main函数中系统初始化HAL_Init()、时钟配置SystemClock_Config()和外设初始化MX_DMA_Init(),MX_USART1_UART_Init()的代码都已由CubeMX生成并调用。初始化完成后进入主循环while (1)之前是启动DMA接收的最佳时机。5.1 启动DMA接收在/* USER CODE BEGIN 2 */区域添加以下代码来启动DMA接收/* USER CODE BEGIN 2 */ // 启动串口DMA接收 // 参数串口句柄 接收缓冲区地址 接收数据长度 超时时间对于DMA接收此参数在非阻塞模式下通常无效可填最大值 if (HAL_UART_Receive_DMA(huart1, uart_rx_buffer, sizeof(uart_rx_buffer)) ! HAL_OK) { // 启动失败可以在这里添加错误处理比如点亮一个错误指示灯 Error_Handler(); } /* USER CODE END 2 */这行代码是整个DMA接收逻辑的起点。它告诉DMA控制器请监视USART1的接收寄存器一旦有数据到来就立刻将其搬运到uart_rx_buffer数组的开头连续搬运sizeof(uart_rx_buffer)这里是50个字节。搬运完成后触发DMA传输完成中断。5.2 编写DMA传输完成中断回调函数HAL库采用回调函数机制来处理中断。我们不需要直接编写中断服务函数ISR而是重写对应的回调函数。当DMA发送或接收完成时HAL库会在其中断服务函数里调用这些回调函数。在main.c文件中找到/* USER CODE BEGIN 4 */区域这是我们放置用户代码的地方。添加以下两个函数/* USER CODE BEGIN 4 */ // DMA发送完成中断回调函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // 判断是哪个串口触发的回调 if (huart-Instance USART1) { // USART1的DMA发送完成 // 可以在这里置位一个标志位通知主循环发送完成或者准备下一次发送 // 例如tx_complete_flag 1; // 注意不要在回调函数内进行长时间操作或调用可能阻塞的HAL_Delay() } } // DMA接收完成中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // USART1的DMA接收完成收到了50个字节 // 1. 处理接收到的数据uart_rx_buffer 中现在有50个新字节 // process_received_data(uart_rx_buffer, 50); // 2. 非常重要重新启动DMA接收以准备接收下一批数据。 // 因为我们在CubeMX中配置的是Normal模式传输完成后DMA就停止了。 // 如果不重新启动串口后续收到的数据将无法被DMA搬运可能导致溢出错误。 HAL_UART_Receive_DMA(huart1, uart_rx_buffer, sizeof(uart_rx_buffer)); } } /* USER CODE END 4 */关于HAL_UART_RxCpltCallback的特别说明 这是最容易忽略的一步。在Normal模式下DMA传输完成后就停止了。如果你只启动一次接收那么DMA在收满50个字节后就会永远停止之后串口再来的数据将无处安放除非你开启了溢出中断并处理。因此必须在本次接收完成的回调函数中立即重新调用HAL_UART_Receive_DMA来启动下一次接收从而形成一个“接收-处理-再准备接收”的循环。这是DMA接收Normal模式下的标准用法。5.3 主循环中触发DMA发送现在我们可以在主循环中模拟一个发送任务。例如当按下某个按键如果连接了的话或者定时发送时启动DMA发送。/* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // 示例每秒通过DMA发送一次数据 HAL_Delay(1000); // 准备要发送的数据 sprintf((char*)uart_tx_buffer, Hello DMA! Count: %lu\r\n, HAL_GetTick()); // 启动DMA发送 // 参数串口句柄 发送数据地址 发送数据长度 超时时间DMA模式下通常无效 if (HAL_UART_Transmit_DMA(huart1, uart_tx_buffer, strlen((char*)uart_tx_buffer)) ! HAL_OK) { // 发送启动失败处理 } // 注意调用此函数后立即返回发送在后台由DMA进行。 // 发送完成后会触发HAL_UART_TxCpltCallback回调。 // 主循环可以继续做其他事情比如闪烁LED而不被发送任务阻塞。 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } /* USER CODE END 3 */6. 进阶话题与实战避坑指南当你成功跑通基础的DMA收发后可能会遇到一些更复杂的需求和陷阱。下面分享几个实战中总结的经验。6.1 如何接收不定长数据DMA空闲中断Idle Interrupt前面我们用的是Normal模式配合固定长度接收。但在实际应用中比如接收一串以回车换行\r\n结尾的指令或者一个变长的数据包数据长度是不固定的。此时“DMA空闲中断”是STM32 HAL库推荐的优雅方案。原理使能串口的空闲中断Idle Interrupt。当DMA在接收数据时如果串口总线空闲没有新数据的时间超过一帧数据的传输时间硬件就会产生一个空闲中断。在这个中断里我们可以知道从上次处理完数据到现在DMA又搬了多少数据到缓冲区。配置步骤CubeMX配置在USART1的NVIC Settings中使能USART1 global interrupt。在代码中我们需要手动使能空闲中断。代码修改在启动DMA接收后使能空闲中断。// 在启动DMA接收后使能空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);重写串口全局中断回调函数HAL_UART_IRQHandler会调用的UART_ExRxEventCallbackHAL库较新版本或直接在USART1_IRQHandler的中断服务函数中添加空闲中断判断老版本方法。这里以新版本回调为例// 在main.c的USER CODE BEGIN 4区域 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // Size参数在空闲中断触发时表示自上次事件以来接收到的数据量 // 但更通用的做法是在空闲中断标志位判断中计算 } }更通用的做法是在USART1_IRQHandler调用的HAL_UART_IRQHandler函数执行后我们检查空闲中断标志。// 在stm32f1xx_it.c的USART1_IRQHandler函数中添加用户代码 void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ // 检查是否是空闲中断 if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除空闲中断标志必须 // 计算本次收到了多少数据 // DMA接收的数据量 设定的接收长度 - DMA寄存器中剩余未传输的数据量 uint16_t received_size sizeof(uart_rx_buffer) - __HAL_DMA_GET_COUNTER(huart1.hdmarx); if(received_size 0) { // 处理数据uart_rx_buffer[0] 到 uart_rx_buffer[received_size-1] process_data(uart_rx_buffer, received_size); // 重新启动DMA接收指向缓冲区起始位置 HAL_UART_Receive_DMA(huart1, uart_rx_buffer, sizeof(uart_rx_buffer)); } } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }关键点一定要清除空闲中断标志__HAL_UART_CLEAR_IDLEFLAG否则会连续进入中断。通过__HAL_DMA_GET_COUNTER可以获取DMA通道中剩余未传输的数据量从而反推出已经接收了多少数据。6.2 DMA发送的“坑”数据覆盖与缓存一致性当你调用HAL_UART_Transmit_DMA后函数会立即返回但DMA可能还在搬运数据。绝对不能在DMA发送完成前修改发送缓冲区uart_tx_buffer的内容否则DMA可能会发送出被修改后的混乱数据。解决方案双缓冲区Ping-Pong Buffer准备两个发送缓冲区A和B。当DMA正在发送缓冲区A的数据时主循环可以准备下一帧数据到缓冲区B。在A发送完成的回调函数中切换并启动B的发送同时主循环可以填充A。如此循环。使用发送完成标志在HAL_UART_TxCpltCallback中设置一个标志位tx_busy 0。主循环中只有检测到tx_busy 0时才填充缓冲区并启动下一次DMA发送同时设置tx_busy 1。6.3 调试技巧如何判断DMA是否在工作查看寄存器在调试器中查看DMA通道的CNDTR寄存器Counter。这个值表示剩余待传输的数据量。如果它在递减说明DMA正在搬运数据。传输完成后它会变为0并可能自动重载循环模式或停止普通模式。使用断点和标志位在DMA发送/接收完成回调函数入口处设置断点。如果程序能停在这里说明DMA传输完成中断触发了基本证明DMA工作正常。检查硬件连接和波特率这是老生常谈但最容易出错的地方。务必确认TX、RX线接对了且两端设备STM32和PC串口工具的波特率、数据位、停止位、校验位完全一致。简化测试初次测试时先只测试发送或只测试接收。例如先注释掉接收的启动代码只测试DMA发送用串口助手看是否能收到数据。成功后再测试接收可以用串口助手定时发送固定数量的字节看是否能进入接收完成回调。6.4 资源管理与错误处理HAL库的DMA函数会返回HAL_StatusTypeDef。除了HAL_OK还可能返回HAL_BUSYDMA正忙、HAL_ERROR参数错误或DMA状态错误等。在生产代码中应该检查这些返回值并进行适当的错误处理比如重试、记录错误日志或系统复位。此外STM32的DMA资源有限。在复杂项目中需要像管理内存一样管理DMA通道。CubeMX的图形化分配能解决大部分冲突但当你手动修改代码或移植工程时务必确认没有两个外设试图使用同一个DMA通道。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表