STM32L475串口打印改为阻塞式打印兼DMA, 两种打印方式实时切换

阻塞式打印的缺陷

当前的串口打印发送字符是通过CPU轮训一个个发送字符到串口的, 代码如下

c 复制代码
void _putchar(char character)
{
  // 使用HEL库发送1字节数据
  HAL_UART_Transmit(&huart1, (uint8_t *)&character, 1, HAL_MAX_DELAY);
}

当打印变多时, CPU占用会彪得非常高, 到80%左右. 通过CPU发送一个字符到串口总线耗时, 在115200波特率下需要约86微秒, 如果发送一个很长的字符串, 会浪费掉大量CPU时钟周期.

DMA+阻塞结合的打印方案

在DMA还没有初始化好时, 我们需要打印数据, 就通过CPU阻塞式打印

当DMA初始化完成后, 自动切换到DMA打印

另外就是增加错误中断处理, 当DMA发生故障时, 能够快速切换回CPU阻塞式打印

关于DMA的工作原理

DMA(Direct memory access)是一个可以控制总线的系统外设, 核心功能就是搬运数据. 可以在内存与其他外设之间搬运数据, 也可以在内存与内存之间搬运, 整个数据搬运过程不需要CPU的参与

根据手册描述, STM32L475VET6中有两个DMA实例, 分别为DMA1(7个通道)和DMA2(7个通道), 每个通道专门负责一个或者多个外设发起的内存访问请求, 然后每个DMA实例内部都有一个仲裁器, 用来裁决自身各个通道之间传输请求的优先级.

当DMA和CPU都要试图控制总线时, 系统总线仲裁器会判定谁先控制总线. 这里要明白的是, DMA内部的仲裁器只负责DMA内部各个通道之间的请求谁先传输, 系统总线仲裁器判定DMA和CPU谁先传输.

通道与外设的关系, 每个DMA通道都有固定的外设请求映射表, 比如USART1的发送请求只能映射到DMA1的某几个特定通道上, 不能随意绑定, 映射表局部如下, 可以看到USART1_TX1在DMA1上只能使用第4通道

DMA数据源端与目的端的传输宽度可以独立配置, 比如源(1Byte)->目的(1WORD). DMA可以实现数据模拟打包与解包. 打包就是把小单位数据组装为大单位, 比如从Byte到WORD, DMA可以根据配置规则把4Byte打包为一个WORD传输过去, 也可以反过来, 把1WORD解包为4Byte传输.

DMA支持循环缓冲区功能, 就比如指定给DMA一块连续内存区域作为缓冲区(0x00-0xFF), 开启循环模式后, 从开头开始读到0xFF时, DMA自动跳到0x00继续传输, 形成循环.

单次数据传输长度配置范围为0-2^16-1, 也就是0-65535. 这个长度是数据个数, 当前面配置传输单位为Byte时, 单次传输最大长度为64kb, 当配置为WORD时, 单次传输长度为256kb

DMA每个通道都可以触发3种中断, 传输完成中断, 半传输完成中断, 传输错误中断. 假如我们要传输1024个字节, 传输完成512字节时触发半完成中断, 传输完1024字节时, 触发完成中断, 传输失败时, 触发失败中断.

总结DMA传输的流程:

  1. HAL_UART_Transmit_DMA() 启动, 发起单次传输请求, 告诉DMA数据源地址, 数据源长度, 目标地址.
    这时, DMA开始根据优先级裁决串口的TX请求, 串口开启DMA请求源, 串口TDR寄存器为空, TXE=1
  2. 开始传输, DMA搬运第1字节到TDR寄存器中, 此时TDR满, TXE=0
    这时, 串口把TDR寄存器的数据送出(送到移位寄存器Shift Register后发送到总线)
    然后, TDR又空了, TXE=1
  3. DMA开始搬运第2个字节, 重复步骤2, DMA与串口就这样并行地搬运并发送数据
  4. DMA根据数据长度, 把最后一字节搬运到TDR时, 触发DMA的TC中断, 传输任务结束
  5. 串口清空TDR寄存器后, 移位寄存器Shift Register发送完最后一个字节, 这时TDR和移位寄存器都为空, 触发串口的TC中断, 这时整个传输过程结束.
术语翻译

