【物联网学习笔记】串口接收

前言

本文是本人备赛物联网赛项的学习笔记,主要供本人学习、复习,不是经验分享或教学,若有错误,大佬轻喷。


一、学习目标

  1. 理解串口IDLE 空闲中断的工作原理与使用场景
  2. 掌握DMA + 串口 IDLE 中断实现不定长数据收发的核心方案
  3. 从零完成 CubeMX 工程配置→代码编写→功能验证全流程
  4. 实现串口数据回环、LED 控制、按键读取三大基础功能
  5. 生成可复用的app.c/app.h驱动文件,替代网课品牌缩写代码

二、核心理论基础

2.1 为什么用 IDLE 中断 + DMA?

传统串口接收存在两大痛点:

  • 固定长度接收:无法适配上位机发送的任意长度数据
  • 轮询接收:需要 CPU 持续占用资源等待数据,效率极低

IDLE 空闲中断 + DMA是 STM32 最主流的不定长串口接收方案,优势:

  • DMA:串口收到的数据直接由 DMA 搬运到内存,全程无需 CPU 干预,极大节省资源
  • IDLE 中断:检测到串口总线空闲(一帧数据发送完成)时触发中断,精准判断一帧数据的结束,完美适配不定长数据

2.2 核心概念详解

  1. 串口 IDLE 空闲中断
    • 定义:串口总线在接收到第一个数据后,持续 1 个字节的传输时间内没有新数据到来,就会触发 IDLE 空闲中断
    • 关键特性:
      • 无数据时不会触发,必须先收到数据,再进入空闲状态才会触发
      • 标志位不会自动清零,必须在中断服务函数中手动清除
      • 清除标志位后,必须再次收到新数据,才会重新触发下一次中断
  2. DMA 直接存储器访问
    • 定义:无需 CPU 参与,直接实现外设与内存、内存与内存之间的数据高速传输
    • 本场景用法:配置 DMA 为外设→内存模式,串口收到的每一个字节,都会被 DMA 自动搬运到我们定义的接收缓存数组中,CPU 只需要在一帧数据接收完成(IDLE 中断触发)后,读取缓存数组即可

三、开发环境与硬件准备

3.1 软件环境

  • STM32CubeMX(任意版本,通用配置逻辑)
  • MDK-Keil5(ARMCC V5 编译器)
  • 串口调试助手(如 SSCOM、串口助手)

3.2 硬件准备

  • STM32 物联网开发板(本教程以 CT127C/STM32WLE5CC 为例,其他型号通用)
  • USB 转 TTL 模块
  • Type-C 数据线 / 下载器
  • 杜邦线若干

四、CubeMX 保姆级全步骤配置

步骤 1:新建工程,选择 MCU 型号

  1. 打开 STM32CubeMX,点击「ACCESS TO MCU SELECTOR」
  2. 在搜索框输入你的开发板 MCU 型号(如 STM32WLE5CCU6),选中后点击「Start Project」

步骤 2:配置系统核心时钟 RCC

  1. 左侧侧边栏点击「RCC」
  2. High Speed Clock (HSE) 选择「Crystal/Ceramic Resonator」(外部高速晶振)
  3. 其他保持默认,完成基础时钟配置

步骤 3:配置调试接口,防止芯片锁死

  1. 左侧侧边栏点击「SYS」
  2. Debug 选项选择「Serial Wire」(串行线调试模式)
  3. 其他保持默认

步骤 4:配置 USART2 串口基础参数

  1. 左侧侧边栏点击「USART2」
  2. Mode 选择「Asynchronous」(异步串口通信模式)
  3. 下方参数配置(通用串口参数):
    • 波特率(Baud Rate):115200 Bits/s
    • 数据位(Word Length):8 Bits
    • 校验位(Parity):None
    • 停止位(Stop Bits):1
    • 数据流控制(Hardware Flow Control):Disabled
  4. 其他保持默认

