当前位置: 首页 > news >正文

STM32串口通信实战:双机UART连接、协议设计与DMA优化

1. 项目概述:为什么需要连接两块STM32板子?

在嵌入式开发的实际项目中,我们常常会遇到一个核心需求:让两块或多块单片机协同工作。比如,一块板子负责采集传感器数据(温度、湿度、姿态),另一块板子负责复杂算法处理或作为人机交互界面;又或者,在分布式系统中,主控板需要向多个执行单元发送指令。这时,最简单、最经典、成本最低的通信方式,就是串口(UART)。它只需要两根线(TX和RX)就能实现全双工数据交换,是MCU之间“对话”的首选协议。

我手头这个项目,就是基于两块STM32F103C8T6(俗称“蓝桥杯”或“最小系统板”)来实现一个简单的数据透传和指令控制系统。一块板子作为“主机”,模拟采集数据并发送控制命令;另一块作为“从机”,接收命令并执行相应动作(比如点亮不同的LED),同时将自身状态回传。这个场景虽然基础,但涵盖了串口通信从硬件连接到软件协议设计的完整流程,是理解更复杂通信协议(如CAN、SPI、I2C)的绝佳起点。无论你是刚接触STM32的新手,还是想巩固通信基础的老鸟,跟着走一遍,都能把串口那点事儿彻底搞明白。

2. 硬件连接与电平匹配:别让第一步就“翻车”

串口通信听起来简单,但硬件连接上的坑最多。很多初学者烧了芯片或者通信乱码,问题都出在这里。

2.1 核心连接原则:交叉互联

串口通信的核心是交叉互联:A设备的发送端(TX)连接B设备的接收端(RX),A设备的接收端(RX)连接B设备的发送端(TX)。对于两块STM32板子,假设我们使用USART1,那么连接方式如下:

  • 板A的PA9(USART1_TX) ---> 板B的PA10(USART1_RX)
  • 板A的PA10(USART1_RX) ---> 板B的PA9(USART1_TX)
  • 板A的GND---> 板B的GND至关重要!共地是信号参考的基础)

注意:务必使用杜邦线牢固连接,并确保GND已连接。浮空的GND会导致电平参考混乱,通信必然失败。

2.2 电平匹配:3.3V的STM32世界

STM32的IO口电平是3.3V。幸运的是,STM32的USART端口是兼容3.3V TTL电平的。因此,两块同为3.3V供电的STM32板子,其串口可以直接相连,无需任何电平转换电路。这是STM32生态带来的便利。

但是,如果你需要连接一个5V TTL电平的设备(比如某些老款的Arduino),直接连接就可能损坏STM32的IO口(虽然很多STM32的IO口容忍5V,但并非全部,且非长久之计)。这时就需要一个电平转换电路,例如使用TXS0108E这类双向电平转换芯片,或者简单的电阻分压电路(将5V TX端的信号通过两个电阻分压至约3.3V后再接入STM32的RX)。

实操心得:在连接任何设备前,先用万用表测量一下对方串口的空闲电平。对于TTL串口,空闲时为高电平。如果测出是5V,就要警惕了。对于STM32之间的通信,确认双方都是3.3V供电,就可以放心直连。

2.3 是否需要外部上拉?

STM32的USART TX引脚在配置为推挽输出后,驱动能力足够,一般不需要上拉。RX引脚配置为浮空输入即可。但在一些干扰较强的环境中,或者线路较长时,可以在RX引脚上添加一个4.7kΩ~10kΩ的上拉电阻到3.3V,以提高抗干扰能力,避免因干扰产生误起始位。对于板间短距离通信(<30cm),通常可以省略。

3. 软件配置详解:从CubeMX到代码生成

硬件连好后,我们进入软件层面。我将使用STM32CubeMX进行初始化配置,这是目前最高效、最不容易出错的方式。

3.1 CubeMX工程基础配置

  1. 选择芯片:为两个工程分别选择正确的STM32型号(例如STM32F103C8Tx)。
  2. 系统核心(SYS):将Debug改为Serial Wire,这样既可以用ST-Link调试,又不占用用于串口的PA13/PA14引脚。
  3. 时钟(RCC)
    • High Speed Clock (HSE)选择Crystal/Ceramic Resonator。外部8MHz晶振是保证串口波特率精确的基础。
    • Clock Configuration标签页,配置系统时钟。对于F103,通常通过PLL将HSE 8MHz倍频到72MHz作为系统时钟(SYSCLK)。
  4. USART1配置
    • 模式:选择Asynchronous(异步模式)。
    • 基本参数:这是关键,两块板子的这些参数必须完全一致!
      • Baud Rate(波特率): 115200 Bits/s(常用值,兼顾速度和稳定性)。
      • Word Length(字长): 8 Bits(最常用)。
      • Parity(奇偶校验): None(无校验)。
      • Stop Bits(停止位): 1。
      • Over Sampling(过采样): 16 Samples(默认,抗噪性好)。
    • 引脚:默认PA9为USART1_TX, PA10为USART1_RX,通常无需改动。
    • NVIC设置:勾选USART1 global interrupt使能全局中断。这是我们采用“中断接收+轮询发送”架构的基础。

