一、串口通信
- 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
- 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力
1. 硬件电路
- 简单双向串口通信有两根通信线(发送端TX和接收端RX)
- TX与RX要交叉连接
- 当只需单向的数据传输时,可以只接一根通信线
- 当电平标准不一致时,需要加电平转换芯片
一个设备使用TX发送高低电平,另一个设备使用RX接收高低电平。
因为STM32是3.3V,所以线路对地是3.3V,就代表发送了逻辑1,线路对地为0V,就代表了发送逻辑0。
2. 串口参数及时序
- 波特率:串口通信的速率(串口一般使用异步通信,需要双方约定一个通信速率)
- 起始位:标志一个数据帧的开始,固定为低电平
- 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
- 校验位:用于数据验证,根据数据位计算得来(奇校验:加入需要传输的字节为0000 1111,则在最后一位补一个1为 5个1为奇数,如果传输的字节为0000 0111,则最后一位补一个0,3个1为奇数,最后在接收放验证个数是不是奇数。偶校验同理,但奇偶校验只能保证一定程度上的数据校验)
- 停止位:用于数据帧间隔,固定为高电平
串口中每一个字节都装载在一个数据帧里,每个数据帧都由起始位,数据位,停止位组成。左图数据位有8位,代表一个字节8位,右图数据位有9位,最后一个为奇偶校验位。
没用工作的时候都是空闲为高电位,开始工作的时候有一个起始位为低电平,产生下降沿,来告诉接收设备,我要发数据了。同理,一个字节数据发送完成后,必须要有一个停止位。
二、USART
- USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
- USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
- 自带波特率发生器,最高达4.5Mbits/s
- 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
- 可选校验位(无校验/奇校验/偶校验)
- 支持同步模式、硬件流控制(在硬件电路上会多出一根线,如果B没准备好接收,就置高电平,如果准备好了就置低电平,A会根据电平来发送数据)、DMA、智能卡、IrDA、LIN
- STM32F103C8T6 USART资源: USART1、 USART2、 USART3
1. USART框图
发送数据寄存器:排队等待打饭;发送移位寄存器:正在打饭;
接收移位寄存器:拿到了饭;接收数据寄存器:开始吃饭。
2. USART基本结构
当数据由数据寄存器转到移位寄存器时,会置一个TXE的标志位,判断这个标志位就可以知道是否可以写入下一个数据;
同理,移位寄存器转到数据寄存器的时,会置一个RXNE的标志位,检查这个标志位就可以知道是否收到数据了。
3. 数据帧
停止位就是停止的位数,一般常用于一位停止位。
4. 起始位侦测
当输入电路侦测到一个数据帧的起始位后,就会以波特率的帧率,连续采样一帧数据。同时,从起始位开始,采样位置就要对其到位的正中间,只要第一位对齐了,后面就肯定对齐。
首先输入的部分电路对采样时钟进行了细分,它会以波特率16倍频率进行采样,也就是一位的时间里,进行16次采样。
最开始,空闲状态高电平采样一直为1,在某个位置突然采样到0,那么说明在这两次采样之间出现了下降沿,如果没有任何噪声,就应该是起始位。在起始位进行连续16次采样。
5. 数据采样
从1到16是一个数据位的时间长度,在一个数据位,有16个采样时钟,由于起始位侦测到已经对齐了采样的时钟,所有就在8、9、10次开始采样,为了保证数据准确性性,是连续采样3次。由于噪声的影响,它是由2:1规则来,2次为1则为1,两次为0则为0。这种情况下,噪声标志位NE也会置1,告诉我,虽然我收到数据了,但有噪声,需要考虑使用。
6. 波特率发生器(寄存器)
- 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
- 计算公式:波特率 = fPCLK2/1 / (16 * DIV) (为什么有16,因为一个数据位,有16个采样时钟)
- 如果是使用库函数,库函数自动帮我们算好BRR
7. HEX数据包
数据包作用:把一个个单独的数据打包起来,方便进行多字节的数据通信。
比如:陀螺仪传感器需要用串口发送数据STM32。
固定包长,含包头包尾。
这里定义0xFF为包头,0xFE为包尾。
可变包长,含包头包尾。
如果载荷会出现包头包尾重复的情况,最好选择固定包长,避免接收错误;如果载荷不会和包头包尾重复,可以选择可变包长。
8. 文本数据包
固定包长,含包头包尾。
这里以@作为包头,以\r\n作为包尾。
可变包长,含包头包尾。
9. HEX数据包流程图
这里可以利用状态机。先定义3个状态,第一个状态是等待包头,第二个状态是接收数据,第三个状态时等待包尾。
最开始S=0,收到一个数据进入中断,根据S=0进入第一个状态的程序,判断数据是不是包头FF,如果时FF,则代表收到包头,之后置S=1,退出中断结束。这样下次进中断,就可以根据S=1进行接收数据程序。那在第一个状态,如果收到的不是FF,证明数据包没有对齐, 所以包头仍为0。下次进中断还是进入包头,直到出现FF,才进入下一状态。如果出现了FF,就可以转移到接收数据状态,这时再收到数据,就直接把它存在数组中,另外再用一个变量,记录收了多少个数据,如果没有收够4个数据,就一直是接收状态,如果收够了,就置S=2,下次中断时,就可以进去下一个状态。最后一个等待包尾,判断是不是FE,如果是则置S=0,回到最初的状态开始下一次轮回,如果不是就一直等待FE。
10. 文本数据包流程图
这个与上面数据包流程图类似,但这个是可变包长,需要在S=1时接收数据并且等待包尾,需要时刻监视是不是该收包尾。
建议
一般情况下,HEX数据包一般多用于传输各种传感器的每个独立数据,比如陀螺仪的X,Y,Z轴数据,温湿度数据等。那文本数据包一般可以利用发送文字到串口,实现相应功能。
二、代码部分
1. 串口发送和接收
#include "Bsp_Serial.h"
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 1.时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; // 2.配置GPIO
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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure; // 3.配置USART
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; // 停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 传输位长
USART_Init(USART1, &USART_InitStructure);
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;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE); // 4.开启串口1
}
/* 发送一个字节 */
void Serial_SendByte(uint8_t Byte) // 这里只传8位,用uint_8可以。要传9位的话就得是uint_16的类型了
{
USART_SendData(USART1, Byte);
while ((USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET));
}
/* 发送一个数组 */
void Serial_SendArray(uint8_t *Array, uint32_t Length)
{
for (uint16_t i = 0; i < Length; i++)
{
Serial_SendByte(Array[i]);
}
}
/* 发送字符串 */
void Serial_SentString(char *String)
{
for (uint8_t i = 0; String[i] != '\0'; i++)
{
Serial_SendByte(String[i]);
}
}
/* 次方 */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
/* 发送十进制数字 */
void Serial_SentNumer(uint32_t Number, uint8_t Length)
{
for (uint8_t i = 0; i < Length; i++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
/* 移植printf到串口 fputc是printf的底层函数,利用这个函数重定位到串口 */
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
/* 封装sprintf */
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg; // va_list是一个类型名,arg是变量名
va_start(arg, format); // va_start是从format位置开始接收参数表,放在arg里
vsprintf(String, format, arg); // 打印位置是String,格式化字符串是format,参数表是arg
va_end(arg); // 释放参数表
Serial_SentString(String);
}
/* 检测RXNE标志位 */
/*
void Serial_RXNE_Flag(uint8_t RxData)
{
if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET)
{
RxData = USART_ReceiveData(USART1);
OLED_ShowHexNum(1, 1, RxData, 2);
}
}
*/
/* 中断接收封装 */
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
/* 中断变量封装 */
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
/* USART1中断函数 */
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == 1)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
2. 串口发送HEX数据包
#include "Bsp_Serial.h"
// uint8_t Serial_RxData;
uint8_t Serial_RxPacket[4];
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxFlag = 0;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 1.时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; // 2.配置GPIO
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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure; // 3.配置USART
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; // 停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 传输位长
USART_Init(USART1, &USART_InitStructure);
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;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE); // 4.开启串口1
}
/* 中断接收封装 */
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
/* 每次发送HEX数据包加上包首和包尾 */
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
/* 中断变量封装 */
/*
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
*/
/* USART1中断函数 */
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; // static类似于全局变量,只初始化一次,但与全局变量不同的是,静态变量只在本函数使用
static uint8_t pRxPacket = 0; // 指示接收到第几个了
if (USART_GetITStatus(USART1, USART_IT_RXNE) == 1)
{
uint8_t RxData = USART_ReceiveData(USART1);
switch (RxState) // 这里使用了状态机
{
case 0:
{
if (RxData == 0xFF)
{
RxState = 1;
pRxPacket = 0;
}
}
break;
case 1:
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
if (pRxPacket >= 4)
{
RxState = 2;
}
}
case 2:
{
if (RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
3. 串口发送文本数据包
#include "Bsp_Serial.h"
// uint8_t Serial_RxData;
char Serial_RxPacket_Char[100]; // 接收文本数据包变量
uint8_t Serial_RxPacket[4]; // 接收HEX数据包变量
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxFlag = 0;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 1.时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; // 2.配置GPIO
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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure; // 3.配置USART
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; // 停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 传输位长
USART_Init(USART1, &USART_InitStructure);
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;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE); // 4.开启串口1
}
/* 文字数据包状态机 */
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; // static类似于全局变量,只初始化一次,但与全局变量不同的是,静态变量只在本函数使用
static uint8_t pRxPacket = 0; // 指示接收到第几个了
if (USART_GetITStatus(USART1, USART_IT_RXNE) == 1)
{
uint8_t RxData = USART_ReceiveData(USART1);
switch (RxState)
{
case 0:
{
if (RxData == '@' && Serial_RxFlag == 0) // 如果两个条件满足才接受,如果没有Serial_RxFlag,就怕到时候传输数据太快,错位。
{
RxState = 1;
pRxPacket = 0;
}
}
break;
case 1:
{
if (RxData == '\r')
{
RxState = 2;
}
else
{
Serial_RxPacket_Char[pRxPacket] = RxData;
pRxPacket ++;
}
}
break;
case 2:
{
if (RxData == '\n')
{
RxState = 0;
Serial_RxPacket_Char[pRxPacket] = '\0';
Serial_RxFlag = 1;
}
}
break;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}