STM32F103 学习笔记-21-串口通信(第6节)-串口发送命令控制RGB灯

一、串口中断到底是什么?(What)

你可以把 CPU 想象成一个正在专心做饭的厨师(执行主循环代码),串口外设就是小区的快递员。当有串口数据(快递)到达时,快递员不会一直敲门等厨师忙完(查询式),而是打一个电话(触发中断)------厨师暂停炒菜,去门口取快递(执行中断服务函数),取完后回到厨房继续炒菜(返回主循环)。

**串口接收中断(RXNE)**​ 是最常用的串口中断:当串口收到 1 字节数据并存入"接收数据寄存器"时,硬件自动触发中断,提醒 CPU"有数据要处理",这是比"厨师每隔 10 秒去门口看一眼(查询式)"更高效的方式。

核心概念通俗解

  • 中断源:触发中断的"事件"(这里是 USART1 收到数据)

  • 中断向量表 :CPU 的"电话簿",记录"哪个中断对应哪个处理函数"(比如 USART1 中断对应 USART1_IRQHandler),位于 STM32F103 存储器映射的 0x08000000 起始处

  • NVIC:嵌套向量中断控制器,相当于"总机",管理所有中断的优先级、是否允许响铃(使能),Cortex-M3 内核内置,支持 60 个可屏蔽中断通道和 16 个优先级

二、串口中断接收的核心硬件逻辑(Why)

串口接收数据的硬件流程就像快递入柜的过程,用 ASCII 流程图表示如下:

复制代码
串口外设接收串行数据 → 移位寄存器(USART_DR的高8位)拼装成1字节 → 存入接收数据寄存器(USART_DR低8位)
→ 硬件置位RXNE标志(USART_SR位5) → NVIC检测到使能的中断 → CPU保存现场 → 跳转到中断服务函数
→ 读取USART_DR(取快递,自动清RXNE灯) → 处理数据 → CPU恢复现场 → 返回主循环

核心寄存器完整说明(基于 STM32F103xCDE 数据手册)

寄存器组 寄存器名 地址偏移 作用(通俗类比) 关键位/操作
USART 通用 USART_SR 0x00 串口状态寄存器(快递柜状态提示灯) • RXNE(位 5): 1=有新数据,0=无数据 • TXE(位 7): 1=发送寄存器空 • TC(位 6): 1=发送完成 • ORE(位 3): 1=溢出错误 • FE(位 1): 1=帧错误
USART_DR 0x04 串口数据寄存器(快递柜储物格) • 低 8 位: 存 1 字节收发数据 • 读操作: 取接收数据,自动清 RXNE • 写操作: 存发送数据,自动清 TXE
USART_BRR 0x08 波特率寄存器(快递速度调节器) • DIV_Mantissa[15:4]: 波特率分频器整数部分 • DIV_Fraction[3:0]: 波特率分频器小数部分
USART_CR1 0x0C 控制寄存器 1(总开关) • UE(位 13): 1=使能 USART • RE(位 2): 1=使能接收 • TE(位 3): 1=使能发送 • RXNEIE(位 5): 1=使能 RXNE 中断 • M(位 12): 0=8 位数据,1=9 位数据 • PCE(位 10): 1=使能奇偶校验
USART_CR2 0x10 控制寄存器 2(停止位等) • STOP[13:12]: 00=1 位停止位
USART_CR3 0x14 控制寄存器 3(流控等) • CTSE(位 9): 1=使能 CTS 流控 • RTSE(位 8): 1=使能 RTS 流控
RCC 时钟 RCC_APB2ENR 0x18 APB2 外设时钟使能寄存器 • USART1EN(位 14): 1=使能 USART1 时钟 • IOPAEN(位 2): 1=使能 GPIOA 时钟
GPIO GPIOA_CRH 0x04 GPIOA 高 8 位配置寄存器 • MODE9[1:0]: PA9 模式 • CNF9[1:0]: PA9 配置 • MODE10[1:0]: PA10 模式 • CNF10[1:0]: PA10 配置
NVIC NVIC_ISER0 0xE000E100 中断使能寄存器 0 • USART1_IRQn(位 37): 1=使能 USART1 中断
NVIC_IPR9 0xE000E424 中断优先级寄存器 9 • USART1_PRIO[7:4]: 抢占优先级 • USART1_PRIO[3:0]: 子优先级

三、从 0 到 1 配置串口中断(How)

配置分 5 步,以下是提取后的最小可用代码,每行都标注"功能+硬件逻辑+寄存器操作+C 语言知识点"。

步骤 0:为什么选择在 stm32f10x_it.c文件中添加串口中断服务函数

