基于STM32F407的串口通信实战:实现DMA传输

AI助手已提取文章相关产品:

基于STM32F407的串口通信实战:用DMA释放CPU,真正实现“发完就忘”

你有没有遇到过这种情况——系统明明跑得挺稳,但只要一打开串口打印大量日志,主任务就开始卡顿?或者在做固件升级时, HAL_UART_Transmit() 一调用就是几百毫秒的阻塞,整个实时性直接崩了?

说实话,我第一次调试一个图像上传功能的时候就被坑惨了。每帧数据大概1.5KB,波特率设的是115200,结果发现MCU几乎啥也干不了——光是等串口发完这一帧,就得花上百毫秒,期间还不断进中断,调度全乱套了。

后来才明白,传统轮询或中断方式处理串口,在大数据量面前根本扛不住。每个字节都要CPU参与?那不是让飞行员去拧螺丝嘛!

直到我用了 DMA + USART 的组合拳,才算真正把串口用明白了。


为什么你的串口总是拖累系统性能?

我们先来算笔账。

假设你用的是常见的 115200bps 波特率,传输8位数据+1停止位(即每帧10bit),那么理论最大吞吐量是:

115200 / 10 = 11,520 字节/秒

也就是说,每秒要收发超过一万一千个字节。如果采用中断方式接收,意味着 每秒要触发上万次中断

这还不包括发送端的情况。一旦你在主循环里频繁调用 printf 或者批量发送数据,CPU就会陷入“填缓冲 → 等完成 → 再填”的死循环中。

更别说现在很多应用已经不只是传几个参数了:
- 要上传传感器原始波形?
- 要做远程固件更新(OTA)?
- 要把摄像头拍到的BMP图片通过串口吐出去?

这时候你还指望靠 while(HAL_UART_Transmit()) 撑住系统?别闹了 😅

好在,STM32F407这种级别的MCU早就给你准备好了“外挂”—— DMA


DMA到底是什么?它凭什么能让CPU躺平?

简单说,DMA 就是一个 独立的数据搬运工

它能在不打扰CPU的情况下,直接把内存中的数据搬到外设寄存器(比如USART的TDR),或者反过来,把外设接收到的数据自动存进内存缓冲区。

想象一下:以前是你自己一趟趟扛箱子从A地送到B地;现在你雇了个快递小哥,只告诉他起点、终点和数量,剩下的他全包了。你就可以安心写代码、处理业务逻辑去了。

STM32F407上的DMA架构长什么样?

STM32F407有两个DMA控制器:
- DMA1 :7个stream
- DMA2 :8个stream

每个stream可以绑定不同的channel(通道),对应不同外设的DMA请求信号。

以我们最常用的 USART1 为例:
- 发送(TX)→ 映射到 DMA2_Stream6,Channel 4
- 接收(RX)→ 映射到 DMA2_Stream5,Channel 4

📌 提示:APB2总线最高支持84MHz时钟,所以USART1理论上可达10.5Mbps波特率,配合DMA完全能跑满物理极限。

而且DMA支持多种工作模式:
- Normal Mode :传一次就停
- Circular Mode :循环缓冲,适合持续采样
- 还有FIFO、优先级控制、突发传输优化等高级特性

换句话说,只要你配置得当,DMA甚至能帮你实现一个“永不丢包”的接收环形缓冲区。


实战第一步:让DMA替你发数据

咱们先来看最典型的场景——异步发送一大段数据,比如JSON格式的状态上报、固件镜像传输等等。

目标很明确: 调用发送函数后立即返回,后台静默完成传输,完成后通知我一声就行。

HAL库怎么配DMA发送?

// 全局DMA句柄
DMA_HandleTypeDef hdma_usart1_tx;

static void MX_DMA_Init(void)
{
    __HAL_RCC_DMA2_CLK_ENABLE();

    // 配置DMA2_Stream6用于USART1_TX
    hdma_usart1_tx.Instance = DMA2_Stream6;
    hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;              // 对应USART1
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;     // 内存→外设
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;        // 外设地址固定(都是往TDR写)
    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_HIGH;        // 高优先级
    hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK) {
        Error_Handler();
    }

    // 关键一步:把DMA和UART关联起来!
    __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
}