步骤 5:配置 USART2 RX 接收 DMA

  1. 在 USART2 配置界面,点击上方「DMA Settings」选项卡
  2. 点击「Add」按钮,添加 DMA 请求,选择「USART2_RX」
  3. 对新增的 DMA 通道进行如下配置:
    • 方向(Direction):Peripheral To Memory(外设→内存,串口外设数据搬到内存数组)
    • 优先级(Priority):Low(可根据需求调整,数值越小优先级越高)
    • 模式(Mode):Normal(正常模式,非循环模式)
    • 数据宽度(Data Width):Byte(字节,与串口数据位匹配)
    • 地址递增(Increment Address):Memory 勾选,Peripheral 不勾选 关键:内存地址自增,才能把每个字节依次存到数组的不同位置,否则数据会被覆盖
  4. 配置完成后保持界面

步骤 6:配置 NVIC 中断优先级

  1. 在 USART2 配置界面,点击上方「NVIC Settings」选项卡
  2. 勾选「USART2 global interrupt」(使能串口全局中断)
  3. 左侧侧边栏点击「NVIC」,找到你配置的 DMA 通道(如 DMA1 Channel1 global interrupt),勾选使能
  4. 配置两个中断的抢占优先级与响应优先级(数值越小优先级越高,保持两个中断优先级一致即可,如抢占优先级 1,响应优先级 0)

步骤 7:配置时钟树

  1. 点击上方「Clock Configuration」选项卡
  2. 配置系统时钟为开发板最高主频(如 STM32WLE5CC 配置为 48MHz)
  3. 确保 USART2 的时钟源配置正确,无红色报错提示

步骤 8:工程管理与代码生成

  1. 点击上方「Project Manager」选项卡
  2. 「Project」栏目:配置工程名称、工程保存路径(路径不能有中文、空格),Toolchain/IDE 选择「MDK-ARM V5」
  3. 「Code Generator」栏目:勾选「Generate peripheral initialization as a pair of '.c/.h' files per peripheral」(每个外设生成独立的.c/.h 文件)
  4. 点击右上角「GENERATE CODE」,等待工程生成完成,点击「Open Project」直接打开 Keil 工程

五、工程代码完整实现(app.c/app.h)

5.1 文件添加说明

  • app.h:头文件,存放宏定义、函数声明,添加到工程的Inc文件夹下
  • app.c:源文件,存放函数实现,添加到工程的Src文件夹下

5.2 app.h 完整代码

cpp 复制代码
#ifndef __APP_H
#define __APP_H

#include "string.h"
#include "stdio.h"
#include "gpio.h"

// 状态宏定义
#define APP_ON			1   // 开/有效状态
#define APP_OFF		    2   // 关/无效状态
#define APP_TOGGLE		3   // 翻转状态

// 函数声明
// LED控制相关
void app_write_AL(unsigned short int ALx, unsigned char state);
unsigned char app_read_AL(unsigned short int ALx);
// 按键读取相关
unsigned char app_read_ASW(GPIO_TypeDef *ASWx);
// 串口收发相关
void app_tx_UART(const unsigned char *Data);
void app_UART_IDLE_rx(void);
void app_UART_rx_loop(void);
void app_UART_rx_deal(void);
// 外设初始化
void app_init(void);

#endif

5.3 app.c 完整代码

cpp 复制代码
#include "app.h"

// 串口接收相关全局变量定义
#define APP_UART_RX_BUFFER_LEN  100   // 串口接收缓存区最大长度,可按需修改
unsigned char g_u8app_rx_len;         // 一帧串口数据的实际接收长度
unsigned char g_u8app_rx_end_flag;    // 一帧数据接收完成标志位
unsigned char g_u8app_rx_buffer[APP_UART_RX_BUFFER_LEN];  // 串口接收数据缓存数组

/*************************************************
* 函数名:app_write_AL
* 作用:控制LED灯的亮灭/翻转
* 形参:ALx - LED对应的GPIO引脚(如GPIO_PIN_0)
*       state - LED状态:APP_ON亮/APP_OFF灭/APP_TOGGLE翻转
* 返回值:无
*************************************************/
void app_write_AL(unsigned short int ALx, unsigned char state)
{
	switch (state)
	{
		case APP_ON:
			HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_RESET);
			break;
		case APP_OFF:
			HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_SET);
			break;
		case APP_TOGGLE:
			HAL_GPIO_TogglePin(GPIOB, ALx);
			break;
		default:
			break;
	}
}