3.2 生成代码与项目结构

配置完成后,点击GENERATE CODE生成Keil MDK(或你使用的IDE)工程。CubeMX会生成所有外设的初始化代码MX_USART1_UART_Init(),并自动将HAL库文件加入工程。

生成的代码关键点检查

  • main.c中,SystemClock_Config()函数是否正确配置了72MHz时钟。
  • MX_USART1_UART_Init()函数中,波特率等参数是否与设置一致。
  • stm32f1xx_it.c中,已经生成了USART1_IRQHandler()中断服务函数框架,它会调用HAL库的中断处理函数HAL_UART_IRQHandler(&huart1)

4. 通信程序架构设计:中断、DMA与协议

直接在主循环里用HAL_UART_Receive()这种阻塞式接收是极不推荐的,它会卡死整个程序。我们需要更高效的架构。

4.1 推荐架构:中断接收 + 环形缓冲区 + 协议解析

这是最经典且稳定的方案。

  1. 中断接收:使能串口接收中断。每当收到一个字节,硬件就会触发中断,在中断服务程序(ISR)中,我们迅速将这个字节存入一个预先定义好的数组(环形缓冲区),然后立即退出中断。这个过程非常快,不影响主程序运行。
  2. 环形缓冲区:一个头指针(RxBuf_Head)和一个尾指针(RxBuf_Tail)管理的数组。中断程序向Head位置写数据,主程序从Tail位置读数据。当Head追上Tail,表示缓冲区满;当Tail追上Head,表示缓冲区空。这解决了数据覆盖和丢失的问题。
  3. 主程序轮询解析:在主循环中,不断检查环形缓冲区是否有新数据。一旦发现完整的一帧数据(根据自定义协议判断,例如判断帧头、帧尾和长度),就将其从缓冲区中取出进行解析和处理。

4.2 代码实现:中断接收与缓冲区管理

首先,在main.c的私有变量定义区,定义环形缓冲区及相关变量:

/* Private variables ---------------------------------------------------------*/ #define RX_BUF_SIZE 256 uint8_t Uart1_RxBuf[RX_BUF_SIZE]; // 环形缓冲区 volatile uint16_t Uart1_RxHead = 0; // 写指针,中断修改,必须加volatile volatile uint16_t Uart1_RxTail = 0; // 读指针,主程序修改 uint8_t Uart1_RxByte = 0; // 临时存放接收到的字节

main()函数的初始化部分,在MX_USART1_UART_Init()之后,启动串口接收中断:

/* 启动串口空闲中断接收一个字节 */ /* 注意:这里先接收一个字节,后续在中断中连续接收 */ if (HAL_UART_Receive_IT(&huart1, &Uart1_RxByte, 1) != HAL_OK) { Error_Handler(); }

然后,我们需要重写(Override)HAL库的串口接收完成回调函数。这个函数会在每次成功接收一个字节后,被HAL_UART_IRQHandler调用。在main.c/* USER CODE BEGIN 4 */区域添加:

/* 重写HAL库的UART接收完成回调函数 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) // 判断是哪个串口 { /* 将收到的字节存入环形缓冲区头部 */ Uart1_RxBuf[Uart1_RxHead] = Uart1_RxByte; Uart1_RxHead = (Uart1_RxHead + 1) % RX_BUF_SIZE; // 头指针循环 /* 如果缓冲区满了,丢弃最旧的数据(尾指针前移) */ if (Uart1_RxHead == Uart1_RxTail) { Uart1_RxTail = (Uart1_RxTail + 1) % RX_BUF_SIZE; // 这里可以设置一个错误标志,通知主程序发生了溢出 } /* 重新启动中断接收,等待下一个字节 */ HAL_UART_Receive_IT(&huart1, &Uart1_RxByte, 1); } }

4.3 自定义通信协议设计

原始字节流没有意义,我们需要定义一种帧格式,让双方知道一段数据的开始、结束和含义。一个简单实用的协议如下:

字节位置字段名长度说明示例值(十六进制)
0帧头1固定值,标识帧开始0xAA
1命令字1指示本帧数据的类型或意图0x01 (查询)
2数据长度 N1后续有效数据的字节数0x04
3 ~ 3+N-1数据域N实际要传输的数据...
3+N校验和1前面所有字节的累加和(取低8位)SUM
3+N+1帧尾1固定值,标识帧结束0x55