📌 注意这个宏: __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
它的作用是告诉HAL库:“以后 huart1 的发送DMA就用我刚配好的这个句柄。”
否则你调 HAL_UART_Transmit_DMA() 的时候会找不到DMA,直接卡死。

然后初始化UART的时候别忘了开启DMA时钟并使能TX DMA请求:

huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;

if (HAL_UART_Init(&huart1) != HAL_OK) {
    Error_Handler();
}

// 启用DMA发送请求
__HAL_UART_ENABLE_DMA(&huart1, UART_DMAReq_Tx);

怎么发?一句话搞定!

uint8_t tx_buffer[] = "{\"temp\":25.3,\"hum\":60}\r\n";
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer) - 1);

✅ 调用完立刻返回!
✅ CPU继续执行其他任务!
✅ 数据由DMA自动搬进USART_TDR!
✅ 传完触发中断,回调告诉你“搞定了”!

别忘了写回调函数

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 可以在这里置标志位、切换状态机、启动下一轮发送...
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // 指示发送完成
    }
}

💡 小技巧:如果你想连续发送多个包,可以在回调里再次调用 HAL_UART_Transmit_DMA() ,形成链式传输。


更狠的操作:DMA接收 + 空闲线检测,彻底告别字节级中断

发送还好办,顶多是阻塞问题。真正头疼的是接收——你怎么知道对方发完了?

很多人还在用这种方式:

while (HAL_UART_Receive(&huart1, &ch, 1, 10) == HAL_OK) {
    buf[i++] = ch;
}

拜托……这种写法不仅效率低,还会导致超时判断不准,尤其在不定长协议(如AT指令、Modbus)中极易出错。

正确的做法是: DMA + IDLE Line Detection(空闲线检测)

它是怎么工作的?

STM32的USART有一个非常实用的功能叫 IDLE 中断 :当RX线上连续出现一个完整字符时间以上的静默期时,就会触发一次中断。

这意味着什么?

👉 如果对方一次性发来一串数据(比如”AT+READ?\r\n”),中间没有停顿,最后突然断开,那这个“断开瞬间”就会产生一个IDLE事件!

于是我们可以这样设计接收机制:
1. 启动DMA,让它一直把收到的数据往某个缓冲区搬;
2. 不管来了多少字节,都先存着;
3. 一旦检测到总线空闲(IDLE中断),说明一帧数据很可能已经收完了;
4. 此时再唤醒CPU去处理这整块数据。

这样一来,哪怕对方发了512字节,你也只被打扰一次!


配置DMA接收流

DMA_HandleTypeDef hdma_usart1_rx;

static void MX_DMA_Init(void)
{
    // ...前面发送部分省略...

    // 接收流:DMA2_Stream5
    hdma_usart1_rx.Instance = DMA2_Stream5;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;        // 关键!循环模式
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

    HAL_DMA_Init(&hdma_usart1_rx);

    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}

注意这里用了 DMA_CIRCULAR 模式 ,意味着缓冲区满了不会停,而是从头开始覆盖。这对长期运行的系统特别有用。

接着开启UART的DMA接收请求和IDLE中断:

// 启动DMA接收
uint8_t rx_buffer[256];
HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);

// 开启IDLE中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

记得在NVIC里也使能USART1的中断:

HAL_NVIC_SetPriority(USART1_IRQn, 0, 1);
HAL_NVIC_EnableIRQ(USART1_IRQn);

在中断服务函数中捕获IDLE事件

void USART1_IRQHandler(void)
{
    uint32_t tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE);
    uint32_t tmp_it_source = __HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_IDLE);

    if (tmp_flag && tmp_it_source) {
        // 清除标志位(必须先读SR,再读DR)
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);

        // 停止当前DMA传输以便读取有效长度
        HAL_UART_DMAStop(&huart1);

        // 计算实际接收到的字节数
        uint16_t rx_len = 256 - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);

        // 把数据拷贝到安全区域处理(避免被后续DMA覆盖)
        memcpy(app_rx_buffer, rx_buffer, rx_len);
        app_rx_length = rx_len;

        // 标志位置位,通知主循环处理
        rx_complete_flag = 1;

        // 重启DMA接收
        __HAL_DMA_DISABLE(&hdma_usart1_rx);
        hdma_usart1_rx.Instance->M0AR = (uint32_t)rx_buffer;  // 重置地址
        hdma_usart1_rx.Instance->NDTR = 256;                 // 重置计数
        __HAL_DMA_ENABLE(&hdma_usart1_rx);
    }

    // 其他中断处理...
    HAL_UART_IRQHandler(&huart1);
}

