串口通讯
串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式,它简单便捷,并且大部分电子设备都支持该通讯方式,在调试设备时经常使用该通讯方式输出调试信息。
串口协议物理层:
TTL电平:
逻辑1:2.4V ~ 5V
逻辑0: 0 ~ 0.5V
RS232电平:
逻辑1:-15V ~ -3V
逻辑0: +3 ~ +15V
串口协议协议层:
串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方
的数据包格式要约定一致才能正常收发数据。
本节主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号 ,所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码,下图为串口数据包的基本组成,其中用虚线分开的每一格就是代表一个码元。常见的波特率为9600,115200 等。
STM32 芯片具有多个 USART 外设用于串口通讯,它是 Universal Synchronous Asynchronous Receiver
and Transmitter 的缩写,即通用同步异步收发器可以灵活地与外部设备进行全双工数据交换。有别于 USART,它还有具有 UART 外设 (Universal Asynchronous Receiver and Transmitter),它是在USART 基础上裁剪掉了同步通信功能,只有异步通信。简单区分同步和异步就是看通信时需不
需要对外提供时钟输出,我们平时用的串口通信基本都是 UART。
功能引脚
TX -> 发送数据输出引脚。
RX -> 接收数据输入引脚。
nRTS -> 请求以发送 (Request To Send), n 表示低电平有效。如果使能 RTS 流控制,当 USART 接收器准备好接收新数据时就会将 nRTS 变成低电平;当接收寄存器已满时, nRTS 将被设置为高电平。该引脚只适用于硬件流控制。
nCTS -> 清除以发送 (Clear To Send), n 表示低电平有效。如果使能 CTS 流控制,发送器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
SCLK -> 发送器时钟输出引脚。这个引脚仅适用于同步模式。
数据寄存器
USART 数据寄存器 (USART_DR) 只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART控制寄存器 1(USART_CR1) 的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9位数据字长,一般使用 8 位数据字长。
USART_DR 包含了已发送的数据或者接收到的数据。
USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR 读取数据会自动提取 RDR数据。
TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的
每一位顺序保存在接收移位寄存器内然后才转移到 RDR。
中断控制
USART 有多个中断请求,可以根据相应标志位判断当前状态。
下面为USART2的初始化,波特率配置为115200,8字长,无校验,1停止位
void USART2_Config(void)
{
/* 第一步:初始化GPIO */
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/* 配置Tx -- PA9引脚为复用功能 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置Rx -- PA10引脚为复用功能 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置GPIO的第二功能 */
/* 连接 PA10 到 USARTx_Tx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
/* 连接 PA9 到 USARTx__Rx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
/* 第二步:配置串口初始化结构体 */
/* 使能 USART 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
/* 配置 USART2 */
/* 波特率设置:DEBUG_USART_BAUDRATE */
USART_InitStructure.USART_BaudRate = 115200;//串口波特率
/* 字长(数据位+校验位):8 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
/* 停止位:1个停止位 */
USART_InitStructure.USART_StopBits = USART_StopBits_1;
/* 校验位选择:不使用校验 */
USART_InitStructure.USART_Parity = USART_Parity_No;
/* 硬件流控制:不使用硬件流 */
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
/* USART模式控制:同时使能接收和发送 */
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
/* 完成USART初始化配置 */
USART_Init(USART2, &USART_InitStructure);
/* 第三步:配置串口的接收中断 */
/* 嵌套向量中断控制器NVIC配置 */
NVIC_Configuration();
/*使能串口接收中断 */
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
/* 第四步:使能串口 */
USART_Cmd(USART2, ENABLE);
}
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART1为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
/* 抢断优先级为1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级为1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置NVIC */
NVIC_Init(&NVIC_InitStructure);
}
完成串口初始化之后,开始着手处理WT1-IMU发送的数据。
tips:
补充一下,为什么要使用数据帧进行通讯
串口通信虽然简单方便,但实际使用时会发现需要传输数据的不止1个字节,往往需要传输多个字节组成的数据包,由于串口通信中字节之间相互独立,在传输多个字节的数据时,将面临以下问题:
- 接收端从串口接收的是字节流,那它是如何知道帧的开始或结束呢?它又是如何知道这个帧有多长?
- 真实的物理硬件是充满噪音和干扰的,由于电噪声,某些字节的位可能被翻转,字节甚至整个帧都可能会丢失。
串口通信虽然简单方便,但实际使用时会发现需要传输数据的不止1个字节,往往需要传输多个字节组成的数据包,而因为串口通信中字节之间相互独立,在接收数据时面临“数据包对齐”和“防止数据出错”两大问题。
数据包对齐在也叫数据帧同步。解决方法就是引入帧同步字节,也就是增加帧头、帧尾等,对于固定长度数据帧通信可以只使用帧头帧尾,对于可变长度数据帧通信还需引入描述帧长的字节。利用帧头、帧尾、帧长即可解决。
防止数据出错也叫差错控制。在通信原理中,有四种差错控制方法:检错重发、前向纠错、反馈校验、检错删除。四种差错控制方法各有其优缺点,一般采用检错删除的差错控制算法,故只需要考虑如何检错这一个问题,常用的方法是在数据帧中增加校验字节。
首先浏览WT1-IMU的通讯协议文档,从中可知:
1.单帧的数据格式
2.数据的发送格式
一次发送三帧数据,由二级帧头TYPE区分(我习惯称之为功能字),最终得到的有效数据为9个short类型的数据。
理清通讯协议之后,开始进行串口接收部分的代码编写。
//IMU数据结构体
typedef struct
{
s16 acceleration_X;
s16 acceleration_Y;
s16 acceleration_Z;//三轴加速度
s16 angular_velocity_X;
s16 angular_velocity_Y;
s16 angular_velocity_Z;//三轴角速度
s16 Roll;
s16 Pitch;
s16 Yaw;//三轴角度
}IMU_Data;
u8 Data_Buf[30];//串口接收缓冲区
IMU_Data IMU_DATA;
//串口接收中断
void USART2_IRQHandler(void)
{
uint8_t ucTemp;
if(USART_GetITStatus(USART2,USART_IT_RXNE)!=RESET)//获取标志位
{
ucTemp = USART_ReceiveData( USART2 );//读取数据
Data_ByteGet(ucTemp);
}
}
void Data_ByteGet(u8 data)
{
static u8 pos;
Data_Buf[pos] = data;
if(pos == 0)
{
if(Data_Buf[0] == 0x55)//帧头判断
{
pos++;
}
else
{
pos=0;
}
}
else if(pos == 1)
{
if( (Data_Buf[1] == acceleration) || (Data_Buf[1] == angular_velocity) || (Data_Buf[1] == angle))//功能字
{
pos++;
}
else
{
pos=0;
}
}
else if(pos >= 2 && pos <= 9)
{
pos++;//数据域
}
else if(pos == 10)
{
//数据和校验比对
if(Data_Buf[10] == SUMCRC(Data_Buf,10))
{
//校验通过,开始对数据进行处理
Data_Analysis(Data_Buf);
}
pos = 0;
}
}
/**
* @brief 和校验计算函数
* @param data_buf -> 待计算的数组地址 lenth -> 数组中待累加元素个数
* @retval 校验和的低8位
*/
u8 SUMCRC(u8* data_buf,u8 lenth)
{
u8 i;
u8 ret;
for(i=0;i<lenth;i++)
{
ret += data_buf[i];
}
return (ret);
}
void Data_Analysis(u8* Buf)
{
if(Buf[1] == acceleration)//加速度帧
{
IMU_DATA.acceleration_X=( (Buf[3]<<8)|Buf[2] ) /32768*16*9.8;
IMU_DATA.acceleration_Y=( (Buf[5]<<8)|Buf[4] ) /32768*16*9.8;
IMU_DATA.acceleration_Z=( (Buf[7]<<8)|Buf[6] ) /32768*16*9.8;
}
else if(Buf[1] == angular_velocity)//角速度帧
{
IMU_DATA.angular_velocity_X=( (Buf[3]<<8)|Buf[2] )/32768*2000;
IMU_DATA.angular_velocity_Y=( (Buf[5]<<8)|Buf[4] )/32768*2000;
IMU_DATA.angular_velocity_Z=( (Buf[7]<<8)|Buf[6] )/32768*2000;
}
else if(Buf[1] == angle)//角度帧
{
IMU_DATA.Roll=( (Buf[3]<<8)|Buf[2] )/32768*180;
IMU_DATA.Pitch=( (Buf[5]<<8)|Buf[4] )/32768*180;
IMU_DATA.Yaw=( (Buf[7]<<8)|Buf[6] )/32768*180;
}
}
至此,对WT1-IMU的数据接收已经完成,接下来可以在main.c中将IMU的数据输出到上位机,以此检验串口数据帧的接收。
总结
使用串口通过数据帧进行多数据的交互在实际应用中相当常见。串口数据帧的结构相对简单,易于实现和解析,可自行设置数据长度以适应不同数据长度的需求,适用性广泛。