stm32教程:USART串口通信
按我目前接触下来的感受,通信一般都是比较常用的,在设计一个项目的时候一般用到的开发板都不止一个,但是最后要将功能联合起来,那么就需要通过通信了, 而其中比较方便的就是串口通信,简单好用。
文章目录
- 什么是通信?
- 通信接口
- 双工
- 时钟
- 电平
- 设备
- 串口通信
- 串口通信概述
- 串口通信核心工作原理
- 1. 物理层基础
- TX 和 RX需要反接
- 2. 串口参数及时序
- STM32的 USART 外设
- 简介
- 硬件连接
- 串口发送
- 完整的串口发送代码
- 串口接收
- 中断服务配置
- 获取接收内容
- 进阶 —— 传输字符串
- 尾声
- 感谢大伙观看,别忘了三连支持一下
- 大家也可以关注一下我的其它专栏,同样精彩喔~
- 下期见咯~
什么是通信?
在硬件领域中,通信就是将一个设备的数据传送到另一个设备,扩展硬件系统。
根据不同的通信规则衍生出了很多种通信协议,根据这些通信协议,通信双方按照协议规则进行数据收发。
通信接口
在stm32f103c8t6这个最小系统板中,实现了很多种通信协议,如下表所示:
名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
---|---|---|---|---|---|
USART | TX、RX | 全双工 | 异步 | 单端 | 点对点 |
I2C | SCL、SDA | 半双工 | 同步 | 单端 | 多设备 |
SPI | SCLK、MOSI、MISO、CS | 全双工 | 同步 | 单端 | 多设备 |
CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 |
USB | DP、DM | 半双工 | 异步 | 差分 | 点对点 |
双工
全双工,就是指通信双方能够同时进行双向通信。一般全双工的都会有两根通信线,发送和接收互不影响。
半双工,这种方式一般只有一根数据线。
单工, 例如说把 USART 的RX去掉,那它就不能实现数据的接收,那它就是一个单工的。
时钟
同步, 同步时钟能够通过时钟线来进行采样。
异步, 通过特定的采样频率来进行采样,并且需要设置帧头帧尾来进行数据的对齐。
电平
单端,引脚的高低电平是相对于GND而言的,所以通信双方需要进行共地才能进行通信。
双端, 是靠两个差分引脚的电压差来进行信号传输的。一般双端通信具有比较好的抗干扰特性和传输速度
设备
点对点,就是两个设备一对一通信。
多设备, 一个设备同时对多个设备进行传输,需要一个寻址的过程,来确定接收对象。
串口通信
串口通信概述
串口通信(Serial Communication)是嵌入式系统中最常用的通信方式之一,其核心思想是通过逐位传输实现设备间的数据交互。在STM32微控制器中,UART(Universal Asynchronous Receiver/Transmitter)模块承担了异步串行通信的核心功能,具有全双工、异步、高可靠性等特点,广泛应用于传感器通信、模块调试、设备控制等场景。
串口通信核心工作原理
1. 物理层基础
电平标准:常见的有TTL电平(3.3V / 5V)和RS-232电平(±12V)
连接方式:采用三线制(TX发送、RX接收、GND地线)实现全双工通信
TX 和 RX需要反接
2. 串口参数及时序
波特率: 串口通信的速率
起始位: 标志一个数据帧的开始,固定为低电平
数据位: 数据帧的有效载荷,1为高电平,0为低电平,低位先行
校验位: 用于数据验证,根据数据位计算得来
停止位: 用于数据帧间隔,固定为高电平
再额外说一下校验位,它与数据有关.
例如说选择奇校验,数据位加上校验位的 1 数量为奇数。
数据位:10001110 —— 1有4个,是偶数 ——那么校验位为 1
数据位:10001100 —— 1有3个,是奇数 ——那么校验位为 0
偶校验则反之。
STM32的 USART 外设
前面所说的都是 USART协议 ,然后下面来详细说一下在stm32中的 USART外设 ,来看看在stm32中怎么去实现和使用USART来进行通讯。
简介
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
自带波特率发生器,最高达4.5Mbits/s
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
可选校验位(无校验/奇校验/偶校验)
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
STM32F103C8T6 USART资源: USART1、 USART2、 USART3
硬件连接
这里就以 STM32 与 PC通信为例。
需要使用的硬件设备有: stm32f103c8t6最小系统板,一台电脑, st-link, USB转串口,几根杜邦线。
stm32 | usb转串口 |
---|---|
PA9(TX) | RX |
PA10(RX) | TX |
也就是stm32的 USART1 的两个引脚连到 usb转串口上,记得要反接。
串口发送
- 使能外设时钟:开启 USART 和对应 GPIO 的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
- 配置 GPIO 引脚:将 TX 引脚设置为复用推挽输出
GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);
- 初始化 USART 参数:设置波特率、数据位、停止位等
/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx; //模式,选择为发送模式USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
- 实现发送功能:通过查询或中断方式发送数据
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
因为stm库函数实现了UART发送一个字符,我们直接调用就能直接实现需求,只需要等到标志位变回SET,即可。
然后我们要发送数组只需要套一层循环即可。
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}
发送字符数组:
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}
完整的串口发送代码
serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>/*** 函 数:串口初始化* 参 数:无* 返 回 值:无*/
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx; //模式,选择为发送模式USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}/*** 函 数:串口发送一个字节* 参 数:Byte 要发送的一个字节* 返 回 值:无*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}/*** 函 数:串口发送一个数组* 参 数:Array 要发送数组的首地址* 参 数:Length 要发送数组的长度* 返 回 值:无*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍历数组{Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数:串口发送一个字符串* 参 数:String 要发送字符串的首地址* 返 回 值:无*/
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止{Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据}
}/*** 函 数:次方函数(内部使用)* 返 回 值:返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1; //设置结果初值为1while (Y --) //执行Y次{Result *= X; //将X累乘到结果}return Result;
}/*** 函 数:串口发送数字* 参 数:Number 要发送的数字,范围:0~4294967295* 参 数:Length 要发送数字的长度,范围:0~10* 返 回 值:无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字}
}/*** 函 数:使用printf需要重定向的底层函数* 参 数:保持原始格式即可,无需变动* 返 回 值:保持原始格式即可,无需变动*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数return ch;
}/*** 函 数:自己封装的prinf函数* 参 数:format 格式化字符串* 参 数:... 可变的参数列表* 返 回 值:无*/
void Serial_Printf(char *format, ...)
{char String[100]; //定义字符数组va_list arg; //定义可变参数列表数据类型的变量argva_start(arg, format); //从format开始,接收参数列表到arg变量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg); //结束变量argSerial_SendString(String); //串口发送字符数组(字符串)
}
serial.h
#ifndef __SERIAL_H
#define __SERIAL_H#include <stdio.h>void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);#endif
串口接收
当外部设备通过串口发送 1 个字节到 STM32 时,数据会被存入 USART 的接收数据寄存器(RDR),此时 RXNE 标志自动置 1。
若提前配置了 “允许 RXNE 中断”,则 RXNE=1 时会触发中断请求,CPU 会跳转到对应的中断服务函数执行。
在中断服务函数中,需读取 RDR 中的数据(读取后 RXNE 标志会自动清零,或手动清零),否则会持续触发中断。
中断服务配置
void Serial_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定义结构体变量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1/*中断输出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断/*NVIC中断分组*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
在原本发送的初始化中添加NVIC配置,也就是配置中断。
下面以 USART1 为例,注意选择需要使用的中断服务函数命名
void USART1_IRQHandler(void)
{if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断{Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量Serial_RxFlag = 1; //置接收标志位变量为1USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位//读取数据寄存器会自动清除此标志位//如果已经读取了数据寄存器,也可以不执行此代码}
}
这时候我们的接收内容就存储到了Serial_RxData 里了。
获取接收内容
下面我们只需要在主函数中读取Serial_RxData 的内容。
在Serial.c里写两个函数,分别返回Serial_RxData 和Serial_RxFlag 的值 。
uint8_t Serial_GetRxFlag(void)
{if (Serial_RxFlag == 1) //如果标志位为1{Serial_RxFlag = 0;return 1; //则返回1,并自动清零标志位}return 0; //如果标志位为0,则返回0
}
uint8_t Serial_GetRxData(void)
{return Serial_RxData; //返回接收的数据变量
}
然后再main.c里面判断 Serial_RxFlag ,为 1 既是读取到了新的内容,再将 Serial_RxData 的值传输出来即可。
while (1){if (Serial_GetRxFlag() == 1) //检查串口接收数据的标志位{RxData = Serial_GetRxData(); //获取串口接收的数据Serial_SendByte(RxData); //串口将收到的数据回传回去,用于测试OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据}}
进阶 —— 传输字符串
传输字符串和单个字符的差别就在于数量,我们只需要 判定字符串头尾,然后将字符串内容都保存下来,然后传递给主函数即可。
首先,修改中断服务函数。
void USART1_IRQHandler(void)
{static uint8_t RxState = 0; //定义表示当前状态机状态的静态变量static uint8_t pRxPacket = 0; //定义表示当前接收数据位置的静态变量if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断{uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量/*使用状态机的思路,依次处理数据包的不同部分*//*当前状态为0,接收数据包包头*/if (RxState == 0){if (RxData == '@' && Serial_RxFlag == 0) //如果数据确实是包头,并且上一个数据包已处理完毕{RxState = 1; //置下一个状态pRxPacket = 0; //数据包的位置归零}}/*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/else if (RxState == 1){if (RxData == '\r') //如果收到第一个包尾{RxState = 2; //置下一个状态}else //接收到了正常的数据{Serial_RxPacket[pRxPacket] = RxData; //将数据存入数据包数组的指定位置pRxPacket ++; //数据包的位置自增}}/*当前状态为2,接收数据包第二个包尾*/else if (RxState == 2){if (RxData == '\n') //如果收到第二个包尾{RxState = 0; //状态归0Serial_RxPacket[pRxPacket] = '\0'; //将收到的字符数据包添加一个字符串结束标志Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包}}USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位}
}
然后在 Serail.h 中加入 ——
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;
就是共同使用同一个变量。
同样,也要在Serial.c 中加入变量声明 ——
char Serial_RxPacket[100]; //定义接收数据包数组,数据包格式"@MSG\r\n"
uint8_t Serial_RxFlag; //定义接收数据包标志位
尾声
OK,串口的大致内容就是这些,如果有问题,可以私信 或者 评论,我会尽我所能帮助大家,需要源代码也是同样可以 私信 或者 评论。