协议解析流程(在主循环中)

  1. 从环形缓冲区读取一个字节。
  2. 如果读到0xAA,进入“寻找帧”状态。
  3. 继续读取,拿到命令字、数据长度N。
  4. 再连续读取N个字节的数据。
  5. 读取校验和字节,计算前面所有字节(帧头到数据域最后一个字节)的累加和,与收到的校验和比对。不一致则丢弃本帧。
  6. 最后读取帧尾0x55,确认帧格式完整。
  7. 校验通过后,根据“命令字”执行相应操作。

发送一帧数据的函数示例

void Uart1_Send_Frame(uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t frame[256]; // 临时缓冲区,足够大 uint8_t frame_len = 0; uint8_t checksum = 0; // 帧头 frame[frame_len++] = 0xAA; checksum += 0xAA; // 命令字 frame[frame_len++] = cmd; checksum += cmd; // 数据长度 frame[frame_len++] = len; checksum += len; // 数据域 for (int i = 0; i < len; i++) { frame[frame_len++] = data[i]; checksum += data[i]; } // 校验和 frame[frame_len++] = checksum; // 帧尾 frame[frame_len++] = 0x55; // 使用HAL库阻塞式发送(简单演示,实际可用DMA) HAL_UART_Transmit(&huart1, frame, frame_len, 1000); }

5. 高级话题:使用DMA提升效率与稳定性

当数据量较大或频率较高时,频繁的中断仍然会消耗不少CPU资源。此时,DMA(直接存储器访问)是更好的选择。DMA可以在外设(如UART)和内存(如数组)之间直接搬运数据,无需CPU干预。

5.1 串口接收DMA配置(CubeMX)

  1. 在CubeMX的USART1配置中,将DMA Settings选项卡下的Add,为USART1_RX添加一个DMA流(例如DMA1_Channel5)。
  2. 模式选择Circular(循环模式)。这样DMA会持续不断地将串口接收到的数据搬运到我们指定的大数组中,当数组填满后,会自动从头开始覆盖,形成一个硬件实现的环形缓冲区。
  3. 数据宽度都选择Byte

5.2 代码实现:DMA接收+空闲中断

仅配置DMA循环接收还不够,我们不知道何时收到了一帧完整的数据。这里需要结合串口空闲中断(IDLE Interrupt)。当串口总线上一段时间没有新数据(即空闲时间超过一个字符传输时间),就会产生空闲中断。

配置步骤

  1. 在CubeMX中使能USART1的全局中断(NVIC)。
  2. 在代码中,需要手动使能串口的空闲中断:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
  3. USART1_IRQHandler函数中,除了调用HAL_UART_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已经接收了多少数据 uint16_t dma_received_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 获取本次数据的起始位置(需要根据上次处理的位置计算) // 将 dma_received_len 长度的新数据交给协议解析函数处理 // 然后更新“上次处理位置”指针 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }

这种方式效率极高,CPU只在收到一整包数据后才被中断唤醒进行处理,非常适合高速、大数据量的串口通信。

6. 调试技巧与常见问题排查

即使按照步骤操作,第一次调通串口也常会遇到问题。下面是我总结的排查清单。

6.1 通信完全无反应(收不到任何数据)

  1. 检查硬件连接:这是第一位的!用万用表通断档检查TX-RX是否交叉连接,GND是否连通。检查杜邦线是否完好。
  2. 检查引脚复用:确认使用的PA9/PA10没有被其他功能占用(比如JTAG)。在CubeMX中检查引脚颜色,确保没有冲突警告。
  3. 检查时钟配置:串口波特率依赖于系统时钟。如果系统时钟配置错误(比如还是用的内部HSI 8MHz),实际波特率会严重偏离。确保SystemClock_Config()正确,并且HAL_RCC_GetSysClockFreq()返回的值是预期的(如72MHz)。
  4. 检查波特率等参数确保两块板子的波特率、数据位、停止位、校验位完全一致!一个标点符号都不能错。
  5. 检查中断优先级:如果程序中还有其他高优先级中断长时间阻塞,可能导致串口中断无法响应。检查NVIC配置。
  6. 使用逻辑分析仪或示波器:这是终极武器。探头连接到TX引脚,看看是否有波形发出。测量一个位的时间(例如115200波特率,一位约8.68us),看是否与设置相符。

6.2 能收到数据但全是乱码

  1. 波特率不匹配:这是最常见原因。即使两边都设为115200,如果时钟源不准,实际波特率也会有细微差异,积累误差会导致错位。尝试降低波特率(如9600)测试。最好使用外部晶振。
  2. 电平问题:虽然都是3.3V,但如果一方电源不稳,高电平电压不足,也可能导致误判。测量TX引脚在发送时的实际高电平电压。
  3. 数据位/停止位/校验位不匹配:仔细核对CubeMX和对方设备(如串口调试助手)的设置。
  4. 缓冲区溢出:如果发送方发送过快,接收方处理不及时,又没用环形缓冲区或DMA,数据会被覆盖。检查接收代码是否有溢出保护。

6.3 数据丢包或断断续续

  1. 中断被屏蔽时间过长:如果在主循环或高优先级中断中有长时间关中断(__disable_irq())或耗时操作,会导致串口中断无法及时响应,丢失数据。优化代码,减少关中断时间。
  2. 环形缓冲区大小不足:如果数据突发量大,缓冲区太小会被快速填满。增加RX_BUF_SIZE
  3. 发送函数阻塞时间过长HAL_UART_Transmit是阻塞的,如果发送一长串数据,期间无法处理接收。对于主机,可以考虑使用HAL_UART_Transmit_IT(中断发送)或HAL_UART_Transmit_DMA(DMA发送)。
  4. 物理连接不可靠:杜邦线接触不良,在振动或移动时导致断续。尝试焊接或使用更可靠的连接器。

6.4 使用串口调试助手辅助调试

在开发初期,可以先用USB转TTL模块将一块STM32的串口连接到电脑,用串口调试助手(如XCOM、SSCOM)代替另一块板子。这样可以:

  • 验证发送:在代码中让STM32定时发送固定数据包,在电脑端查看接收是否正确。
  • 验证接收:从电脑端发送数据,观察STM32能否正确接收并响应(比如让LED闪烁)。
  • 抓取数据:当两块STM32通信异常时,可以用调试助手作为“监听者”,并联到通信线路上,查看实际传输的数据流,判断问题出在发送方还是接收方。

一个关键的调试技巧:在串口初始化后,先让板子发送一段固定的字符串(如"Hello from Board A!\r\n")。如果能在调试助手上看到这行字,至少证明发送端、硬件连接、波特率设置这一半的路径是通的。这能极大缩小问题范围。

http://www.cnnetsun.cn/news/3723987.html

相关文章:

  • 单片机、嵌入式与PLC:核心区别、应用场景与学习路径全解析
  • 固定资产管理系统技术演进解析:台账架构、标签打印、盘点模式、维保体系与信创迭代史
  • ReactNative与OpenHarmony跨平台开发实战
  • AI芯片内功心法大全:为什么没有一款芯片能通吃所有AI任务?
  • JAVA毕业设计-基于 SpringBoot+Vue 的智能仓储进销存管理系统设计与实现 基于前后端分离的智慧仓储物资监控管理平台(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 华为OD C++面试指南:核心考点与实战策略解析
  • 数字芯片CDC设计实战:从亚稳态原理到SystemVerilog验证
  • 喜马拉雅音频下载器:3步轻松实现VIP专辑本地永久保存
  • 智能抄表在能源管理上的用处
  • 元宇宙课程PPT设计:Python技术栈与教学实践
  • 物联网设备安全芯片SE050与dsPIC30F4011集成方案
  • 如何安全解锁Switch全部功能:Atmosphere大气层系统完整指南
  • 计算机毕业设计之“我爱我家”地方特色农产品商城的设计与实现
  • STM32移植OpenHarmony LiteOS-M内核实战:从环境搭建到驱动开发
  • VS Code Git 工作树:多分支并行开发体验
  • C++代码耗时测量:从原理到实践,四种方法精准性能分析
  • 2026服务好加密软件公司 7项核心维度深度横评
  • 企业培训视频为什么需要加密保护?
  • yuzu模拟器终极实战指南:从零构建高性能Switch游戏体验
  • Playwright无痕模式与无头模式深度解析:从概念到实战配置
  • LaTeX子公式编号与对齐排版实战指南
  • 抖音下载器完全指南:三步保存无水印高清视频的终极方案
  • 企业级AI智能体开发实战:从LangChain到LangGraph完整指南
  • 代码安全智能体|灵脉CodeAI让复杂漏洞有迹可循
  • Python构建基金数据分析系统:爬虫、处理与可视化实战
  • 龙蟠润滑油连续12年蝉联中国十大品牌解析
  • 西门子S7-200 PLC通信指令深度解析:从NETR/NETW到自由口与Modbus实战
  • Dijkstra算法详解:从原理到实现,解决最短路径问题
  • 2026年专科定向士官出路揭秘!他们的未来究竟有哪些机会?
  • 从零打造仿生扑翼飞行器:机械设计、控制算法与工程实践全解析