1. 基本原理
1.1. 为什么在该项目中需要串口
为了使上位机(电脑)与下位机(STM32)之间能方便地信息交互以debug,故使用串口通信以实现。此外,由于蓝牙等外设也是依据串口协议通信的,故弄清楚了此模块也会更方便地驱动蓝牙。
1.2 串口通信的特点
STM32上有两类串口:USART(通用同步/异步收发器)和UART(通用异步收发器),就实际经验来说,用的一般都是异步模式。故将STM32串口归纳出三个特点
- 串行:意思就是一串数据的发送需要一位一位地来,对应的概念“并行”就是指所有位的数据同时发送。
- 异步:就是说通信双方不必按照统一的时钟,可以按照各自的节奏发送和接收,照样不出差错,稍后会阐述这是如何办到的。
- 全双工:简单来说就是上位机和下位机之间可以互相发送和接受消息。
1.3 串口通信的原理
在1.2中提到,串口通信是异步的,发送和接收方不依赖于统一的时钟信号,依然可以正确地接收信息,这主要依赖于两个东西:数据帧和波特率。
1.3.1 数据帧
数据帧规定了传输过程中数据的格式,满足这个格式的才叫一组数据,否则就可能是干扰信号。具体来说,串口通信的数据帧格式如下:
| 起始位 (1位) | 数据位 (8或9位) | 校验位 (可选) | 停止位 (1-2位) |
但就实话来讲,这个格式不必记忆(除非你打算自己写一份彻彻底底的串口通信底层代码),我们只需明白数据帧的作用就是让接收方知道在发来的一大串比特流中哪一位开始是我需要开始读的数据。
1.3.2 波特率
对于波特率,我们可以直接理解为发送比特(bit)的频率,也就是一秒发送的比特的个数,常见的波特率值如9600bps,表示的含义就是串口通信中发送方一秒会发送9600个比特。
写到这里,我们不妨思考一下:为什么我说依靠数据帧和波特率就可以基本实现异步通信了呢?其实可以这样理解,作为一个接收方,在面对发送方发来的一长串比特流中,他突然发现了数据帧的开头,于是他懂了:我得开始接收数据了,怎么接收呢?根据波特率来,假如波特率是9600,就说明发一位的时间是1/9600秒,如果数据帧是十位的,就说明我应该在10*(1/9600)秒后,暂时停止接收,因为就在这段时间内,我成功地依据数据帧开头和波特率实现了一次数据的接收!
1.4 代码实现串口通信的大致流程
对于初学者而言,虽然讲了原理,如何落实可能仍旧是云里雾里的:到底该怎样才能实现我们最初的目的,让电脑与单片机能用串口通信起来?
可见上图,其实电脑和单片机都有串口通信的模块,二者都有TX,RX,GND三个端,我们只需要用杜邦线或者别的线,让二者的TX与RX相连,RX与TX相连,GND相连,就可以通信了。
在STM32f4上,有可以拿来做RX端和TX端的GPIO口,但是同时它们是复用的,也就是说这些GPIO口可以同时承担别的功能,我们要想用它们来承担做串口通信的功能就得进行配置,因此,我们在代码上的任务就变成了配置GPIO口,将其配置成了串口需要的模式之后,就可以用了。
2. STM32CUBEMX配置
由于博主使用的是STM32CUBEMX软件进行辅助开发,因此在软件GUI上便可进行初步的配置,省去了很多自己写代码的步骤,如果需要自己写代码初始化串口配置的话,核心流程如下:
- 配置串口相关参数,如波特率和数据帧格式
- 使能GPIO时钟:要想使用它们来复用TX和RX就得先使能其时钟。
- 配置GPIO复用:将其复用为TX和RX端
如果使用STM32CUBEMX软件,上面这三步便不用自己写了,详细的操作可看下面这个视频,跟着视频随便点几下就可以配置好。
下图是我的配置
3. 实现代码
#include "MySerial.h"
#include "usart.h"
// 定义一个数组用于存储接收到的数据
char data[30] = {0};
// 单字节数据存储
uint8_t dat;
// 数据指针,指向当前写入位置
uint pointer;
/**
* @brief 串口初始化函数
*
* 初始化串口接收中断,使能接收数据。
*
* @param 无
* @return 无
*/
void MySerial_Init(void)
{
// 初始化UART1的接收中断,每次接收一个字节数据
HAL_UART_Receive_IT(&huart1, &dat, 1);
}
/**
* @brief UART接收中断回调函数
*
* 在串口接收到数据后调用,处理接收到的字节数据。
*
* @param huart 串口句柄指针
* @return 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 将接收到的数据存储到data数组中,并移动指针
data[pointer++] = dat;
// 再次启动UART接收中断以接收后续数据
HAL_UART_Receive_IT(&huart1, &dat, 1);
}
/**
* @brief 重定向标准输出函数
*
* 用于实现printf函数的重定向,将标准输出数据通过串口发送。
*
* @param ch 要发送的字符
* @param f 文件指针(这里无实际作用)
* @return 返回发送的字符
*/
int fputc(int ch, FILE *f)
{
// 通过UART1发送数据,等待时间为最大
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xfff);
return ch;
}
/**
* @brief 串口接收数据处理函数
*
* 检查是否有数据接收完成并稳定。如果接收完成调用数据处理函数。
*
* @param 无
* @return 无
*/
void MySerial_ReceiveData(void)
{
// 检查指针是否有变化,判断是否接收到新数据
if(pointer != 0) {
int tmp = pointer;
// 延迟1ms,等待数据接收稳定
HAL_Delay(1);
if(tmp == pointer) { // 如果延迟后指针未变,说明数据已完全接收
UART_RX_PROC(); // 调用数据处理函数
}
}
}
/**
* @brief 数据处理函数
*
* 处理接收到的数据并通过串口打印到标准输出。
*
* @param 无
* @return 无
*/
void UART_RX_PROC(void)
{
// 打印接收到的数据,并换行
printf("%s\n", data);
}