stm32f10x_it.c是 STM32 标准库模板中**专门用于集中存放所有中断服务例程(ISR)**的核心文件,包含两部分核心内容:

  1. Cortex-M3 内核异常处理函数(如 HardFault、SysTick、PendSV 等);

  2. 外设中断服务函数的实现/模板(如串口、定时器、GPIO 等外设的中断处理)。

STM32 的中断服务函数命名必须严格匹配启动文件(如 startup_stm32f10x_xx.s)中中断向量表定义的函数名(例如 DEBUG_USART_IRQHandler),而 stm32f10x_it.c是 ST 官方推荐的中断服务函数实现位置,既符合 STM32 工程的标准化开发规范,也便于集中管理所有中断逻辑,因此串口中断服务函数需要在此文件中实现。

复制代码
// 串口中断服务函数(函数名需匹配启动文件中断向量表,不可随意修改)
void DEBUG_USART_IRQHandler(void)
{
  uint8_t ucTemp;  // 定义临时变量,用于暂存串口接收到的1字节数据
  
  // 检查DEBUG_USARTx的"接收数据寄存器非空(RXNE)"中断标志是否置位
  // 作用:判断是否有新的串口数据被接收,避免无中断时误处理
  // USART_GetITStatus:返回指定中断标志的状态(RESET/SET)
  if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
  {		
    ucTemp = USART_ReceiveData(DEBUG_USARTx);  // 读取接收寄存器中的数据到临时变量
    USART_SendData(DEBUG_USARTx, ucTemp);      // 将接收到的数据回发(实现串口echo回显功能)
  }	 
}

补充说明

  1. 中断标志判断USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE)是核心判断,确保仅在"有数据接收"时执行后续操作,避免无效的寄存器读写;

  2. 数据回显 :代码中 USART_SendData是"回显"示例逻辑,实际项目中可替换为数据解析、缓存、协议处理等业务逻辑;

  3. 函数名约束DEBUG_USART_IRQHandler的命名由串口外设(如 USART1 对应 USART1_IRQHandler)决定,需与启动文件中断向量表完全一致,否则中断无法触发。

步骤 1:宏定义与头文件(bsp_usart.h)

复制代码
#ifndef __BSP_USART_H
#define __BSP_USART_H

#include "stm32f10x.h"
#include <stdio.h>

// 宏定义:改这里就能切换串口,无需改核心逻辑(C语言宏的复用价值)
// STM32F103: USART1在APB2(72MHz), USART2-5在APB1(36MHz)
#define DEBUG_USART1     1
#if DEBUG_USART1
#define  DEBUG_USARTx                   USART1          // 目标串口
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1  // 串口时钟
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd // 时钟使能函数
#define  DEBUG_USART_BAUDRATE           115200          // 波特率

// GPIO配置:USART1_TX=PA9,RX=PA10(数据手册表5)
#define  DEBUG_USART_GPIO_CLK           RCC_APB2Periph_GPIOA
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10

// 中断配置(数据手册表58)
#define  DEBUG_USART_IRQ                USART1_IRQn     // 中断通道号(37)
#define  DEBUG_USART_IRQHandler         USART1_IRQHandler // 中断函数名(必须和向量表一致)
#endif

// 函数声明
void USART_Config(void);
#endif

步骤 2:配置 NVIC(中断优先级)

复制代码
// bsp_usart.c
static void NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;  // C语言:定义结构体,存NVIC配置参数
    
    // 配置优先级分组:2位抢占优先级+2位子优先级(整个工程只能设1次)
    // 抢占优先级:高优先级中断能打断低优先级(比如快递电话能打断外卖电话)
    // 子优先级:同抢占优先级时,先处理子优先级高的
    // 寄存器操作:SCB->AIRCR = 0x05FA0000 | (0x2 << 8)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    // 选择要配置的中断通道(USART1)
    NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
    // 设置抢占优先级为1(0-3,数字越小越紧急)
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    // 设置子优先级为1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    // 使能该中断通道(允许总机转接这个电话)
    // 寄存器操作:NVIC->ISER[1] |= 1 << (DEBUG_USART_IRQ - 32)
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    // 将配置写入硬件寄存器(C语言:传结构体地址,函数修改硬件)
    NVIC_Init(&NVIC_InitStructure);
}

步骤 3:初始化 GPIO 与 USART

