STM32 进阶封神之路(十二):串口实战全攻略 —— 发送 / 接收 / 中断 /printf 重定向(库函数 + 寄存器)

STM32 进阶封神之路(十二):串口实战全攻略 ------ 发送 / 接收 / 中断 /printf 重定向(库函数 + 寄存器)

上一篇我们吃透了串口通信的底层原理,这一篇就进入核心实战环节!基于 STM32F103 的 USART 外设,从串口初始化配置、单字节 / 字符串发送、查询式接收,到中断非阻塞接收、printf 重定向,全程拆解每一步操作,让你真正实现 "STM32 与电脑串口双向通信",彻底掌握串口的全场景应用!

本文基于实战资料,所有代码均以 "USART1+115200bps+8N1" 为标准配置,库函数与寄存器双版本覆盖,新手可直接照搬,进阶者可深挖底层逻辑!

一、STM32 串口核心资源与硬件连接

1. STM32F103 串口资源分布

STM32F103 系列搭载 3 个 USART(通用同步异步收发器)和 2 个 UART(仅异步),核心资源如下:

表格

串口型号 挂载总线 TX 引脚 RX 引脚 核心特性 典型应用
USART1 APB2(72MHz) PA9(复用推挽) PA10(浮空输入) 支持同步 / 异步、中断、DMA 与电脑通信、高速数据传输
USART2 APB1(36MHz) PA2 / PD5 PA3 / PD6 支持中断、DMA 与蓝牙 / WiFi 模块通信
USART3 APB1(36MHz) PB10 / PC10 PB11 / PC11 支持中断、DMA 与 485 模块通信

实战选型:优先选择 USART1(APB2 总线,时钟频率高,传输速度快),本文以 USART1 为例。

2. 硬件连接(STM32 与电脑通信)

  • 核心链路:STM32 USART1 → USB-TTL 模块(CH340G) → 电脑 USB 口;
  • 具体接线:
    • STM32 PA9(USART1_TX) → USB-TTL 模块 RX;
    • STM32 PA10(USART1_RX) → USB-TTL 模块 TX;
    • STM32 GND → USB-TTL 模块 GND;
    • USB-TTL 模块 USB 口 → 电脑 USB 口;
  • 关键注意:TX 与 RX 必须交叉连接(STM32 的 TX 接模块的 RX,反之亦然),共地是通信稳定的前提。

二、串口初始化配置(核心步骤,必掌握)

串口使用前必须完成 "时钟使能→GPIO 配置→串口参数配置→中断配置(可选)",以下是库函数与寄存器双版本实现。

1. 库函数版初始化(USART1,115200bps 8N1)

c

运行

复制代码
#include "stm32f10x.h"

// USART1初始化:115200bps,8位数据位,无校验,1位停止位
void USART1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;

    // 1. 使能USART1和GPIOA时钟(USART1挂载APB2,GPIOA挂载APB2)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置GPIO引脚(PA9=TX,PA10=RX)
    // 配置PA9为复用推挽输出(USART1_TX)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出(关键!)
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 配置PA10为浮空输入(USART1_RX)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入(推荐)
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置USART1参数(8N1)
    USART_InitStruct.USART_BaudRate = 115200; // 波特率115200
    USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 8位数据位
    USART_InitStruct.USART_StopBits = USART_StopBits_1; // 1位停止位
    USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 收发模式
    USART_Init(USART1, &USART_InitStruct);

    // 4. 使能USART1
    USART_Cmd(USART1, ENABLE);
}

2. 寄存器版初始化(底层逻辑拆解)

c

运行

复制代码
#include "stm32f10x.h"

void USART1_Init(void) {
    // 1. 使能USART1和GPIOA时钟(APB2ENR寄存器)
    RCC->APB2ENR |= (1<<14) | (1<<2); // bit14=USART1,bit2=GPIOA

    // 2. 配置GPIOA9(TX)为复用推挽输出
    GPIOA->CRH &= ~(0x0F<<4); // 清除PA9配置(CRH对应PA8~PA15,PA9对应bit5~bit4)
    GPIOA->CRH |= (0x0B<<4);  // MODE9=11(50MHz),CNF9=10(复用推挽)

    // 3. 配置GPIOA10(RX)为浮空输入
    GPIOA->CRH &= ~(0x0F<<8); // 清除PA10配置(PA10对应bit9~bit8)
    GPIOA->CRH |= (0x04<<8);  // MODE10=00(输入),CNF10=01(浮空输入)

    // 4. 配置USART1波特率(115200bps,APB2时钟72MHz)
    // 波特率计算公式:USARTDIV = 时钟频率 / (16×波特率) = 72000000/(16×115200)≈39.0625
    USART1->BRR = 0x271; // 39.0625 → 整数部分39=0x27,小数部分0.0625×16=1 → 0x271

    // 5. 配置USART1参数(8N1)
    USART1->CR1 &= ~(1<<12); // M=0(8位数据位)
    USART1->CR1 &= ~(1<<10); // PCE=0(无校验)
    USART1->CR2 &= ~(3<<12); // STOP=00(1位停止位)

    // 6. 使能USART1收发功能
    USART1->CR1 |= (1<<2) | (1<<3); // RE=1(接收使能),TE=1(发送使能)

    // 7. 使能USART1
    USART1->CR1 |= (1<<13); // UE=1(USART使能)
}