/*************************************************
* 函数名:app_read_AL
* 作用:读取LED灯当前的状态
* 形参:ALx - LED对应的GPIO引脚
* 返回值:LED当前状态 APP_ON/APP_OFF
*************************************************/
unsigned char app_read_AL(unsigned short int ALx)
{
	unsigned char state = HAL_GPIO_ReadPin(GPIOB, ALx);
	
	if(state == GPIO_PIN_SET)
		return APP_OFF;
	else
		return APP_ON;
}

/*************************************************
* 函数名:app_read_ASW
* 作用:读取按键状态,带消抖处理
* 形参:ASWx - 按键对应的GPIO端口(如GPIOA)
* 返回值:按键状态 APP_ON-按下/APP_OFF-松开
* 备注:默认按键引脚为GPIO_PIN_8,可根据硬件修改
*************************************************/
unsigned char app_read_ASW(GPIO_TypeDef *ASWx)
{
	unsigned char state = APP_OFF;
		
	if(HAL_GPIO_ReadPin(ASWx, GPIO_PIN_8) == GPIO_PIN_RESET)
	{
		HAL_Delay(20);  // 延时消抖
		
		if(HAL_GPIO_ReadPin(ASWx, GPIO_PIN_8) == GPIO_PIN_RESET)
		{
			state = APP_ON;
			while(HAL_GPIO_ReadPin(ASWx, GPIO_PIN_8) == GPIO_PIN_RESET);  // 等待按键松开
		}
	}
	
	return state;
}

/*************************************************
* 函数名:app_tx_UART
* 作用:串口发送字符串数据
* 形参:Data - 要发送的字符串首地址
* 返回值:无
*************************************************/
void app_tx_UART(const unsigned char *Data)
{
	extern UART_HandleTypeDef huart2;  // 引入CubeMX生成的串口句柄
	
	HAL_UART_Transmit(&huart2, Data, strlen((const char *)Data), 0xFFFF);
}

/*************************************************
* 函数名:fputc
* 作用:重定向C库printf函数到串口,支持printf串口打印
* 形参:系统标准库函数固定参数
* 返回值:发送的字符
*************************************************/
int fputc(int ch, FILE *f)
{
	extern UART_HandleTypeDef huart2;
	
	HAL_UART_Transmit(&huart2, (unsigned char*)&ch, 1, 0xFFFF);
	return ch;
}

/*************************************************
* 函数名:app_UART_IDLE_rx
* 作用:串口IDLE空闲中断处理函数,判断一帧数据接收完成
* 形参:无
* 返回值:无
* 备注:必须在USART2_IRQHandler中断服务函数中调用
*************************************************/
void app_UART_IDLE_rx(void)
{  
	extern UART_HandleTypeDef huart2;
	extern DMA_HandleTypeDef hdma_usart2_rx;  // 引入CubeMX生成的DMA句柄
	
	if(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) == SET)  // 检测到空闲标志位
	{
		__HAL_UART_CLEAR_IDLEFLAG(&huart2);  // 手动清除空闲标志位
		HAL_UART_DMAStop(&huart2);            // 停止本次DMA传输
		// 计算实际接收的数据长度:总缓存长度 - DMA剩余未传输计数
		g_u8app_rx_len = APP_UART_RX_BUFFER_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
		g_u8app_rx_end_flag = 1;  // 置位接收完成标志位
	}
}

/*************************************************
* 函数名:app_UART_rx_loop
* 作用:串口接收主循环处理函数,需在main函数while(1)中调用
* 形参:无
* 返回值:无
*************************************************/
void app_UART_rx_loop(void)     
{
	extern UART_HandleTypeDef huart2;
	
	if(g_u8app_rx_end_flag)  // 判断是否接收到一帧完整数据
	{
		// 串口数据处理函数,可在此自定义数据解析逻辑
		app_UART_rx_deal();
		
		// 接收完成后清空标志位与长度,重新开启DMA接收
		g_u8app_rx_len = 0;
		g_u8app_rx_end_flag = 0;
		HAL_UART_Receive_DMA(&huart2, g_u8app_rx_buffer, APP_UART_RX_BUFFER_LEN);
	}
}