复制代码
// bsp_usart.c
void USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    // 1. 使能GPIO时钟(给PA9/PA10供电)
    // 寄存器操作:RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
    DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
    // 2. 使能USART1时钟(给串口外设供电)
    // 寄存器操作:RCC->APB2ENR |= RCC_APB2ENR_USART1EN
    DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);

    // 3. 配置TX引脚(PA9):复用推挽输出
    // 复用推挽:引脚既可以当普通IO,也能作为串口TX(专门对外发数据的喇叭)
    // 寄存器操作:GPIOA->CRH &= ~(0xF << 4); GPIOA->CRH |= (0xB << 4)
    // 即MODE9=11(50MHz), CNF9=10(复用推挽)
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;    // 复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 引脚响应速度
    GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

    // 4. 配置RX引脚(PA10):浮空输入
    // 浮空输入:不接高/低电平,只接收外部串口信号(专门听数据的麦克风)
    // 寄存器操作:GPIOA->CRH &= ~(0xF << 8); GPIOA->CRH |= (0x4 << 8)
    // 即MODE10=00(输入), CNF10=01(浮空输入)
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
    GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);

    // 5. 配置USART参数
    // 波特率计算:USARTDIV = 72000000 / (16 * 115200) = 39.0625
    // 整数部分39,小数部分0.0625 * 16=1 → USART_BRR=0x271
    USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 波特率115200
    USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;     // 1位停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;        // 无校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    // 寄存器操作:写入USART_BRR、USART_CR1、USART_CR2、USART_CR3
    USART_Init(DEBUG_USARTx, &USART_InitStructure);

    // 6. 配置中断优先级
    NVIC_Configuration();

    // 7. 使能RXNE中断(允许快递员打电话)
    // 寄存器操作:USART1->CR1 |= USART_CR1_RXNEIE
    USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);

    // 8. 使能USART外设(打开串口)
    // 寄存器操作:USART1->CR1 |= USART_CR1_UE
    USART_Cmd(DEBUG_USARTx, ENABLE);
}

步骤 4:编写中断服务函数

复制代码
// stm32f10x_it.c
#include "bsp_usart.h"

// 中断服务函数:函数名必须和中断向量表一致(不能随便改)
// 向量表定义在startup_stm32f10x_hd.s中
void DEBUG_USART_IRQHandler(void)
{
    uint8_t ucTemp; // C语言:uint8_t=无符号8位整数,存1字节数据
    
    // 好写法:先检查中断标志,防止处理非RXNE中断(比如串口错误中断)
    // 库函数实现:return ((USARTx->SR & USART_SR_RXNE) && (USARTx->CR1 & USART_CR1_RXNEIE))
    if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
    {       
        // 读取接收数据(自动清除RXNE标志,无需手动清)
        // 寄存器操作:ucTemp = (uint8_t)(USART1->DR & 0xFF)
        ucTemp = USART_ReceiveData(DEBUG_USARTx);
        
        // 回显数据:把收到的字节发回去(测试用)
        // 寄存器操作:USART1->DR = ucTemp
        USART_SendData(DEBUG_USARTx, ucTemp);    
        
        // 坏写法:缺少发送完成等待,高波特率下可能丢数据
        // 好写法补充:等待TXE标志置1(发送寄存器空)
        // 寄存器操作:while(!(USART1->SR & USART_SR_TXE))
        while(USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
    }   
}

四、实战:串口命令控制 RGB 灯

方式 1:查询式接收(简单但低效)

复制代码
// main.c
#include "stm32f10x.h"
#include "bsp_usart.h"
#include "bsp_led.h"

int main(void)
{
    uint8_t ch; // 存接收的字符
    
    // 初始化串口(含中断配置,但这里先关闭中断,用查询式)
    USART_Config();
    // 寄存器操作:USART1->CR1 &= ~USART_CR1_RXNEIE
    USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, DISABLE); // 关闭中断
    // 初始化RGB灯GPIO
    LED_GPIO_Config();
    
    // printf能串口输出:因为重写了fputc(底层调用USART_SendData)
    printf("串口控制RGB灯\r\n");
    printf("发送'1'亮红灯 | '2'亮绿灯 | '3'亮蓝灯 | 其他灭灯\r\n");
    
    while(1) // 主循环(厨师一直炒菜)
    {
        // 查询:有没有新数据(每隔10秒看一眼门口)
        // 寄存器操作:if(USART1->SR & USART_SR_RXNE)
        if(USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == SET)
        {
            ch = USART_ReceiveData(DEBUG_USARTx); // 读取数据
            printf("收到:%c(0x%02X)\r\n", ch, ch);
            
            // 命令解析:注意是字符'1'(ASCII=0x31),不是数字1
            switch(ch)
            {
                case '1': LED_RED;    break; // 亮红灯
                case '2': LED_GREEN;  break; // 亮绿灯
                case '3': LED_BLUE;   break; // 亮蓝灯
                default:  LED_RGBOFF; break; // 灭所有灯
            }
        }
    }
}

方式 2:中断式接收(高效)

