USART串口通信
通信接口
通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
通信协议的最基本的配置如下表
双工
全双工:通信双方能够同时进行双向通信
半双工:通信双方不能同时接收和发送数据
单工:只能接收或只能发送
时钟
同步:有时钟线,通信双方根据时钟线的频率接收和发送数据
异步:没有时钟线,只能通信双方约定使用相同的频率接收和发送数据
电平
单端:根据两端的电压确定数据,例如STM32就是0V发送数据0,3.3V发送数据1
差分:根据差分线的电压差确定数据
设备
点对点:设备一对一发送和接收
多设备:可以挂载多个设备,通过地址来与不同的设备通信
串口通信
串口通信介绍
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力
串口硬件电路
- 简单双向串口通信有两根通信线(发送端TX和接收端RX)
- TX与RX要交叉连接
- 当只需单向的数据传输时,可以只接一根通信线
- 当电平标准不一致时,需要加电平转换芯片
串口常用电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
- TTL电平:+3.3V或+5V表示1,0V表示0
- RS232电平:-3-15V表示1,+3+15V表示0
- RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)
串口参数及时序
波特率:串口通信的速率
波特率 = fPCLK2\1 / (16 * DIV)
起始位:标志一个数据帧的开始,固定为低电平
空闲状态时一直处于高电平,发送端来一个下降沿,接收端就知道要开始接收数据了
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
校验位:用于数据验证,根据数据位计算得来
奇校验:当数据位中的1是偶数位时,校验位为1,将1的个数补位奇数个
偶校验:当数据位中的1是寄数位时,校验位为1,将1的个数补位偶数个
无校验:数据帧就只有8位数据
停止位:用于数据帧间隔,固定为高电平
实际波形
STM32的USART
USART简介
USART(Universal Synchronous/Asynchronous
Receiver/Transmitter)通用同步/异步收发器
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
自带波特率发生器,最高达4.5Mbits/s 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
可选校验位(无校验/奇校验/偶校验) 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN STM32F103C8T6
USART资源: USART1、 USART2、 USART3
USART框图
引脚部分
TX:发送引脚
RX:接收引脚
SW_RX、IRDA_OUT、IRDA_IN:智能卡和IrDA通信的引脚
nRTS:请求发送,告诉发送设备我现在能不能接收数据
nCTS:接受其他设备的nRTS信号,根据接收设备的信号判断现在能不能发送数据
注意:n表示低电平有效
寄存器及控制器
- 发送数据寄存器、接收数据寄存器:这两个寄存器占用同一个地址。在程序中只表现为一个寄存器,也就是DR寄存器,但是实际硬件中是分成了两个寄存器。TDR是只写的,RDR是只读的
- 发送移位寄存器:把一个字节的数据一位一位的移出去。当我们给TDR(发送数据寄存器)写入数据时,会检测移位寄存器是不是有数据正在移位,如果没有,则会自动把写入TDR的数据全部放到移位寄存器上准备发送数据。当数据从TDR移动到移位寄存器时,会置一个标志位:TXE;当TXE=1,表示TDR为空了,我们就可以写入下一个要发送的数据了
- 接收移位寄存器:与发送移位寄存器的原理是类似的。数据从RX引脚通向接受移位寄存器,一位一位的读取RX电平,每一次读的数据放在最高位,然后向右移,移位八次后就接收到了一个字节,再将这个字节整个放到RDR(接收数据寄存器)中。从移位寄存器转移数据到RDR的过程中会置一个标志位:RXNE;当RXNE=1时,说明RDR中已经接收到了数据,可以读取数据了
- 发送器控制、接收器控制:根据波特率分别控制发送移位寄存器和接受移位寄存器进行移位的操作
- 中断控制:查看SR(状态寄存器)的各种标志位,其中TXE(发送寄存器空)和RXNE(接收寄存器非空)很重要;还控制中断是否通信NVIC
- 波特率发生器部分:APB时钟进行分频,得到发送和接受移位的时钟
- 硬件数据流控:如果发送设备发的太快,接收设备来不及处理,会造成数据丢失或被覆盖,该控制器就是避免这个问题的
- SCLK控制:用于产生同步的时钟信号。配合发送移位寄存器输出,发送移位寄存器每移位一次,同步时钟电平就跳变一个周期
- 唤醒单元:实现串口挂载多设备。可以在USART地址这个地方给串口分配一个地址,当发送指定地址时,此设备唤醒开始工作
USART基本结构
USART数据帧
设置字长
设置停止位
一个停止位的时长与一个数据位的时长一致
USART起始位侦测
当USART的电路检测到下降沿后,如果没有噪声,后面开始就应该是起始位了。
为了检测是否存在噪声导致误判,USART会以波特率的16倍频率进行采样,也就是在一位的时间里进行16次采样,开始起始位侦测
- 如果3个采样点都为’0’(在第3、5、7位的第一次采样,和在第8、9、10的第二次采样都为’0’), 则确认收到起始位,这时设置RXNE标志位,如果RXNEIE=1,则产生中断。
- 如果两次3个采样点上最少有2个是’0’(第3、5、7位的采样点和第8、9、10位的采样点),那么起始 位仍然是有效的,但是会设置NE噪声标志位。如果不能满足这个条件,则中止起始位的侦测过 程,接收器会回到空闲状态(不设置标志位)。
USART检测噪声的数据采样
通过USART的起始位侦测后,就开始接收数据了,跟起始位侦测一样,也是以波特率的16倍频率进行采样,也就是在一个数据位的时间里进行16次采样
根据第8、9、10次的采样数据确定接收到的数据
- 当三次采样都为0或1时,可以确定接收的数据就是0或1
- 当三次采样有两个0或两个1时,则以出现次数多的电平为准,同时会设置NE噪声标志位
波特率计算公式
发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
计算公式:波特率 = fPCLK2\1 / (16 * DIV)
fPCLK2\1:APB1或APB2的时钟频率
DIV:分频系数,可以是小数,小数部分最多4位
16:数据采样的频率,每一位数据都要进行16次采样
例如要配置USART1为9600的波特率,计算DIV(分频系数):
USART1属于APB2总线的外设,所以fPCLK2=72MHz
9600=72MHz / (16 * DIV)
DIV = 72MHz / 9600 / 16
DIV = 468.75
CH340原理图
USART库函数
USART_InitTypeDef
typedef struct
{
uint32_t USART_BaudRate; /*!< 波特率 */
uint16_t USART_WordLength; /*!< 帧中发送或接收的数据位数。 */
uint16_t USART_StopBits; /*!< 停止位的长度 */
uint16_t USART_Parity; /*!< 校验模式 */
uint16_t USART_Mode; /*!< 指定是否启用或禁用接收或发送模式 */
uint16_t USART_HardwareFlowControl; /*!< 开启或关闭硬件流控制 */
} USART_InitTypeDef;
/* USART_BaudRate */
直接写需要的波特率就好了,函数会自动根据传入的波特率来计算BRR的值
/* USART_WordLength */
USART_WordLength_8b // 一帧发送8位数据
USART_WordLength_9b // 一帧发送9位数据
/* USART_StopBits */
USART_StopBits_1 // 一位个数据位长的停止位
USART_StopBits_0_5 // 0.5个数据位长的停止位
USART_StopBits_2 // 2个数据位长的停止位
USART_StopBits_1_5 // 1.5个数据位长的停止位
/* USART_Parity */
USART_Parity_No // 无校验
USART_Parity_Even // 奇校验
USART_Parity_Odd // 偶校验
/* USART_Mode */
USART_Mode_Rx // 开启发送数据
USART_Mode_Tx // 开启接收数据
// 如果即开启发送数据又开启接收数据,可以用 "|" 连接
/* USART_HardwareFlowControl */
USART_HardwareFlowControl_None // 不开启硬件流控制
USART_HardwareFlowControl_RTS // 告诉对方是否可以发送数据了
USART_HardwareFlowControl_CTS // 判断对方是否准备好接收数据了
USART_HardwareFlowControl_RTS_CTS // RTS和CTS都开启
函数
// 复位USART的配置
void USART_DeInit(USART_TypeDef* USARTx);
// 根据USART_InitTypeDef中的指定参数初始化USART
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
// 用默认值填充每个USART_InitTypeDef成员。
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
// 根据usart_clockkinitstruct中的指定参数初始化USARTx外设时钟。配置同步时钟输出的
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
// 用默认值填充每个usart_clockkinitstruct成员。配置同步时钟输出的
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
// 开启或关闭USART
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 开启或关闭USART的中断
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
// 开启或关闭USART的DMA功能
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
// 设置USART节点的地址,挂载多个USART时使用
void USART_SetAddress(USART_TypeDef* USARTx, uint8_t USART_Address);
// 选择USART唤醒方法
void USART_WakeUpConfig(USART_TypeDef* USARTx, uint16_t USART_WakeUp);
// 开启或关闭USART的静音模式
void USART_ReceiverWakeUpCmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 设置USART LIN中断检测长度
void USART_LINBreakDetectLengthConfig(USART_TypeDef* USARTx, uint16_t USART_LINBreakDetectLength);
// 启用或禁用USART LIN模式
void USART_LINCmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 发送数据。写USART的DR寄存器
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
// 接收数据。读USART的DR寄存器
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
/* 智能卡、IrDA的函数用不上 */
void USART_SendBreak(USART_TypeDef* USARTx);
void USART_SetGuardTime(USART_TypeDef* USARTx, uint8_t USART_GuardTime);
void USART_SetPrescaler(USART_TypeDef* USARTx, uint8_t USART_Prescaler);
void USART_SmartCardCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_SmartCardNACKCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_HalfDuplexCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_OverSampling8Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_OneBitMethodCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_IrDAConfig(USART_TypeDef* USARTx, uint16_t USART_IrDAMode);
void USART_IrDACmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 获取指定的USART相关标志位的状态
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 清除指定的USART相关标志位
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 获取指定的USART相关中断标志位的状态
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
// 清除指定的USART相关的中断标志位
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
案例1
串口发送数据
用到函数
// 根据USART_InitTypeDef中的指定参数初始化USART
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
// 开启或关闭USART
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
// 发送数据。写USART的DR寄存器
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
// 获取指定的USART相关标志位的状态
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
接线图
直接使用串口发送
示例代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief 配置USART1的发送功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx; // 开启USART的发送
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 发送字节
* @param Data: 要发送的字节
* @retval 无
*/
void Serial_SendByte(uint8_t Data)
{
// 写入数据到发送数据寄存器中
USART_SendData(USART1, Data);
// 获取USART的TXE标志位是否为1。TXE=1说明发送数据寄存器已经将数据转移到发送移位寄存器了
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
// 通过手册可知:对USART_DR的写操作,硬件将该位清零。所以不用手动清除
// USART_ClearFlag(USART1, USART_FLAG_TXE);
}
int main()
{
Serial_Init();
while(1)
{
uint8_t Data = 0x41;
// 循环打印A~Z
for(uint8_t i = 0;i < 26; i++)
{
Serial_SendByte(Data++);
Delay_s(1);
}
}
}
通过printf函数发送
因为printf默认打印输出到屏幕,所以需要重定向printf函数,将printf函数打印的东西输出到串口
勾选Use MicroLIB
包含stdio头文件
#include <stdio.h>
重写fput函数
因为printf函数是通过fput函数实现的,所以重写fput函数就是重定向printf函数
// 重定向fput函数,通过串口输出
int fput(int ch, FILE *f)
{
// Serial_SendByte是自己写的输出到串口的函数,参数是字节
Serial_SendByte(ch);
return ch;
}
最后就可以直接使用printf函数打印串口输出的数据了
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include <stdio.h>
/**
* @brief 配置USART1的发送功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx; // 开启USART的发送
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 发送字节
* @param Data: 要发送的字节
* @retval 无
*/
void Serial_SendByte(uint8_t Data)
{
// 写入数据到发送数据寄存器中
USART_SendData(USART1, Data);
// 获取USART的TXE标志位是否为1。TXE=1说明发送数据寄存器已经将数据转移到发送移位寄存器了
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 重定向fput函数,通过串口输出
int fputc(int ch, FILE *f) // 这个是fputc的函数原型,不能变
{
// Serial_SendByte是自己写的输出到串口的函数
Serial_SendByte(ch);
return ch;
}
int main()
{
Serial_Init();
// 直接使用printf函数打印数据到串口
printf("Send=%d\r\n", 666);
while(1)
{
}
}
使用sprintf函数重定向不同的串口
printf函数重定向串口1后,串口2、串口3等等就用不了printf函数打印数据了。因此需要使用到sprintf函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include <stdio.h>
/**
* @brief 配置USART1的发送功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx; // 开启USART的发送
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 发送字节
* @param Data: 要发送的字节
* @retval 无
*/
void Serial_SendByte(uint8_t Data)
{
// 写入数据到发送数据寄存器中
USART_SendData(USART1, Data);
// 获取USART的TXE标志位是否为1。TXE=1说明发送数据寄存器已经将数据转移到发送移位寄存器了
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
/**
* @brief 发送字符串
* @param string: 要发送的字符串
* @retval 无
*/
void Serial_SendString(char *string)
{
for(uint8_t i = 0;string[i] != 0; i++)
{
Serial_SendByte(string[i]);
}
}
// 重定向fput函数,通过串口输出
int fputc(int ch, FILE *f) // 这个是fputc的函数原型,不能变
{
// Serial_SendByte是自己写的输出到串口的函数
Serial_SendByte(ch);
return ch;
}
int main()
{
Serial_Init();
// 定义一个足够长的字符串
char string[100];
// 通过sprintf将printf函数中的数据打印到定义的字符串String中
sprintf(string, "num=%d\r\n", 777);
// 将字符串发送出去
Serial_SendString(string);
while(1)
{
}
}
封装sprintf
添加头文件stdarg
#include <stdarg.h>v
封装代码
void Serial_printf(char *format, ...)
{
char string[100];
va_list arg;
va_start(arg, format);
vsprintf(string, format, arg);
va_end(arg);
Serial_SendString(string);
}
示例代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include <stdio.h>
#include <stdarg.h>
/**
* @brief 配置USART1的发送功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx; // 开启USART的发送
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 发送字节
* @param Data: 要发送的字节
* @retval 无
*/
void Serial_SendByte(uint8_t Data)
{
// 写入数据到发送数据寄存器中
USART_SendData(USART1, Data);
// 获取USART的TXE标志位是否为1。TXE=1说明发送数据寄存器已经将数据转移到发送移位寄存器了
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
/**
* @brief 发送字符串
* @param string: 要发送的字符串
* @retval 无
*/
void Serial_SendString(char *string)
{
for(uint8_t i = 0;string[i] != 0; i++)
{
Serial_SendByte(string[i]);
}
}
// 重定向fput函数,通过串口输出
int fputc(int ch, FILE *f) // 这个是fputc的函数原型,不能变
{
// Serial_SendByte是自己写的输出到串口的函数
Serial_SendByte(ch);
return ch;
}
void Serial_printf(char *format, ...)
{
char string[100];
va_list arg;
va_start(arg, format);
vsprintf(string, format, arg);
va_end(arg);
Serial_SendString(string);
}
int main()
{
Serial_Init();
Serial_printf("num=%d\r\n", 888);
while(1)
{
}
}
utf-8格式下使用printf打印中文字符串写不报错
串口接收和发送数据
用到的函数
// 开启或关闭USART的中断
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
// 接收数据。读USART的DR寄存器
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
// 获取指定的USART相关中断标志位的状态
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
// 清除指定的USART相关的中断标志位
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
接线图
使用查询的方式
示例代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include <stdio.h>
#include <stdarg.h>
/**
* @brief 配置USART1的发送和接收功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 根据引脚图可知USART1的RX端是PA10
// 初始化PA10为上拉输入
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 开启USART的发送和接收功能
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
int main()
{
Serial_Init();
OLED_Init();
while(1)
{
// RXNE=1:读数据寄存器非空
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET)
{
// 获取USART_DR寄存器的值
OLED_ShowHexNum(1,1,USART_ReceiveData(USART1),5);
}
}
}
使用中断的方式
#include "stm32f10x.h" // Device header
#include "OLED.h"
// 接收串口发送来的数据
uint16_t receive;
/**
* @brief 配置USART1的发送和接收功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启USART和GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 根据引脚图可知USART1的TX端是PA9
// 初始化PA9为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 根据引脚图可知USART1的RX端是PA10
// 初始化PA10为上拉输入
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1的发送和接收数据
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; // 波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 开启USART的发送和接收功能
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 一位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 一帧8个数据位
USART_Init(USART1, &USART_InitStruct);
// 开启USART1的RXNE的中断通道
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// NVIC优先级分组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 联通USART1的NVIC的通道
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; // 选择USART1的中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 开启通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 响应优先级
NVIC_Init(&NVIC_InitStruct);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
int main()
{
Serial_Init();
OLED_Init();
while(1)
{
OLED_ShowHexNum(1, 1, receive, 5);
}
}
// USART1的中断函数
void USART1_IRQHandler()
{
// 检测USART1的RXNE是否产生中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
// 读取USART_DR寄存器的值
receive = USART_ReceiveData(USART1);
// 清除中断标志位,在读取USART_DR寄存器的值后硬件会自动清除,所以这条代码可以不写
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
USART串口数据包
首先数据包的作用是把一个个单独的数据给打包起来,方便我们进行多字节的数据通信
数据包的任务,就是把属于同一批的数据进行打包和分割,方便接收方进行识别
HEX数据包
0xff为包头,0xfe为包尾
固定包长:每一个数据包的包头和包尾之间的字节数是一样的
只要包头包尾对齐,就可以避免载荷数据与包头包尾混淆
可以不写包尾,只使用包头
可变包长:每一个数据包的包头和包尾之间的字节数是不一样的
文本数据包
文本数据包和 HEX 数据包,就分别对应了文本模式和 HEX 这两种模式。在 HEX 数据包里面,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的,就是文本格式,但实际上,每个文本字符背后,其实都还是一个字节的 HEX 数据。
文本和HEX数据包比较
HEX 数据包:
- 优点:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据。比如一些使用串口通信的陀螺仪、温湿度传感器。
- 缺点:灵活性不足、载荷容易和包头包尾重复。
文本数据包
- 优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,比如蓝牙模块常用的 AT 指令,CNC 和 3D 打印机常用的 G 代码,都是文本数据包的格式。
- 缺点:解析效率低。比如发送一个数 100,HEX 数据包就是一个字节 100,完事,文本数据包就得是 3 个字节的字符,‘1’,‘0’,‘0’,收到之后还要把字符转换成数据,才能得到 100。所以说,我们需要根据实际场景来选择和设计数据包格式。
接收数据包
HEX数据包接收
先定义一个转态位S。当S=0时,等待包头数据;当S=1时接收传输的数据;当S=2时等待包尾数据
等待包头:接收到字节:0xff后,判断接收到了包头数据,令S=1,准备接收数据
接收数据:
- 如果是固定4个包长,还需要定义一个变量num来记录接收的字节个数,每接收一个字节num++,当num=4后全部数据接收完成,并保存这四个数据,后面再接收到的其他数据都不会进行保存。令S=2,等待包尾
- 如果是可变包长,就不需要定义num记录接收到的字节,只需要对接收到的所以字节进行判断是否等于包尾:0xfe,如果不等于则一直保存接收到的字节,直到接收到0xfe,令S=2
等待包尾:接收到数据0xfe后,判断接收到了包尾,数据包接收完毕,令S=0,重新准备开始接收下一个数据包
文本数据包接收
文本数据包接收与HEX数据包接收的原理差不多,只不过文本数据包的包尾习惯性使用\r\n,所以需要判断两次包尾才能确定数据接收结束
案例2
HEX数据包的收发
固定包长
接线图
示例代码
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Delay.h"
// 接收串口发送来的数据
uint8_t Byte;
// 接收标志位
uint8_t Receive_Flag;
// 保存接收到的数据包
char Receive_Data[4];
// 要发送出去的数据包,可以自定义数据
char Send_Data[4] = {0x11, 0x22, 0x33, 0x44};
// 初始化按键
void Key_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// PB1上拉输入
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
}
// 判断按键是否被按下
uint8_t Key_Inspect()
{
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
{
// 消抖
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET);
Delay_ms(20);
// 返回1说明按键被按下
return 1;
}
return 0;
}
/**
* @brief 配置USART1的发送和接收功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启GPIOA和USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 初始化PA9,USART1的Tx发送端
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化PA9,USART1的Rx接收端
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化USART1
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
// 开启USART1的RXNE中断
USART_ITConfig(USART1,USART_IT_RXNE, ENABLE);
// NVIC优先级分组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化USART1的NVIC通道
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
// 开启USART1
USART_Cmd(USART1, ENABLE);
}
// 发送一个字节
void Send_Byte(uint8_t ByteData)
{
// 往USART_DR中写入数据
USART_SendData(USART1, ByteData);
// TXE=RESET表示发送数据寄存器中的数据还没有转移到发送移位寄存器中
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 显示保存的数据包
void Receive_DataPacket()
{
// 接收数据包标志位为1说明有新的数据包接收成功了
if(Receive_Flag == 1)
{
// 将标志位清零
Receive_Flag = 0;
// 显示数据包中的数据
for(uint8_t i = 0;i < 4; i++)
{
OLED_ShowHexNum(i + 1, 1, Receive_Data[i], 5);
}
}
}
// 发送数据包
void Send_DataPacket()
{
// 包头
Send_Byte(0xff);
// 有效载荷
for(uint8_t i = 0; i < 4; i++)
{
// 发送前对数组内的数据自增,可以让每次发送的数据包都不一样
Send_Byte(Send_Data[i]++);
}
// 包尾
Send_Byte(0xfe);
}
int main()
{
Serial_Init();
OLED_Init();
Key_Init();
while(1)
{
// 显示接收到的数据包
Receive_DataPacket();
// 如果按键按下则发送数据包
if(Key_Inspect())
{
Send_DataPacket();
}
}
}
// USART1的中断函数
void USART1_IRQHandler()
{
// 记录当前数据包的状态
static uint8_t Status = 0;
// 记录保存的字节数据个数
static uint8_t PData;
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
// 读取字节
Byte = USART_ReceiveData(USART1);
// Status=0,等待包头0xff
if(Status == 0 && Byte == 0xff)
{
// 接收到了包头,Status置1准备保存数据
Status = 1;
// 初始化记录保存的字节个数为0
PData = 0;
}
// Status=1,接收字节
else if(Status == 1)
{
// 将接收到的字节放到数组中保存
Receive_Data[PData] = Byte;
// 接收到一个数据就自增一次
PData++;
// 固定包长4,收到4个数据后等待包尾,Status=2
if(PData >=4)
{
Status = 2;
}
}
// 等待包尾0xfe
else if(Status == 2 && Byte == 0xfe)
{
// 成功接收到包尾,Status=0,准备接收下一个数据包
Status = 0;
// 接收数据包标志位置1,成功接收到了数据
Receive_Flag = 1;
}
}
}
文本数据包的收发
可变包长
这里会使用一个的string.h的函数
接线图
示例代码
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Delay.h"
#include <string.h>
// 接收串口发送来的数据
uint8_t Byte;
// 接收标志位
uint8_t Receive_Flag;
// 保存接收到的数据包,可变包长,可以把范围给大一点
char Receive_String[100];
// 要发送出去的数据包,可以自定义数据
char Send_String[] = "Hello world";
// 初始化LED在PA1上
void LED_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// PA1推挽输出
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 熄灭LED
void LED_OFF()
{
GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
}
// 点亮LED
void LED_ON()
{
GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
}
/**
* @brief 配置USART1的发送和接收功能,波特率:9600
* @param 无
* @retval 无
*/
void Serial_Init()
{
// 开启GPIOA和USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 初始化PA9,USART1的Tx发送端
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化PA9,USART1的Rx接收端
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化USART1
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStruct);
// 开启USART1的RXNE中断
USART_ITConfig(USART1,USART_IT_RXNE, ENABLE);
// NVIC优先级分组2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化USART1的NVIC通道
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
// 开启USART1
USART_Cmd(USART1, ENABLE);
}
// 发送一个字节
void Send_Byte(uint8_t ByteData)
{
// 往USART_DR中写入数据
USART_SendData(USART1, ByteData);
// TXE=RESET表示发送数据寄存器中的数据还没有转移到发送移位寄存器中
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送数据包
void Send_DataPacket()
{
// 包头
Send_Byte('@');
// 有效载荷
for(uint8_t i = 0; Send_String[i] != '\0'; i++)
{
// 发送前对数组内的数据自增,可以让每次发送的数据包都不一样
Send_Byte(Send_String[i]);
}
// 包尾
Send_Byte('\r');
Send_Byte('\n');
}
int main()
{
Serial_Init();
OLED_Init();
LED_Init();
Send_DataPacket();
while(1)
{
if(Receive_Flag)
{
// 标志位清零
Receive_Flag = 0;
/*将收到的数据包与预设的指令对比,以此决定将要执行的操作*/
// strcmp函数用于比较字符串的大小,返回值为0时两个字符串相等,需要添加头文件:string.h
if(strcmp(Receive_String, "LED_ON") == 0)
{
LED_ON();
OLED_ShowString(1, 1, " ");
OLED_ShowString(1,1,"LED_ON");
}
else if(strcmp(Receive_String, "LED_OFF") == 0)
{
LED_OFF();
OLED_ShowString(1, 1, " ");
OLED_ShowString(1,1,"LED_OFF");
}
else
{
OLED_ShowString(1, 1, " ");
OLED_ShowString(1,1,"ERROR");
}
}
}
}
// USART1的中断函数
void USART1_IRQHandler()
{
// 记录当前数据包的状态
static uint8_t Status = 0;
// 记录保存的字节数据个数
static uint8_t PData;
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
// 读取字节
Byte = USART_ReceiveData(USART1);
// Status=0,等待包头字符@
if(Status == 0 && Byte == '@')
{
// 接收到了包头,Status置1准备保存数据
Status = 1;
// 初始化记录保存的字节个数为0
PData = 0;
}
// Status=1,接收字节
else if(Status == 1)
{
// 判断是否出现第一个包尾
if(Byte == '\r')
{
Status = 2;
}
else
{
// 将接收到的字节放到数组中保存
Receive_String[PData] = Byte;
// 接收到一个数据就自增一次
PData++;
}
}
// 等待第二个包尾
else if(Status == 2 && Byte == '\n')
{
// 成功接收到包尾,Status=0,准备接收下一个数据包
Status = 0;
// 接收数据包标志位置1,成功接收到了数据
Receive_Flag = 1;
// 字符串数据需要在后面加上'\0'结束符
Receive_String[PData] = '\0';
}
}
}