/*************************************************
* 函数名:app_UART_rx_deal
* 作用:串口接收数据处理函数,可自定义解析逻辑
* 形参:无
* 返回值:无
* 备注:默认实现串口回环(收到什么发回什么)
*************************************************/
void app_UART_rx_deal(void)
{
	extern UART_HandleTypeDef huart2;
	
	// 回环功能:将收到的数据原样发送回上位机
	HAL_UART_Transmit(&huart2, g_u8app_rx_buffer, g_u8app_rx_len, 0xFFFF); 
}

/*************************************************
* 函数名:app_init
* 作用:外设功能初始化,需在main函数中while(1)之前调用
* 形参:无
* 返回值:无
*************************************************/
void app_init(void)
{
	extern UART_HandleTypeDef huart2;
	
	__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);  // 使能串口IDLE空闲中断
	HAL_UART_Receive_DMA(&huart2, g_u8app_rx_buffer, APP_UART_RX_BUFFER_LEN);  // 开启DMA接收
}

5.4 串口中断服务函数修改

  1. 在 Keil 工程中,打开stm32wle5xx_it.c文件(文件名根据你的 MCU 型号变化)
  2. 找到USART2_IRQHandler函数,在其中添加app_UART_IDLE_rx()函数调用,必须放在 HAL 库自带的中断处理函数之前,修改后代码如下:
cpp 复制代码
void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */
  app_UART_IDLE_rx();  // 新增:调用我们写的空闲中断处理函数
  /* USER CODE END USART2_IRQn 0 */
  HAL_UART_IRQHandler(&huart2);
  /* USER CODE BEGIN USART2_IRQn 1 */

  /* USER CODE END USART2_IRQn 1 */
}

关键提醒:必须把app_UART_IDLE_rx()放在HAL_UART_IRQHandler之前,否则 HAL 库会提前清除 IDLE 标志位,导致我们的函数无法检测到中断配图:USART2_IRQHandler 函数修改完成后的代码截图


六、主函数调用与功能集成

  1. 在 Keil 工程中打开main.c文件
  2. 在文件顶部的 USER CODE BEGIN Includes 区域,包含我们的头文件:
cpp 复制代码
/* USER CODE BEGIN Includes */
#include "app.h"
/* USER CODE END Includes */
  1. 在 main 函数中,MX_GPIO_Init()MX_USART2_UART_Init()MX_DMA_Init()等初始化函数之后,while (1) 之前,调用我们的初始化函数:
cpp 复制代码
/* USER CODE BEGIN 2 */
app_init();  // 初始化串口IDLE+DMA、使能中断
/* USER CODE END 2 */
  1. 在 while (1) 主循环中,调用串口接收循环处理函数:
cpp 复制代码
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  app_UART_rx_loop();  // 串口接收数据循环处理
}

七、核心函数逐行详解

7.1 空闲中断处理函数 app_UART_IDLE_rx

  • 核心作用:在串口中断中检测空闲标志位,计算接收数据长度,标记接收完成
  • 关键步骤:
    1. 读取 IDLE 标志位,判断是否触发空闲中断
    2. 手动清除 IDLE 标志位,避免重复触发中断
    3. 停止 DMA 传输,防止数据继续写入缓存
    4. 计算实际接收长度:总缓存长度 - DMA 剩余未传输的计数值
    5. 置位接收完成标志位,通知主循环处理数据

7.2 主循环处理函数 app_UART_rx_loop

  • 核心作用:在主循环中轮询接收完成标志位,处理数据并重新开启 DMA 接收
  • 关键逻辑:检测到一帧数据接收完成后,调用数据处理函数,清空标志位,重新开启 DMA,等待下一次数据接收,实现持续的不定长数据接收