查询式会让 CPU 一直"查有没有数据",中断式只在有数据时才处理,核心是用全局变量共享数据:

复制代码
// bsp_usart.h 新增全局变量声明
// volatile关键字:告诉编译器该变量可能被中断修改,不要优化
extern volatile uint8_t g_rx_data;   // 存接收的字节
extern volatile uint8_t g_rx_flag;   // 接收完成标志(1=有新数据)

// bsp_usart.c 新增全局变量定义
volatile uint8_t g_rx_data = 0;
volatile uint8_t g_rx_flag = 0;

// 修改中断服务函数
void DEBUG_USART_IRQHandler(void)
{
    if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
    {       
        g_rx_data = USART_ReceiveData(DEBUG_USARTx);
        g_rx_flag = 1; // 标记有新数据
    }   
}

// main.c 修改
int main(void)
{
    USART_Config(); // 开启中断
    LED_GPIO_Config();
    printf("中断式串口控制RGB灯\r\n");
    printf("发送'1'或0x01亮红灯 | '2'或0x02亮绿灯 | '3'或0x03亮蓝灯\r\n");
    
    while(1)
    {
        if(g_rx_flag == 1) // 检测到新数据
        {
            g_rx_flag = 0; // 清除标志
            
            // 支持ASCII和16进制命令
            if(g_rx_data == '1' || g_rx_data == 0x01) LED_RED;
            else if(g_rx_data == '2' || g_rx_data == 0x02) LED_GREEN;
            else if(g_rx_data == '3' || g_rx_data == 0x03) LED_BLUE;
            else LED_RGBOFF;
            
            printf("执行命令:0x%02X\r\n", g_rx_data);
        }
        // 主循环可执行其他任务(比如灯闪烁),不被串口接收阻塞
    }
}

五、新手必避的 7 个坑(注意事项)

  1. 中断标志未检查:中断服务函数直接读数据,可能处理非 RXNE 中断(如错误中断),导致数据错误。

  2. 混淆 ASCII 与 16 进制:串口助手发"1"是 ASCII=0x31,不是数字 1;勾选"16 进制发送"时,发"01"才是 0x01。

  3. 发送未等完成 :连续发送数据时,未等待 TXE标志置 1 就发下一个,会覆盖未发送的数据。

  4. 优先级分组重复设置 :整个工程只能调用 1 次 NVIC_PriorityGroupConfig,多次设置会导致所有中断优先级混乱。

  5. 中断与查询混用:既开中断又在主循环查询,数据会被中断先读走,主循环拿不到数据。

  6. 缺少 volatile 关键字 :全局变量未加 volatile,编译器可能优化掉变量读取,导致主循环看不到中断修改的值。

  7. 错误中断未处理:未使能错误中断,发生溢出、帧错误时无法及时发现,导致后续数据全部错误。

六、扩展练习

  1. 实现多字节命令解析:比如接收"RED ON"亮红灯、"RED OFF"灭红灯。

  2. 用环形缓冲区(数组)实现批量数据接收,解决连续发送多字节时的数据丢失问题。

  3. 给串口中断加错误处理:使能 ORE、FE、PE 错误中断,在中断服务函数中清除错误标志并打印错误信息。

  4. 实现 printf 重定向:重写 fputc函数,支持 printf串口输出(已隐含在代码中,可自行实现)。

参考资料

  • 《零死角玩转 STM32F103-指南者》第 26 章及 USART 相关章节

  • STM32F103xCDE 官方数据手册(USART 章节,第 5.3.16 节)

相关推荐
玄米乌龙茶1238 小时前
LLM成长笔记(十二):质量评估与可观测性
大数据·人工智能·笔记
快乐得小萝卜9 小时前
笔记:理解 KL 散度与 INT8 量化校准
笔记
hnult9 小时前
考试云:九重防作弊体系与六大AI能力,打造安全智能在线笔试系统云平台
人工智能·笔记·安全
炽烈小老头9 小时前
【每天学习一点算法 2026/05/25】矩阵中的最长递增路径
学习·算法·矩阵
yongui478349 小时前
水表集中抄表器单片机实现方案
单片机·嵌入式硬件
handler0110 小时前
【MySQL】教你库与表的增删查改操作(基础)
运维·数据库·笔记·sql·mysql·数据·分析
wuxinyan12310 小时前
工业级大模型学习之路021:LangChain零基础入门教程(第四篇):文档加载与文本分块技术
人工智能·python·学习·langchain
Qres82110 小时前
Git基础命令学习笔记
笔记·git·学习
奔跑的Ma~10 小时前
Azure OpenAI Codex 详细配置与使用教程(国内用户专属)
学习·microsoft·flask·ai编程·azure