本章基于 STM32F103 标准外设库开发从最基础的“串口是什么”讲起通过生活化类比拆解硬件原理配合逐行注释的可运行代码让零基础读者读完就能实现单片机与电脑的双向通信。一、什么是串口通信1.1 串口的本质单片机的“电话”串口USART是单片机和电脑之间最常用的通信方式你可以把它想象成两个人打电话TX发送端你的嘴巴负责说话RX接收端你的耳朵负责听话全双工通信两个人可以同时说话和听话串口最常用的模式半双工通信同一时间只能一个人说另一个人听单工通信只能一个人说另一个人只能听比如广播1.2 串口通信的“约定”通信参数就像两个人打电话必须说同一种语言、语速差不多才能听懂一样串口通信也需要双方提前约定好参数否则会出现“鸡同鸭讲”的乱码。参数含义最常用值类比波特率每秒传输的二进制位数115200说话的速度单位比特/秒数据位每次传输的数据长度8 位每次说 8 个字停止位数据传输结束的标志1 位说完一句话停顿一下校验位检查数据传输是否出错无校验说完后确认对方有没有听清注意电脑和单片机的这四个参数必须完全一致否则一定会出现乱码。二、STM32 串口硬件原理2.1 电脑和单片机之间的“翻译官”CH340G电脑只有 USB 接口没有串口接口而单片机只有串口接口没有 USB 接口。所以需要一个翻译官把 USB 信号和串口信号互相转换这个翻译官就是 CH340G 芯片。电脑 USB 接口 ↔ CH340G 芯片 ↔ STM32 单片机串口 USB 信号 串口信号 串口信号CH340G 的硬件连接非常简单CH340G 的 TX 引脚接单片机的 RX 引脚CH340G 的 RX 引脚接单片机的 TX 引脚两者的 GND 引脚必须连接在一起共地注意TX 和 RX 必须交叉连接这是新手最容易犯的错误。如果接反了串口会完全没有反应。2.2 STM32 串口内部结构STM32F103 一共有 5 个串口USART1、USART2、USART3、UART4、UART5。它们的内部结构基本相同主要由以下几个部分组成┌─────────────────────────────────────┐ │ │ CPU ─────►│ 数据寄存器(DR) ◄──► 移位寄存器 │ ◄──► TX引脚 │ (一个地址对应两个寄存器) │ │ 发送寄存器(TDR) 接收寄存器(RDR) │ ◄──► RX引脚 │ │ ├─────────────────────────────────────┤ │ │ │ 波特率发生器(BRR) ◄─── 控制器 │ │ │ └─────────────────────────────────────┘数据寄存器(DR)相当于一个邮箱。发送数据时CPU 把数据放进发送邮箱(TDR)接收数据时CPU 从接收邮箱(RDR)里取数据。移位寄存器把并行数据转换成串行数据发送出去或者把接收到的串行数据转换成并行数据。波特率发生器根据配置的波特率产生串口通信需要的时钟信号。控制器控制串口的工作模式、中断等。三、串口初始化的核心步骤串口初始化就像给手机设置通话功能需要一步步完成以下操作给手机充电使能 GPIO 和串口的时钟设置手机模式配置 TX 和 RX 引脚的工作模式设置通话参数配置波特率、数据位、停止位、校验位开启来电提醒配置接收中断可选开机使能串口3.1 为什么要使能时钟STM32 的所有外设默认都是关闭的这样可以节省功耗。就像你不用手机的时候会关机一样使用外设之前必须先打开它的电源时钟。USART1 挂载在APB2 总线上时钟频率 72MHzUSART2、USART3、UART4、UART5 挂载在APB1 总线上时钟频率 36MHz注意这是串口初始化最容易出错的地方如果 USART2 用了 APB2 的时钟串口会完全无法工作。3.2 GPIO 引脚模式配置TX 引脚配置为复用推挽输出。因为 TX 需要主动输出高低电平推挽输出可以提供足够的电流。RX 引脚配置为浮空输入。因为 RX 需要接收外部的信号浮空输入可以准确检测外部的高低电平。四、完整代码实现4.1 头文件设计bsp_usart.h我们把所有硬件相关的参数都用宏定义封装起来这样以后切换串口时只需要修改宏定义即可。#ifndef __BSP_USART_H #define __BSP_USART_H #include stm32f10x.h #include stdio.h #define DEBUG_USART1 0 #define DEBUG_USART2 0 #define DEBUG_USART3 0 #define DEBUG_USART4 0 #define DEBUG_USART5 1 #if DEBUG_USART1 // 串口1-USART1 #define DEBUG_USARTx USART1 #define DEBUG_USART_CLK RCC_APB2Periph_USART1 #define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口1 USART GPIO 引脚宏定义 #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 #define DEBUG_USART_IRQ USART1_IRQn #define DEBUG_USART_IRQHandler USART1_IRQHandler #elif DEBUG_USART2 //串口2-USART2 #define DEBUG_USARTx USART2 #define DEBUG_USART_CLK RCC_APB1Periph_USART2 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口2 USART GPIO 引脚宏定义 #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_2 #define DEBUG_USART_RX_GPIO_PORT GPIOA #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_3 #define DEBUG_USART_IRQ USART2_IRQn #define DEBUG_USART_IRQHandler USART2_IRQHandler #elif DEBUG_USART3 //串口3-USART3 #define DEBUG_USARTx USART3 #define DEBUG_USART_CLK RCC_APB1Periph_USART3 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口3 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOB) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOB #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_RX_GPIO_PORT GPIOB #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_11 #define DEBUG_USART_IRQ USART3_IRQn #define DEBUG_USART_IRQHandler USART3_IRQHandler #elif DEBUG_USART4 //串口4-UART4 #define DEBUG_USARTx UART4 #define DEBUG_USART_CLK RCC_APB1Periph_UART4 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口4 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOC) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOC #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_RX_GPIO_PORT GPIOC #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_11 #define DEBUG_USART_IRQ UART4_IRQn #define DEBUG_USART_IRQHandler UART4_IRQHandler #elif DEBUG_USART5 //串口5-UART5 #define DEBUG_USARTx UART5 #define DEBUG_USART_CLK RCC_APB1Periph_UART5 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口5 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOC #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_12 #define DEBUG_USART_RX_GPIO_PORT GPIOD #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_2 #define DEBUG_USART_IRQ UART5_IRQn #define DEBUG_USART_IRQHandler UART5_IRQHandler #endif void USART_Config(void); void Usart_SendByte(USART_TypeDef* pUSARTx,uint8_t data); void Usart_sendHalfWord(USART_TypeDef * pUSARTx, uint16_t data); void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *array, uint8_t size); void Usart_SendString(USART_TypeDef * pUSARTx, char *str); #endif /* __BSP_USART_H */4.2 源文件实现bsp_usart.c// bsp_usart.c #include bsp_usart.h /** * brief 配置嵌套向量中断控制器 NVIC * param 无 * retval 无 */ static void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 配置中断优先级分组为 2 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 配置 USART 为中断源 */ NVIC_InitStructure.NVIC_IRQChannel DEBUG_USART_IRQn; /* 抢断优先级为 1 */ NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; /* 子优先级为 1 */ NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; /* 使能中断 */ NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; /* 初始化 NVIC */ NVIC_Init(NVIC_InitStructure); } /** * brief USART 初始化函数 * param 无 * retval 无 */ void USART_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 使能串口时钟 DEBUG_USART_APBx(DEBUG_USART_CLK, ENABLE); // 2. 使能 GPIO 时钟 RCC_APB2PeriphClockCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 3. 配置 TX 引脚为复用推挽输出 GPIO_InitStructure.GPIO_Pin DEBUG_USART_TX_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 引脚速度 50MHz GPIO_Init(DEBUG_USART_GPIO, GPIO_InitStructure); // 4. 配置 RX 引脚为浮空输入 GPIO_InitStructure.GPIO_Pin DEBUG_USART_RX_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(DEBUG_USART_GPIO, GPIO_InitStructure); // 5. 配置串口参数 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_Init(DEBUG_USARTx, USART_InitStructure); // 6. 配置串口接收中断 USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); // 7. 配置 NVIC 中断优先级 NVIC_Configuration(); // 8. 使能串口 USART_Cmd(DEBUG_USARTx, ENABLE); } /** * brief 发送单个字节 * param pUSARTx: 串口外设 * param ch: 待发送字节 * retval 无 */ void Usart_SendByte(USART_TypeDef * pUSARTx, uint8_t ch) { // 把数据写入发送数据寄存器 USART_SendData(pUSARTx, ch); // 等待发送数据寄存器为空TXE 标志位置 1 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) RESET); } /** * brief 发送字符串 * param pUSARTx: 串口外设 * param str: 待发送字符串 * retval 无 */ void Usart_SendString(USART_TypeDef * pUSARTx, char *str) { unsigned int k 0; do { // 逐个发送字符串中的字符 Usart_SendByte(pUSARTx, *(str k)); k; } while (*(str k) ! \0); // 直到遇到字符串结束符\0 // 等待所有数据发送完成TC 标志位置 1 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) RESET); } /** * brief 重定向 C 库函数 printf 到串口 * param ch: 待发送字符 * param f: 文件指针 * retval 发送的字符 */ int fputc(int ch, FILE *f) { Usart_SendByte(DEBUG_USARTx, (uint8_t)ch); return ch; } /** * brief 串口接收中断服务函数 * param 无 * retval 无 */ void DEBUG_USART_IRQHandler(void) { uint8_t ucTemp; // 检查是否是接收中断RXNE 标志位置 1 if (USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) ! RESET) { // 读取接收到的数据 ucTemp USART_ReceiveData(DEBUG_USARTx); // 把接收到的数据回显给电脑 Usart_SendByte(DEBUG_USARTx, ucTemp); } }4.3 主函数main.c// main.c #include stm32f10x.h #include bsp_usart.h int main(void) { // 初始化串口 USART_Config(); // 发送欢迎信息 Usart_SendString(DEBUG_USARTx, 这是一个串口回显实验\r\n); Usart_SendString(DEBUG_USARTx, 你发送什么单片机就会返回什么\r\n); // 也可以使用 printf 发送 printf(printf 重定向成功\r\n); while(1) { // 主循环什么都不做所有工作都在中断中完成 } }五、printf 重定向详解5.1 什么是 printf 重定向printf 是 C 语言标准库中的函数默认是把数据输出到电脑的屏幕上。在单片机中我们没有屏幕所以需要把 printf 的输出重定向到串口这样就可以用 printf 方便地打印调试信息了。5.2 重定向的原理printf 函数内部会调用 fputc 函数来输出单个字符。我们只需要重新实现 fputc 函数让它把字符发送到串口printf 就会自动把所有数据输出到串口了。5.3 必须注意的事项在 Keil MDK 中使用 printf 重定向必须勾选“Use MicroLIB”选项点击 Keil 工具栏的“魔术棒”按钮切换到“Target”选项卡勾选“Use MicroLIB”点击“OK”保存设置注意如果不勾选这个选项程序会编译失败或者运行时卡死。六、串口接收的两种方式6.1 查询方式不推荐查询方式就是 CPU 一直不停地检查接收数据寄存器是否有数据。就像你一直盯着门口等快递什么事都干不了。// 查询方式接收数据 uint8_t Usart_ReceiveByte(USART_TypeDef * pUSARTx) { // 等待接收数据寄存器非空RXNE 标志位置 1 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_RXNE) RESET); // 返回接收到的数据 return USART_ReceiveData(pUSARTx); }6.2 中断方式推荐中断方式就是 CPU 不用一直等当有数据到来时串口会给 CPU 发一个中断信号CPU 再去处理数据。就像快递员给你打电话你再去门口取快递平时你可以干别的事。这就是我们在代码中使用的方式它可以大大节省 CPU 资源。七、实验验证步骤7.1 软件准备安装 CH340G 驱动程序如果电脑没有自动安装下载并打开串口助手推荐使用野火串口助手7.2 硬件连接用 USB 线连接开发板的“USB TO UART”接口和电脑确保开发板上 USART1 的跳帽已经插上7.3 串口助手设置选择正确的串口号可以在设备管理器中查看设置波特率为 115200设置数据位为 8停止位为 1校验位为无点击“打开串口”按钮7.4 下载程序并测试编译程序并下载到开发板此时串口助手应该会收到欢迎信息在串口助手的发送框中输入任意字符点击“发送”串口助手的接收框中会显示你发送的字符八、常见问题与避坑指南8.1 串口完全没有输出可能原因及解决方法跳帽未插检查开发板上 USART1 的跳帽是否已经插上TX 和 RX 接反确认 CH340G 的 TX 接单片机的 RXRX 接单片机的 TX时钟总线配置错误USART1 在 APB2 总线其他串口在 APB1 总线串口助手未打开确认串口助手已经打开了正确的串口号程序未下载确认程序已经成功下载到开发板8.2 串口输出乱码可能原因及解决方法参数不匹配确认串口助手和代码中的波特率、数据位、停止位、校验位完全一致系统时钟配置错误如果系统时钟不是 72MHz串口波特率会不准CH340G 驱动问题重新安装 CH340G 驱动程序8.3 只能发送不能接收可能原因及解决方法RX 引脚配置错误确认 RX 引脚配置为浮空输入接收中断未使能确认调用了 USART_ITConfig 函数使能了接收中断中断服务函数名错误确认中断服务函数名和头文件中定义的一致九、小结串口是单片机和电脑之间最常用的通信方式本质是全双工的串行通信串口通信的四个参数波特率、数据位、停止位、校验位必须完全一致CH340G 是 USB 转串口芯片负责电脑和单片机之间的信号转换串口初始化的核心步骤使能时钟→配置 GPIO→配置串口参数→配置中断→使能串口推荐使用中断方式接收数据可以大大节省 CPU 资源使用 printf 重定向需要勾选 Keil 中的“Use MicroLIB”选项参考出处《零死角玩转 STM32F103-指南者》第 21 章 USART 串口通信STM32F103 官方参考手册 RM0008野火 STM32 串口通信教学视频