7.3 数据处理函数 app_UART_rx_deal

  • 核心作用:自定义串口数据的解析逻辑
  • 默认实现:串口回环,将收到的数据原样发回上位机
  • 可扩展:可在此添加指令解析,比如收到 "ON" 点亮 LED,收到 "OFF" 熄灭 LED 等自定义逻辑

八、实验验证与现象说明

8.1 基础串口回环实验

  1. 编译工程,无报错后下载程序到开发板
  2. 用 USB 转 TTL 模块连接开发板的 USART2 TX/RX 引脚,连接电脑
  3. 打开串口调试助手,配置参数:波特率 115200、8 位数据位、1 停止位、无校验
  4. 在串口调试助手发送框输入任意字符 / 字符串,点击发送
  5. 实验现象:串口调试助手接收区会收到你发送的完全一致的数据,实现不定长数据回环

8.2 附加功能测试

  1. LED 控制:在 main 函数中调用app_write_AL(GPIO_PIN_5, APP_ON);,可点亮对应 LED
  2. 按键读取:在主循环中调用app_read_ASW(GPIOA);,按下按键会返回 APP_ON,可配合 LED 实现按键控制灯效
  3. printf 打印:在代码中直接使用printf("Hello STM32\r\n");,串口调试助手会收到对应的打印内容

九、常见问题排查指南

  1. 串口发送数据后,没有回传数据,中断不触发

    • 排查 1:检查app_UART_IDLE_rx()是否放在USART2_IRQHandlerHAL_UART_IRQHandler之前
    • 排查 2:检查 CubeMX 中是否使能了 USART2 全局中断和 DMA 通道中断
    • 排查 3:检查app_init()是否在 main 函数中正确调用,是否开启了 IDLE 中断和 DMA 接收
  2. 串口收到的数据乱码

    • 排查 1:检查串口调试助手的波特率、数据位、停止位、校验位,与 CubeMX 配置完全一致
    • 排查 2:检查开发板的系统时钟配置是否正确,时钟树无报错
    • 排查 3:检查 USB 转 TTL 模块的接线是否牢固,TX/RX 是否交叉连接
  3. 只能收到第一帧数据,后续数据无法接收

    • 排查 1:检查app_UART_rx_loop()中,是否在处理完数据后重新调用了HAL_UART_Receive_DMA开启下一次接收
    • 排查 2:检查是否正确清空了g_u8app_rx_end_flagg_u8app_rx_len
  4. printf 函数无法打印,编译报错

    • 排查 1:在 Keil 工程中,点击魔术棒→Target,勾选「Use MicroLIB」
    • 排查 2:检查fputc函数是否正确实现,串口句柄是否正确
  5. 收到的数据长度不对,或只有最后一个字节

    • 排查 1:检查 DMA 配置中,是否勾选了 Memory 的地址自增 Increment Address
    • 排查 2:检查接收缓存数组的长度是否足够,发送的数据长度是否超过了APP_UART_RX_BUFFER_LEN
相关推荐
studyForMokey2 小时前
【跨端技术ReactNative】JavaScript学习
android·javascript·学习·react native·react.js
左左右右左右摇晃2 小时前
TCP三次握手与四次挥手
笔记
Be for thing2 小时前
Android 充电 & BMS 电池管理系统原理与测试实战(手机 / 手表通用)
android·学习·智能手机
是孑然呀2 小时前
【笔记】openclaw+飞书多agent(非群聊方式)
笔记·飞书
财迅通Ai2 小时前
海立股份子公司参展AWE2026 以创新科技赋能行业转型升级
大数据·人工智能·物联网
renhongxia12 小时前
人工智能代理能生成微服务吗?我们离多远了?
人工智能·深度学习·学习·微服务·云原生·架构·机器人
鸽子一号2 小时前
c#之常用的字符串操作
笔记
Kiyra2 小时前
[特殊字符] LeetCode 做题笔记(二):678. 有效的括号字符串
笔记·算法·leetcode
陈皮糖..2 小时前
Docker Compose 学习之多容器应用编排与运维实践 —— 基于 Nginx+MySQL+Redis 服务栈的部署与管理
运维·redis·学习·mysql·nginx·docker