3. 初始化核心要点(避坑关键)

  • GPIO 模式:TX 必须配置为GPIO_Mode_AF_PP(复用推挽输出),不能用普通输出模式(否则无串口信号输出);
  • 波特率计算:USART1 挂载 APB2(72MHz),波特率 = 72MHz/(16×USARTDIV),115200bps 对应的 USARTDIV=39.0625,BRR=0x271
  • 时钟使能:USART1 属于 APB2 总线外设,需调用RCC_APB2PeriphClockCmd,而非 APB1;
  • 引脚复用:PA9/PA10 默认复用为 USART1_TX/RX,无需额外配置复用寄存器(库函数自动处理)。

三、串口发送实战:单字节 + 字符串 + printf 重定向

串口发送是最常用的功能(如调试打印、数据上传),核心是 "等待发送数据寄存器为空→写入数据",以下是三种发送场景的实现。

1. 单字节发送(库函数 + 寄存器)

(1)库函数版:USART_SendData

c

运行

复制代码
// 发送单个字节
void USART1_SendByte(uint8_t data) {
    // 等待发送数据寄存器(TDR)为空
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    // 写入数据到发送寄存器
    USART_SendData(USART1, data);
    // 等待发送完成(可选,确保数据发送完毕)
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
(2)寄存器版:直接操作TDR寄存器

c

运行

复制代码
void USART1_SendByte(uint8_t data) {
    // 等待TXE位(bit7)置1(发送数据寄存器为空)
    while(!(USART1->SR & (1<<7)));
    // 写入数据到TDR寄存器(DR寄存器)
    USART1->DR = data;
    // 等待TC位(bit6)置1(发送完成)
    while(!(USART1->SR & (1<<6)));
}

2. 字符串发送(基于单字节封装)

c

运行

复制代码
// 发送字符串(以'\0'为结束标志)
void USART1_SendString(uint8_t *str) {
    while(*str != '\0') {
        USART1_SendByte(*str);
        str++;
    }
    // 可选:发送换行符(\r\n),电脑串口助手自动换行
    USART1_SendByte('\r');
    USART1_SendByte('\n');
}

3. printf 重定向(调试打印神器)

通过重定向fputc函数,可直接使用printf打印调试信息(如变量值、程序状态),无需手动调用发送函数。

(1)重定向实现(需包含stdio.h

c

运行

复制代码
#include <stdio.h>

// 重定向fputc函数,printf输出到USART1
int fputc(int ch, FILE *f) {
    // 调用单字节发送函数
    USART1_SendByte((uint8_t)ch);
    return ch;
}
(2)工程配置(关键!避免编译报错)
  1. 点击 KEIL 工具栏 "Options for Target"→"Target" 选项卡;
  2. 勾选 "Use MicroLIB"(启用微型标准库,支持 printf 重定向);
  3. 点击 "OK",编译时自动链接标准库函数。
(3)使用示例

c

运行

复制代码
int main(void) {
    uint16_t temp = 255;
    float voltage = 3.3f;

    USART1_Init(); // 初始化USART1

    while(1) {
        // 发送字符串
        USART1_SendString("Hello STM32 USART!");
        // printf打印变量
        printf("temp = %d, voltage = %.2fV\r\n", temp, voltage);
        // 延时1秒
        delay_ms(1000);
    }
}
(4)运行效果

电脑串口助手(如 SecureCRT、SSCOM)配置 115200bps 8N1,会周期性接收:

plaintext

复制代码
Hello STM32 USART!
temp = 255, voltage = 3.30V

四、串口接收实战:查询式 + 中断式(非阻塞)

串口接收分为 "查询阻塞式" 和 "中断非阻塞式",查询式适用于简单场景,中断式不占用 CPU 资源,是实战首选。

1. 查询阻塞式接收(简单场景,少用)

核心逻辑:循环查询接收数据寄存器(RDR)是否有数据,有则读取,无则等待(阻塞主程序)。

库函数版实现

c

运行

复制代码
// 查询接收单个字节(阻塞,超时返回0)
uint8_t USART1_ReceiveByte_Block(uint32_t timeout) {
    uint32_t tick = 0;
    // 等待接收数据寄存器(RDR)非空
    while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) {
        tick++;
        if(tick > timeout) {
            return 0; // 超时返回0
        }
    }
    // 读取接收数据
    return (uint8_t)USART_ReceiveData(USART1);
}

// 主函数测试
int main(void) {
    uint8_t rx_data;

    USART1_Init();
    USART1_SendString("USART1 Ready!");

    while(1) {
        // 等待接收数据(超时1000000次循环)
        rx_data = USART1_ReceiveByte_Block(1000000);
        if(rx_data != 0) {
            // 回显接收的数据
            USART1_SendString("Receive: ");
            USART1_SendByte(rx_data);
            USART1_SendByte('\r');
            USART1_SendByte('\n');
        }
    }
}
优缺点
  • 优点:代码简单,无需配置中断;
  • 缺点:阻塞主程序,期间无法执行其他任务(如 LED 闪烁),仅适用于低要求场景。

2. 中断非阻塞式接收(实战首选)

核心逻辑:接收数据时触发中断,在中断服务函数中读取数据,主程序无阻塞,可并行执行其他任务。

(1)库函数版完整实现

c

运行

复制代码
#include "stm32f10x.h"
#include <stdio.h>

#define RX_BUFFER_SIZE 128 // 接收缓冲区大小
uint8_t rx_buffer[RX_BUFFER_SIZE]; // 接收缓冲区
uint16_t rx_index = 0; // 缓冲区索引

// USART1初始化(含中断配置)
void USART1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置GPIO
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 配置USART1参数
    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStruct);

    // 4. 配置USART1接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收数据中断

    // 5. 配置NVIC(中断优先级)
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 6. 使能USART1
    USART_Cmd(USART1, ENABLE);
}