🎯 核心思路就是:
- 收到IDLE → 说明帧结束 → 算出DMA已搬了多少字节 → 拷走数据 → 重置DMA继续监听

这样既保证了高效接收,又避免了频繁中断打扰CPU。


实际工程中的那些坑,我都替你踩过了 💣

你以为配完就能高枕无忧?Too young.

我在实际项目中遇到过不少诡异问题,分享几个典型雷区,帮你少走弯路。

❌ 问题1:DMA接收缓冲区被Cache“保护”了?

如果你开启了DCache(比如某些带FPU的复杂系统),而你的接收缓冲区恰好落在可缓存区域,可能会出现一种奇怪现象:

“明明看到DMA说收到了100个字节,但我一读内存,全是0!”

原因很简单:DMA写进了物理内存,但CPU读的是Cache里的旧数据。

✅ 解决方案有两种:
1. 把接收缓冲区定义在 非缓存区(Uncached Region)
2. 或者在读取前手动刷新Cache:

SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, 256);

推荐做法是在链接脚本里划一块NO_CACHE区域专门给DMA用。

❌ 问题2:DMA传输中途被抢占,数据错乱?

DMA虽然强大,但也怕冲突。比如你正在用DMA发数据,另一个高优先级任务突然也要用同一个Stream(比如ADC也在用DMA2_Stream6),会发生什么?

轻则传输失败,重则地址错乱、HardFault。

✅ 解决办法:
- 使用不同的DMA控制器或Stream;
- 必须共用时,加互斥锁(比如用信号量保护DMA资源);
- 或者干脆提高关键DMA的优先级到“极高”。

❌ 问题3:循环模式下如何防止数据被覆盖?

你开了Circular模式,DMA一直在写rx_buffer。但如果主程序没及时处理,新来的数据就把老数据盖掉了。

怎么办?

✅ 建议引入双缓冲机制(Double Buffer)或使用 DMA Half-Transfer Interrupt

// 启用半传输中断
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemBurst = DMA_MBURST_SINGLE;
hdma_usart1_rx.XferHalfCpltCallback = MyHalfCpltCallback;  // 注册回调

当DMA写满前一半(128字节)时就提醒你处理,另一半还能接着收。相当于流水线作业,大大降低丢包风险。


这些场景,闭眼用DMA就对了 ✅

别再纠结要不要上DMA了,下面这些情况直接冲:

应用场景 是否推荐DMA 理由
日常调试打印 ❌ 否 数据量小,用 ITM 或普通中断足矣
固件OTA升级 ✅ 强烈推荐 几百KB数据,必须异步后台传输
传感器原始数据聚合 ✅ 推荐 高频采样+批量上报,减少中断抖动
Modbus RTU主站轮询 ✅ 推荐 提升响应速度,降低协议层延迟
图像/音频流传输 ✅ 必须用 否则CPU直接瘫痪
日志持久化存储 ✅ 推荐 避免影响主控逻辑

特别是做工业网关、边缘计算盒子这类设备,串口往往是连接PLC、仪表的核心通路,DMA几乎是标配。


给初学者的几点建议 🛠️

  1. 不要一开始就追求完美架构
    先从简单的单次DMA发送开始,确保基本流程通了再说。然后再加接收、加IDLE、加双缓冲。

  2. 善用调试工具
    - 用逻辑分析仪看TX波形,确认是否真的连续发出;
    - 在回调函数里翻转GPIO,用示波器测DMA传输耗时;
    - 查看 __HAL_DMA_GET_COUNTER() 值验证剩余字节数。

  3. 学会看参考手册的关键章节
    - RM0090 第9章:DMA controller
    - RM0090 第19章:USART
    - 数据手册中的DMA请求映射表

  4. 关注DMA状态机
    Stream不是随时都能写的,要检查是否处于“Ready”状态。可以用 HAL_DMA_GetState() 判断。

  5. 命名规范很重要
    比如:
    c DMA_HandleTypeDef hdma_usart1_tx; DMA_HandleTypeDef hdma_usart1_rx;
    清晰明了,后期维护不抓瞎。