on-chip memory-mapped devices 片上内存映射设备
round-robin scheduling 公平的总线仲裁算法, 即同等优先级的主设备轮流占用总线
single DMA transfer 单次传输
block transfer 块传输, 多次单传输的组合
TDR(Transmit data register)寄存器 数据发送寄存器
TXE(Transmit data register empty) 串口ISR寄存器中的一个状态位, 0=数据尚未传送, 1=数据已传送
TC: Transmission complete 串口ISR寄存器中的一个状态位, 0=传输未完成, 1=传输已完成

CubeMX配置串口DMA

在TX行中, Priority优先级选择中等

Mode模式有两种

Normal: 发送完设定的长度就停止

Circular: 发送完之后自动重头开始(这个模式常用于接受, 发送很少用)

Increment Address, 地址自增

Peripheral(外设地址): 不勾选, 串口发送的寄存器地址是固定的

Memory(内存地址):要勾选, 发送时要依次读取数组的下一个字节

Data Width 数据宽度均选择Byte字节

CubeMX配置串口中断

DMA搬运完数据后, 会通过串口中断来统治HAL库更新 huart->gState状态, 如果不开启, HAL_UART_Transmit_DMA 就只能执行成功一次, 之后会一直卡在HAL_BUSY状态.

执行完上述配置后, 点击 GENERATE 生成代码

DMA代码配置解析

配置完成后新增代码如下各个文件所示

dma.h

c 复制代码
#ifndef __DMA_H__
#define __DMA_H__

#ifdef __cplusplus
extern "C" {
#endif

#include "main.h"

void MX_DMA_Init(void);

#ifdef __cplusplus
}
#endif

#endif /* __DMA_H__ */

dma.c

c 复制代码
#include "dma.h"

void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Channel4_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);

}

stm32l4xx_it.h

c 复制代码
#ifndef __STM32L4xx_IT_H
#define __STM32L4xx_IT_H

#ifdef __cplusplus
extern "C" {
#endif

// ...
void DMA1_Channel4_IRQHandler(void);
void USART1_IRQHandler(void);
// ...

#ifdef __cplusplus
}
#endif

#endif /* __STM32L4xx_IT_H */

stm32l4xx_it.c

c 复制代码
#include "main.h"
#include "stm32l4xx_it.h"

extern DMA_HandleTypeDef hdma_usart1_tx;
extern UART_HandleTypeDef huart1;

/**
  * @brief This function handles DMA1 channel4 global interrupt.
  */
void DMA1_Channel4_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_tx);
}

/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

usart.c

c 复制代码
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
    // ...
    
    /* USART1 DMA Init */
    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA1_Channel4;
    // 硬件触发源为DMA_REQUEST_2
    hdma_usart1_tx.Init.Request = DMA_REQUEST_2;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    // DMA_PINC_DISABLE外设地址不自增, 因为串口发送寄存器的地址TDR时固定的
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    // DMA_MINC_ENABLE 内存地址自增. 发送字符串时, 
    // DMA搬运完第1个字节, 指针自动指向第2个字节
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; 
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{
  if(uartHandle->Instance==USART1)
  {
    // ...
    
    /* USART1 DMA DeInit */
    HAL_DMA_DeInit(uartHandle->hdmatx);

    /* USART1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(USART1_IRQn);
  }
}

main.c

c 复制代码
#include "dma.h"

int main(void)
{
  ...
  MX_DMA_Init();
  MX_USART1_UART_Init();
  
  uint8_t msg[] = "DMA Working!\r\n";
  while (1)
  {
    HAL_UART_Transmit_DMA(&huart1, msg, sizeof(msg)-1);
    delay_s(1);
    ...
  }
}

通过上述代码可知, 串口DMA初始化配置流程如下:

  1. 使能DMA时钟

  2. 设置DMA通道4的优先级(我选择了DMA串口通过通道4传输数据)

  3. 使能DMA中断

    串口 DMA 发送不仅需要 DMA 中断,还需要串口全局中断。DMA 负责搬运,串口中断负责在搬运完后发送"停止位"并清理状态。

  4. 在中断处理中编写DMA通道4中断处理函数还有串口1的中断处理函数

  5. 在串口Msp初始化函数中配置串口DMA初始化参数, 包括使用哪个DMA通道, DMA优先级等等.

    这里最关键的是 __HAL_LINKDMA 宏,它把 hdma_usart1_tx(搬运工)和 huart1(仓库管理员)真正绑定在了一起

  6. DMA初始化, 串口初始化, main函数打印

    这里要注意的是, MX_DMA_Init 必须在 MX_USART1_UART_Init 之前调用,否则串口初始化时 DMA 句柄还是空的

封装完整的串口传输逻辑

uart.h

c 复制代码
#define UART_DMA_BUF_SIZE 128

typedef struct {
    volatile bool is_busy;       // DMA 是否正在发送
    volatile bool dma_enabled;   // DMA 模式总开关
} UART_DMA_Control_t;

extern UART_DMA_Control_t g_uart_dma;

void UART_DMA_StartTransfer(void);

uart.c

c 复制代码
UART_DMA_Control_t g_uart_dma = {0};


// DMA 传输完成中断回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        g_uart_dma.is_busy = false;
    }
}

// 异常捕获:自动切回阻塞
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 发生任何串口错误(溢出、断路、DMA 错误),立即降级
        g_uart_dma.dma_enabled  = false;
        g_uart_dma.is_busy      = 0;
    }
}

trace.c

c 复制代码
#include "usart.h"

static char g_dma_tx_buf[UART_DMA_BUF_SIZE] = {0};

void trace_uart_send(const char *buf, uint16_t len);

void trace_printf(const char *fmt, ...)
{
  char tmp[UART_DMA_BUF_SIZE] = {0};

    va_list va;
    va_start(va, fmt);
    int len = vsnprintf(tmp, sizeof(tmp), fmt, va);
    va_end(va);

    if (len <= 0) return;
    if (len >= sizeof(tmp)) {
      // 超出长度直接截断, 在结尾加cut标记
      const char *cut = "...[cut]\r\n";
      memcpy(tmp + sizeof(tmp) - strlen(cut) - 1, cut, strlen(cut));
      len = sizeof(tmp) - 1;
    }

    memcpy(g_dma_tx_buf, tmp, len);

    trace_uart_send(g_dma_tx_buf, len);
}

void trace_uart_send(const char *buf, uint16_t len) {
  /* 关中断,防止中断里也打印 */
    __disable_irq();

    if (g_uart_dma.dma_enabled && !g_uart_dma.is_busy) {
        /* DMA 模式发送 */
        g_uart_dma.is_busy = true;
        HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, len);
        __enable_irq();
    } else {
        /* DMA 未就绪 / 正忙 / 故障 → 阻塞发送 */
        __enable_irq();
        HAL_UART_Transmit(&huart1, (uint8_t *)buf, len, 1000);
    }
}