// USART1中断服务函数
void USART1_IRQHandler(void) {
    uint8_t rx_data;
    // 检查接收数据中断标志位
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        // 读取接收数据
        rx_data = (uint8_t)USART_ReceiveData(USART1);
        // 数据存入缓冲区(循环缓冲区,防止溢出)
        if(rx_index < RX_BUFFER_SIZE) {
            rx_buffer[rx_index++] = rx_data;
            // 回显数据(可选)
            USART1_SendByte(rx_data);
        } else {
            rx_index = 0; // 缓冲区溢出,重置索引
        }
        // 清除中断标志位(库函数自动清除,寄存器版需手动清除)
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

// 读取接收缓冲区数据(非阻塞)
uint16_t USART1_ReadBuffer(uint8_t *buf, uint16_t len) {
    uint16_t count = 0;
    while(rx_index > 0 && count < len) {
        buf[count++] = rx_buffer[--rx_index];
    }
    return count;
}

// 单字节发送函数(printf重定向用)
void USART1_SendByte(uint8_t data) {
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, data);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}

// printf重定向
int fputc(int ch, FILE *f) {
    USART1_SendByte((uint8_t)ch);
    return ch;
}

// 主函数测试(非阻塞接收+LED闪烁)
int main(void) {
    uint8_t buf[RX_BUFFER_SIZE];
    uint16_t len;

    USART1_Init();
    printf("USART1 Interrupt Receive Ready!\r\n");

    // 配置PB0为推挽输出(LED)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    while(1) {
        // 非阻塞读取接收数据
        len = USART1_ReadBuffer(buf, RX_BUFFER_SIZE);
        if(len > 0) {
            // 打印接收的数据
            printf("Receive %d bytes: ", len);
            for(uint16_t i=0; i<len; i++) {
                printf("%02X ", buf[i]);
            }
            printf("\r\n");
        }

        // 并行执行LED闪烁(无阻塞)
        GPIO_SetBits(GPIOB, GPIO_Pin_0);
        delay_ms(500);
        GPIO_ResetBits(GPIOB, GPIO_Pin_0);
        delay_ms(500);
    }
}
(2)寄存器版中断接收核心代码

c

运行

复制代码
// USART1中断服务函数(寄存器版)
void USART1_IRQHandler(void) {
    uint8_t rx_data;
    // 检查RXNE中断标志位(bit5)
    if(USART1->SR & (1<<5)) {
        // 读取接收数据
        rx_data = USART1->DR;
        // 存入缓冲区
        if(rx_index < RX_BUFFER_SIZE) {
            rx_buffer[rx_index++] = rx_data;
        } else {
            rx_index = 0;
        }
        // 寄存器版无需手动清除RXNE标志位(读取DR后自动清除)
    }
}
核心优势
  • 非阻塞:主程序可并行执行其他任务(如 LED 闪烁、传感器采集),不被接收操作阻塞;
  • 高效:数据接收由中断触发,仅在有数据时占用 CPU 资源;
  • 可扩展:通过循环缓冲区支持多字节连续接收,适配大数据量传输。

