1、原理
1、USART串口协议

同步:有单独的时钟线,接收方可以在时钟信号的指引下进行采样
异步:双方需要约定一个采样频率,并添加一些帧头帧尾等进行采样位置的对齐
单端:引脚的高低电平都是相对于GND的电压差,需要接GND引脚
差分:抗干扰,靠两个差分引脚的电压差来传输信号,可以不需要GND
点对点:直接传输数据
多设备:需要寻址,来确定通信对象

串口通信TX、RX、GND必须要接
如果设备1、设备2都有独立供电,VCC可以不接
如果其中一个设备没有供电,如设备1为STM32,设备2为蓝牙串口模块,就需要将它们的VCC接在一起,STM32通过这根线向右边的子模块供电

波特率:异步通信通信速率,每秒传输码元的个数、bit/s。高电平表示1,低电平表示0。决定了每个多久发送一位
起始位:串口的空闲状态是高电平,起始位为低电平,通知接收设备这一帧数据要开始了
停止位:用于数据帧间隔,固定为高电平
2、USART串口外设

同步模式多了一个时钟输出,不支持时钟输入,并不支持两个USART之间的通信
波特率发生器:用来配置波特率,相当于分频器,最常用(9600,115200)。
数据位长度:8、9位
停止位长度:在进行连续发送时,帧的间隔
硬件流控制:防止接收方处理慢而导致数据丢失的问题
USART资源:USART1是APB2总线上的设备,USART2、3都是APB1总线上的设备

TDR和RDR占用同一个地址,在程序上表示为一个寄存器,数据寄存器DR,在实际的硬件中,是两个寄存器,一个用于发送,一个用于接收,TDR只写,RDR只读。写操作,数据写道TDR;读操作,数据从RDR中读出
发送端 :把一个字节的数据一位一位地向右移出去(低位先行),对应串口协议的数据位波形。 当硬件检测到写入数据,会检查当前移位寄存器是否有数据正在移位,如果没有该数据会立刻全部被发送到发送移位寄存器,准备发送。当数据从TDR发送到移位寄存器时,会置标志位TEX(TX Empty),TDR为空,TEX置1,就可以在TDR里写入下一个数据了,此时发送移位寄存器中的数据还没有发送出去。当数据移位完成后,新的数据就会再次自动地从TDR转移到发送移位寄存器里。可以保证连续发送数据时,数据帧之间不会有空闲。
接收端: 从高位到低位方向移动,一个字节移位完成之后,这一个字节的数据就会整体移到接收数据寄存器RDR,转移过程中会置标志位RXNE(RX Not Empty),接收数据寄存器非空。RXNE置1时,就可以把数据读走了
硬件流控制:避免发送设备发的太快,接收设备来不及处理,导致丢弃或覆盖
nRTS:接对方的CTS,能接受的时候RTS置低电平,请求对方发送,对方的CTS接收到之后,就可以发送数据;当数据处理不过来,如接收数据寄存器一直没有读,又有数据过来了,RTS置高电平,对方CTS接收到之后,暂停发送数据,直到RTS置低电平。
nCTS:清除发送,用于接收其他设备的nRTS。
SCLK:用于产生同步的时钟信号,配合发送移位寄存器输出,发送寄存器移位一次,同步时钟电平就跳变一个周期,时钟告诉对方,移出去一位数据。只支持输出,不支持输入,两个USART之间不能实现同步的串口通信。用于兼容别的协议,如SPI,和自适应波特率,如接收设备不确定发送设备给的是什么波特率,可以测量时钟周期,计算得出波特率。
唤醒单元:实现串口挂载多设备,一条总线上接多个从设备,每个设备分配一个地址,想跟某个设备通信,就先进行寻址,确定通讯对象。如给设备分配一个地址,当发送指定地址时,此设备唤醒开始工作;没收到地址就保持沉默。
状态寄存器:TEX和RXNE是判断发送状态和接收状态的必要标志位。
USART中断控制:配置中断是否能通向NVIC。
波特率发生器:相当于分频器,对APB时钟进行分频,得到发送和接收移位的时钟。USART1挂载在APB2,PCLK2的时钟,72MHZ;其余USART挂载在APB1,PCLK1的时钟,36MHZ。之后这个时钟会进行分频,除USARTDIV的分频系数,分频完之后再除16。TE为1,发送器使能,RE为1接收器使能。
3、基本结构