main.c

c 复制代码
  TRACE_INFO("UART TX Polling Mode");

  // 在此之前, 使用阻塞串口输出
  // 检查硬件状态后开启高速模式
  if (huart1.hdmatx != NULL) { // 简单检查 DMA 是否链接
      g_uart_dma.dma_enabled = true;
      TRACE_INFO("UART DMA Mode Enabled. High Performance On.");
  }
  // 这里后面就都是DMA输出了

当调用printf时, 完全用阻塞输出

调用TRACE打印时, 会执行DMA打印逻辑

在这里没有使用循环队列打印的方式处理, 一个是逻辑复杂, 再者就是能够用到那样长度打印的场景不多见, 这样直接根据BUFFER长度截断的方式已经能够覆盖绝大多数场景.

测试超长打印字符串时, 输出结果如下:

复制代码
[I] abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi...[cut]
[I] abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi...[cut]
[I] abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi...[cut]
相关推荐
猫猫的小茶馆2 小时前
【Linux 驱动开发】二. linux内核模块
linux·汇编·arm开发·驱动开发·stm32·嵌入式硬件·架构
飞睿科技2 小时前
解析ESP-SparkBot开源大模型AI桌面机器人的ESP32-S3核心方案
人工智能·嵌入式硬件·物联网·机器人·esp32·乐鑫科技·ai交互
風清掦2 小时前
【江科大STM32学习笔记-03】GPIO通用输入输出口
笔记·stm32·单片机·学习
碎碎思2 小时前
走向开放硅:Baochip-1x 的 RISC-V MCU 架构与工程实践
单片机·嵌入式硬件·risc-v
搞全栈小苏2 小时前
嵌入式之 LVGL 的切换页面研究:杜绝内存泄漏(单片机与 Linux 平台)(链表与多进程方式)
linux·单片机·链表·lvgl
想放学的刺客2 小时前
单片机嵌入式试题(第20期)通信协议深度解析与系统调试实战
stm32·单片机·嵌入式硬件·物联网·51单片机
赤~峰2 小时前
S32DS for S32 Platform RTC输出时间
单片机·mcu
Y1rong11 小时前
STM32之中断(二)
stm32·单片机·嵌入式硬件
Y1rong11 小时前
STM32之中断(一)
stm32·单片机·嵌入式硬件