五、串口通信常见问题与避坑指南

1. 串口无输出 / 接收不到数据

高频原因与解决方案
  • 原因 1:TX/RX 接反→数据传输方向错误;解决:交换 STM32 TX 与 USB-TTL 模块 RX 的接线;
  • 原因 2:GPIO 模式配置错误(TX 未设为复用推挽);解决:TX 引脚必须配置为GPIO_Mode_AF_PP,而非GPIO_Mode_Out_PP
  • 原因 3:时钟未使能(USART1 或 GPIOA 时钟未开启);解决:确认RCC_APB2PeriphClockCmd已使能对应时钟;
  • 原因 4:波特率配置错误(如 USART1 按 APB1 时钟计算);解决:USART1 挂载 APB2(72MHz),波特率计算需用 72MHz 时钟;
  • 原因 5:未共地→信号干扰导致通信失败;解决:确保 STM32 与 USB-TTL 模块共地。

2. 通信乱码

高频原因与解决方案
  • 原因 1:波特率 / 数据位 / 校验位 / 停止位不匹配;解决:确保 STM32 与电脑串口助手配置一致(115200bps 8N1);
  • 原因 2:时钟频率误差过大(如外部晶振不是 8MHz);解决:核对system_stm32f10x.c中的系统时钟配置,确保 USART1 时钟为 72MHz;
  • 原因 3:信号干扰(导线过长、靠近电源模块);解决:使用短于 10cm 的优质杜邦线,远离电源电路和高频信号;
  • 原因 4:printf 重定向未启用 MicroLIB;解决:勾选 KEIL 的 "Use MicroLIB" 选项。

3. 中断接收不响应

高频原因与解决方案
  • 原因 1:未使能 USART 接收中断;解决:调用USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)
  • 原因 2:NVIC 未配置或优先级错误;解决:正确配置 NVIC 中断通道和优先级,确保中断使能;
  • 原因 3:中断服务函数名称错误;解决:中断服务函数名称必须为USART1_IRQHandler(启动文件弱定义名称);
  • 原因 4:未清除中断标志位;解决:寄存器版需读取 DR 寄存器清除 RXNE 标志位,库函数版调用USART_ClearITPendingBit

六、总结:串口实战核心要点与进阶方向

1. 核心要点回顾

  • 串口初始化四步走:时钟使能→GPIO 配置(TX 复用推挽,RX 浮空输入)→串口参数配置→中断配置(可选);
  • 发送核心:等待 TXE 位为空→写入数据,printf 重定向需启用 MicroLIB;
  • 接收核心:查询式适用于简单场景,中断式非阻塞是实战首选,需配合缓冲区存储数据;
  • 避坑关键:TX/RX 交叉连接、共地、GPIO 复用模式、波特率计算正确。

2. 进阶学习方向

  • DMA 接收:使用 DMA 实现串口数据高速接收,彻底解放 CPU;
  • 多串口通信:同时使用 USART1(与电脑)和 USART2(与蓝牙模块),实现数据转发;
  • 串口协议:自定义串口通信协议(如帧头 + 数据 + 校验位),实现可靠数据传输;
  • 无线通信:通过串口对接蓝牙、WiFi、4G 模块,实现远程数据传输。

串口是 STM32 开发的 "万能接口",掌握发送、接收、中断、printf 重定向后,你可以实现调试打印、设备通信、数据上传等几乎所有嵌入式通信场景。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 等外设的交互!

相关推荐
LCG元1 小时前
STM32项目实战:基于FreeRTOS的多任务智能家居控制系统
stm32·嵌入式硬件·智能家居
✎ ﹏梦醒͜ღ҉繁华落℘2 小时前
单片机基础知识 -- 大端模式 与 小端模式
单片机·嵌入式硬件
雾削木2 小时前
STM32 基于外部时钟源的 PWM 测量
stm32·单片机·嵌入式硬件
qq_411262422 小时前
esp的深度睡眠关机功耗很高,一般软件方面应该查哪里?
单片机·嵌入式硬件
San_a dreamer fish2 小时前
STM32开发入门(二):
stm32·单片机·嵌入式硬件
v先v关v住v获v取2 小时前
CC1031载货汽车变速器结构设计13张cad+设计说明书+三维图
科技·单片机·51单片机
冒险家KL3 小时前
STM32 ISP自动下载探索及官方STM32CubeProgrammer实现自动下载
stm32·嵌入式硬件·isp
Wave8453 小时前
智能家居安防系统
stm32·单片机·智能家居
鄭郑3 小时前
STM32学习笔记--SPI初始化与数据收发(01)
笔记·stm32·学习