写到最后:嵌入式开发的本质是“聪明地偷懒”

DMA不是一个炫技的功能,它是嵌入式工程师用来 提升系统效率、增强实时性、降低功耗 的核心武器之一。

当你学会让硬件替你干活,你才有资格谈“高性能系统设计”。

下次当你又要写一堆 while(!tx_complete) 的时候,停下来问问自己:

“我真的需要亲自盯着每一个字节发出去吗?”

也许答案早已写在STM32的DMA控制器里了 😉

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

内容概要:本文介绍了ENVI Deep Learning V1.0的操作教程,重点讲解了如何利用ENVI软件进行深度学习模型的训练与应用,以实现遥感图像中特定目标(如集装箱)的自动提取。教程涵盖了从数据准备、标签图像创建、模型初始化与训练,到执行分类及结果优化的完整流程,并介绍了精度评价与通过ENVI Modeler实现一键化建模的方法。系统基于TensorFlow框架,采用ENVINet5(U-Net变体)架构,支持通过点、线、面ROI或分类图生成标签数据,适用于多/高光谱影像的单一类别特征提取。; 适合人群:具备遥感图像处理基础,熟悉ENVI软件操作,从事地理信息、测绘、环境监测等相关领域的技术人员或研究人员,尤其是希望将深度学习技术应用于遥感目标识别的初学者与实践者。; 使用场景及目标:①在遥感影像中自动识别和提取特定地物目标(如车辆、建筑、道路、集装箱等);②掌握ENVI环境下深度学习模型的训练流程与关键参数设置(如Patch Size、Epochs、Class Weight等);③通过模型调优与结果反馈提升分类精度,实现高效自动化信息提取。; 阅读建议:建议结合实际遥感项目边学边练,重点关注标签数据制作、模型参数配置与结果后处理环节,充分利用ENVI Modeler进行自动化建模与参数优化,同时注意软硬件环境(特别是NVIDIA GPU)的配置要求以保障训练效率。
内容概要:本文系统阐述了企业新闻发稿在生成式引擎优化(GEO)时代下的全渠道策略与效果评估体系,涵盖当前企业传播面临的预算、资源、内容与效果评估四大挑战,并深入分析2025年新闻发稿行业五大趋势,包括AI驱动的智能化转型、精准化传播、首发内容价值提升、内容资产化及数据可视化。文章重点解析央媒、地方官媒、综合门户和自媒体四类媒体资源的特性、传播优势与发稿策略,提出基于内容适配性、时间节奏、话题设计的策略制定方法,并构建涵盖品牌价值、销售转化与GEO优化的多维评估框架。此外,结合“传声港”工具实操指南,提供AI智能投放、效果监测、自媒体管理与舆情应对的全流程解决方案,并针对科技、消费、B2B、区域品牌四大行业推出定制化发稿方案。; 适合人群:企业市场/公关负责人、品牌传播管理者、数字营销从业者及中小企业决策者,具备一定媒体传播经验并希望提升发稿效率与ROI的专业人士。; 使用场景及目标:①制定科学的新闻发稿策略,实现从“流量思维”向“价值思维”转型;②构建央媒定调、门户扩散、自媒体互动的立体化传播矩阵;③利用AI工具实现精准投放与GEO优化,提升品牌在AI搜索中的权威性与可见性;④通过数据驱动评估体系量化品牌影响力与销售转化效果。; 阅读建议:建议结合文中提供的实操清单、案例分析与工具指南进行系统学习,重点关注媒体适配性策略与GEO评估指标,在实际发稿中分阶段试点“AI+全渠道”组合策略,并定期复盘优化,以实现品牌传播的长期复利效应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值