4、数据帧

输出 定时翻转TX引脚高低电平;输入要保证采样频率和波特率一致,还要保证每次输入采样的位置正好处于每一位的正中间,这样高低电平读进来才最可靠。
空闲帧、断开帧用于局域网协议
数据长度 :8位(有、无校验)、9位(有、无校验),最好选择9位字长有校验,8位字长无校验。串口传输的数据类型一般为uint8_t。

一个停止位和一个数据位时长一样,一般使用一个停止位
5、输入电路噪声处理

以波特率16倍频率采样,一位的时间进行16次采样。
为了避免噪声影响,接收电路在第一次遇到下降沿之后,第3、5、7次进行采样,第8、9、10次进行采样,这两批采样都至少有2个0。如果只用2个0,会在状态寄存器里置一个NE(Noise Error),噪声标志位。如果少于2个0,电路忽略前面的数据,重新开始捕捉下降沿。
6、数据采样

三次采样中,两次及以上为1,就认为收到了1;两次及以上为0,就认为收到了0。两次时,噪声标志位NE也会置1。
7、波特率发生器

DIV_Mantissa:DIV整数部分(二进制)
DIV_Fraction:DIV小数部分(二进制)
因为它内部还有一个16倍波特率的采样时钟,所以要多除16。
8、数据模式


