USART串口通信
USART串口通信
通信概述
通信目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
USART/I2C/SPI:需要接GND引脚才能正常通信
UART:单片机和电脑/单片机和模块点对点通信
I2C:一个单片机主控、外挂多个被动传感器/存储器 -> 一主多从
SPI:高速通信 -> 一主多从
CAN:多个主控互相通信
※双工
全双工:可以同时互相通信
半双工:只能同时走一个方向 一个方向走完了才能走另一个方向
单工:只能走一边
※时钟:我如何知道信号是 10 还是 1100
异步:需要确定起始点,帧数等
同步
※电平:两个设备的电压是否相同
单端:不相同
差分:相同 抗干扰能力强
※设备:
点对点:一个设备对一个设备
多设备:有总线的区别 需要有寻址的过程来确定通信对象
串口通信:实现两个设备的互相通信(转协议)
USB转串口 CH340
CH340G 实现两个芯片的通信
串口转蓝牙
硬件电路
发送端TX和接收端RX
(1)简单双向串口通信有两根通信线
(2)TX与RX要交叉连接
(3)只需单向的数据传输时,可以只接一根通信线
(4)电平标准不一致时:需要加电平转换芯片
电平标准:数据1和数据0的表达方式
TTL电平:【常用】+3.3V或+5V表示1,0V表示0
RS232电平:-3 ~ -15V表示1,+3 ~ +15V表示0
一般大型机器使用
RS485电平【差分信号】:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0
串口参数及时序
波特率/比特率:串口通信速率
——————————————————————
【科普】
波特率:单位时间内传送二进制数据的位数 单位bps(位/秒)
比特率:单位时间内传送二进制有效数据的位数 单位bps(位/秒)
比特率 = 波特率 x 单个调制状态对应的二进制位数
eg
1个起始位,8个数据位、1个校验位、2个终止位,每秒传送120个字符。
->有效数据为8位,一帧包含1+8+1+2=12位
故波特率为:120*(1+8+1+2)=1440 bps=1440波特
又因为有效数据位为8位,而传送一个字符需1+8+1+2=12位
故比特率为:1440 * (8/12)=960 bps
(比特率还可以直接求:8 * 120=960 bps)
——————————————————————
空闲状态:引脚必须设高电平
起始位:标志一个数据帧的开始,固定为低电平
数据位:数据帧的有效载荷,1为高电平,0为低电平
低位先行:0x55->01010101->低位先行->故波形:10101010
校验位:用于数据验证,根据数据位计算得来
无校验:一般使用 奇校验和偶校验 在1个信号有2个及以上问题时精度较低
奇校验:看1的个数 如果不为奇数->认为是信号传输有问题
数据位00001111 校验位1 -> 5个1
数据位00001110 校验位0 -> 3个1
偶校验:看1的个数是否为偶数
※更高的检出率:CRC校验
停止位:用于数据帧间隔,固定为高电平
举一些栗子
改变波特率 可以看到时间不一样
偶校验
连着两个0x55数据
中间有停止
USART简介
USART通用同步收发器(很少使用)/UART通用异步收发器
【常用参数】
波特率 9600/115200
数据位长度 常用8
停止位长度 常用1
校验位 常用 无校验
【STM32F103C8T6 USART资源】
APB2:USART1
APB1:USART2、USART3
USART基本结构
USART框图
(1)接受移位寄存器(RDR)没准备好接收:高电平 准备好:低电平
TX接收反馈信号 准备好了再发数据
(2)判断发送状态和接收状态的必要标志位
TXE:发送寄存器非空
RXNE:接受寄存器非空
(3)波特率发生器:分频器
利用APB时钟进行分频,得到发送和接收移位的时钟
数据帧
最好选择9位字长 有校验
或者8位字长 无校验
【检测噪声】:NE标志位
起始位侦测
数据采样:1个数据位16个采样时钟
波特率发生器
波特率由波特率寄存器BRR里的DIV确定
计算公式:波特率 = fPCLK2/1 / (16 * DIV)
CH340原理图
GND与单片机互接
寄存器经典分类【基本每个外设都有】
SR 状态寄存器 : 存放各种标志位
DR 数据寄存器 : 存放关键数据
CR 配置寄存器 : 存放配置参数
USART串口发送
常用库函数
void USART_DeInit(USART_TypeDef* USARTx);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);//发送数据
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);//接受数据
【初始化】
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;
//将PA9引脚初始化为复用推挽输出【有外设】
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*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_Tx 发送模式
USART_Mode_Rx 接收模式
USART_InitStructure.USART_Parity = USART_Parity_No;//奇偶校验,不需要
USART_Parity_No 无校验
USART_Parity_Odd 奇校验
USART_Parity_Even 偶校验
USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位,选择1位
USART_StopBits_1
USART_StopBits_0_5
USART_StopBits_2
USART_StopBits_1_5
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长,选择8位
USART_WordLength_8b
USART_WordLength_9b
USART_Init(USART1, &USART_InitStructure);
//将结构体变量交给USART_Init,配置USART1
/*USART使能*/
USART_Cmd(USART1, ENABLE);//使能USART1,串口开始运行
}
【接收模式】
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_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
串口中断
/*中断输出配置*/
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_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
【串口发送字节】:发送之后要等待标志位!
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
//将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
各种发送
//【发送数组】
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)//遍历数组
{
Serial_SendByte(Array[i]);
}
// 【发送字符串】
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]);
}
}
// 【发送数据】
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;
}
封装Serial_printf
void Serial_Printf(char *format, ...)//可变参数
{
char String[100];//定义字符数组
va_list arg;//定义可变参数列表数据类型的变量arg
va_start(arg, format);//从format开始,接收参数列表到arg变量
vsprintf(String, format, arg);
//使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg);//结束变量arg
Serial_SendString(String);//串口发送字符数组(字符串)
}
USART串口接收
配置串口中断!
【USART有专门的中断】
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收数据的中断
当USART_IT_RXNE为1时向NVIC申请中断
其余和NVIC相同
/*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线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure);//将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE);//使能USART1,串口开始运行
当Rx接收到数据后进入中断
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
//判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1);
//读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1;
//置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
//清除USART1的RXNE标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
【检测标志位】 是否为1,如为1清零标志位
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函数
while (1)
{
if (Serial_GetRxFlag() == 1)//检查串口接收数据的标志位
{
//先进USART中断!!!
RxData = Serial_GetRxData();//获取串口接收的数据
Serial_SendByte(RxData);//串口将收到的数据回传回去,用于测试
OLED_ShowHexNum(1, 8, RxData, 2);//显示串口接收的数据
}
}
如果要显示汉字呢?
※MicroLIB是Keil为嵌入式平台优化的精简库
UTF-8不乱码
小锤子C/C++
内写上
-no-multibyte-chars
Serial_Printf("你好,世界");
切换GB2312编码
右上角打开配置(小扳手)
然后把汉字删掉,把文件关掉再打开->字体变为宋体
串口助手选GBK编码
USART串口数据包
数据模式
ASCII码表
HEX数据包
包头 0xFF
包尾 0xFE
固定包长
可变包长
【优点】
传输直接,解析数据简单
适合模块发送原始数据
eg 使用串口通信的陀螺仪 湿温度传感器
【缺点】
灵活性不足
载荷容易和包头包尾重复
文本数据包
包头 '@'
包尾 '\r''\n'
固定包长
可变包长
【优点】
数据直观易理解,灵活
适合输入指令进行人机交互的场景
eg 蓝牙模块常用AT指令
接收数据包
S:状态机
记住不同状态
在不同状态执行不同操作,进行状态合理转移
接收HEX数据包:固定包长4
接收文本数据包:可变包长
uint8_t Serial_TxPacket[4]; //定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4]; //定义接收数据包数组
uint8_t Serial_RxFlag; //定义接收数据包标志位
.h里写声明全局变量
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
发送HEX数据包
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
串口中断发送数据包
当RXNE为非空时触发
注意静态变量【内存管理】
void USART1_IRQHandler(void)
{
//只能被初始化一次0 具有全局变量的性质但只能在本函数使用:内存管理
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 == 0xFF)//如果数据确实是包头
{
RxState = 1;//置下一个状态
pRxPacket = 0;//数据包的位置归零
}
}
/*当前状态为1,接收数据包数据*/
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket++] = RxData;
//将数据存入数据包数组的指定位置
if (pRxPacket >= 4)//如果收够4个数据
{
RxState = 2;//置下一个状态
}
}
/*当前状态为2,接收数据包包尾*/
else if (RxState == 2)
{
if (RxData == 0xFE)//如果数据确实是包尾部
{
RxState = 0;//状态归0
Serial_RxFlag = 1;//接收数据包标志位置1,成功接收一个数据包
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除标志位
}
}
※一个小问题
Serial_RxPacket数组兼具写入和写出:
数据包之间可能会混在一起
-> 解决方法:在接收部分加入判断
发送文本数据包
※防止处理太快导致的数据包混在一起
分开
【写数据】 处理数据结束后(OLED屏显示后):再将Flag置0
【读数据】 在处理包头时:等待标志位置0再执行
【中断函数】
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
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;
//将数据存入数据包数组的指定位置
}
}
/*当前状态为2,接收数据包第二个包尾*/
else if (RxState == 2)
{
if (RxData == '\n') //如果收到第二个包尾
{
RxState = 0;//状态归0
Serial_RxPacket[pRxPacket] = '\0';
//将收到的字符数据包添加一个字符串结束标志
Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
【主函数】
注意在最后有将Serial_RxFlag
清0
while (1)
{
//会先进中断!
if (Serial_RxFlag == 1)//如果接收到数据包
{
OLED_ShowString(4, 1, " ");
OLED_ShowString(4, 1, Serial_RxPacket);
//OLED清除指定位置,并显示接收到的数据包
/*将收到的数据包与预设的指令对比,以此决定将要执行的操作*/
if (strcmp(Serial_RxPacket, "LED_ON") == 0)
//如果收到LED_ON指令
{
LED1_ON();
Serial_SendString("LED_ON_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LED_ON_OK");
}
else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)
//如果收到LED_OFF指令
{
LED1_OFF();
Serial_SendString("LED_OFF_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "LED_OFF_OK");
}
else //上述所有条件均不满足,即收到了未知指令
{
Serial_SendString("ERROR_COMMAND\r\n");
//串口回传一个字符串ERROR_COMMAND
OLED_ShowString(2, 1, " ");
OLED_ShowString(2, 1, "ERROR_COMMAND");
}
Serial_RxFlag = 0;
//处理完成后,需要将接收数据包标志位清零,否则将无法接收后续数据包
}
}