可以以16进制数和字符的方式进行发送。
数据在线路中传输得形式是16进制数。
2、代码
1、初始化
1、初始化时钟
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
2、初始化GPIO引脚
串口空闲状态是高电平,不使用下拉输入
GPIO输出模式使用复用推挽输出 ,因为在gpio的电路中,是由输出数据寄存器控制,外设无法干预,但使用复用推挽输出后,输出数据寄存器会被断开,由外设控制输出控制
cpp
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);
3、初始化USART
cpp
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate=9600;
USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
//想同时接收和发送可以使用按位或
USART_InitStruct.USART_Mode=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);
USART_Cmd(USART1, ENABLE);
2、发送数据
1、发送一位数据
不需要对TXE手动清零,下一次使用USART_SendData()时,TXE自动清零。TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。对USART_DR 的写操作,将该位清零。
cpp
//串口一次移动8位数据
void Serial_SendData(uint8_t Byte)
{
//数据写入发送数据寄存器TDR
USART_SendData(USART1, Byte);
//等待TDR中的数据转移到移位寄存器
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}
2、发送一个数组
cpp
void Serial_SendArray(uint8_t *Array, uint16_t Lenth)
{
uint16_t i=0;
for(i=0;i<Lenth;i++)
{
Serial_SendData(Array[i]);
}
}
3、发送一个字符串
cpp
void Serial_SendString(char* string)
{
uint16_t i;
for(i=0;string[i]!='\0';i++)
{
Serial_SendData(string[i]);
}
}
4、发送一个数字
一位一位地发送
cpp
uint32_t Serial_pow(uint32_t Num, uint16_t t)
{
uint32_t result=1;
while(t--)
{
result*=Num;
}
return result;
}
void Serial_SendNum(uint32_t Num, uint8_t Lenth)
{
uint8_t i;
for(i=0;i<Lenth;i++)
{
//Lenth-i-1 表示第一位数
Serial_SendData(Num/Serial_pow(10,Lenth-i-1)%10+'0');
}
5、重定向printf到串口
在工程设置->target里把Use MicroLib勾选上
cpp
#include <stdio.h>
//fputc是printf的底层,把fput重定向到串口,printf就重定向到串口
int fputc(int ch, FILE *f)
{
Serial_SendData(ch);
return ch;
}
3、接收数据
1、初始化RX
cpp
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);
USART_InitStruct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
2、串口接收
1、查询,在主函数里不断判断RXNE标志位
cpp
while(1)
{
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE))
{
RxData=USART_ReceiveData(USART1);
}
OLED_ShowHexNum(1,1,RxData,4);
}
2、中断
开启RXNE标志位到NVIC的输出,RXNE一旦置1,就会向NVIC申请中断
cpp
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//分配抢占优先级和响应优先级个数
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
//指定中断通道
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//指定该通道是否为抢占优先级或响应优先级
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;);
cpp
//暂存数据,标志位
uint8_t RxData;
uint8_t RxFlag;
uint8_t Serial_GetRxData()
{
return RxData;
}
//每次获取标志位后清零
uint8_t Serial_GetRxFlag()
{
if(RxFlag==1)
{
RxFlag=0;
return 1;
}
else
{
return 0;
}
}
//收到USART的中断后,将标志位暂存
void USART1_IRQHandler()
{
RxData = USART_ReceiveData(USART1);
RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
4、收发一个数据包
1、HEX数据包
传输直接,解析数据简单,适合模块发送原始的数据如陀螺仪、温湿度传感器

2、文本数据包
数据直观易理解,灵活,适合输入指令进行人机交互的场合,如蓝牙模块AT指令

3、HEX数据包收发

cpp
//只存载荷数据
uint8_t TxPacket[4];
uint8_t RxPacket[4];
uint8_t RxFlag;
中断函数内,用状态机表示状态
cpp
void USART1_IRQHandler()
{
static uint8_t RxState=0;
static uint8_t Rxnum=0;
//每次从串口中接收一个字节
uint8_t RxData = USART_ReceiveData(USART1);
switch(RxState)
{
case 0:
//收到包头,进入转移状态
if(RxData==0xFF)
{
RxState=1;
}
break;
case 1:
RxPacket[Rxnum]=RxData;
Rxnum++;
if(Rxnum>=4)
{
Rxnum=0;
RxState=2;
}
break;
case 2:
if(RxData==0xFE)
{
RxState=0;
//全部接收到,置接收标志位
RxFlag=1;
}
break;
default:
break;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
4、文本数据包接收

cpp
void USART1_IRQHandler()
{
static uint8_t RxState=0;
static uint8_t Rxnum=0;
//每次从串口中接收一个字节
uint8_t RxData = USART_ReceiveData(USART1);
switch(RxState)
{
case 0:
//收到包头,进入转移状态
//同时避免发送太快,第一组数据还没处理完就来第二组的情况
if(RxData=='@' && RxFlag==0)
{
RxState=1;
Rxnum=0;
}
break;
case 1:
//载荷字符数量不确定,先判断是不是包尾
if(RxData=='\r')
{
RxState=2;
}
else
{
RxPacket[Rxnum]=RxData;
Rxnum++;
}
break;
case 2:
//等待第二个包尾
if(RxData=='\n')
{
RxState=0;
//全部接收到,置接收标志位
RxFlag=1;
//字符数组最后,添加字符串结束标志位
RxPacket[Rxnum]='\0';
}
break;
default:
break;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
cpp
while(1)
{
if(Serial_GetRxFlag()==1)
{
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,RxPacket);
//判断传入文本是否和目标命令匹配
if(strcmp(RxPacket, "LED_ON")==0)
{
LED1_ON();
Serial_SendString("LED1_ON_OK\r\n");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"LED1_ON_OK\r\n");
}
else if(strcmp(RxPacket, "LED_OFF")==0)
{
LED1_OFF();
Serial_SendString("LED1_ON_OFF\r\n");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"LED1_ON_OFF\r\n");
}
else
{
Serial_SendString("CommandError\r\n");
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,"CommandError\r\n");
}
}
}