1串口通信介绍
当我们需要在电子设备之间传输数据时,可以使用不同的通信方式,其中一种常见的方式是串口通信。串口通信是一种通过串行传输比特流的通信方式,它用于在计算机和外部设备之间传输数据。串口通信的主要特点是使用单根线传输数据,一次传输一个比特位。这与并行传输方式相反,后者使用多个并行线传输多个比特位。串口通信通常在计算机和其他设备之间建立数据传输连接。这些设备可能包括打印机、传感器、模块等。串口通信使用一对引线,一条用于数据传输(通常称为数据线)和一条用于时钟同步(通常称为时钟线)。传输的比特流通过数据线逐个传输,而时钟线则用于指示传输的时序和速率。串口通信通常有两种主要的标准:RS-232和RS-485。
RS-232是一种较为常见的串口通信标准,用于短距离通信。它使用一对差分信号线,其中一条线用于传输正向电压信号,另一条线用于传输反向电压信号。RS-232通常用于连接计算机和外部设备,如调制解调器、串行打印机等。
RS-485是一种更为复杂的串口通信标准,主要用于距离较长、多设备的数据传输。RS-485使用差分信号线传输数据,信号线可以连接多个设备。这样,RS-485可以实现多设备之间的点对点通信或者多设备之间的总线通信。这使得RS-485非常适合用于工业自动化系统、智能家居等场景。
为了使串口通信工作,我们通常需要使用串口转USB适配器(或者其他适配器)将串口信号转换为计算机上能够识别的USB信号。然后,我们可以使用计算机上的串口通信软件或编程语言来控制和监控串口设备的数据传输。这是因为现在笔记本电脑上基本都没有设置串口数据口,需要usb转串口与单片机相连。
在使用串口通信时,需要注意以下几点:
1. 确保使用相同的波特率(传输速率)设置,以确保发送和接收的数据能够正确地同步。
2. 确保正确配置数据位、奇偶校验位和停止位等参数,以便正确解析传输的比特流。
3. 了解特定设备使用的串口通信协议和命令集,以便正确进行数据交互。
2串口通信的各种标志位和寄存器
2.1接收数据标志位、寄存器
首先,我们来看接收数据的流程中涉及的标志位和寄存器:
1. 数据接收标志位:
- RXNE(Read Data Register Not Empty):当接收缓冲区中有数据时,该标志位会被置位,表示可以读取接收数据。
2. 数据接收寄存器:
- USART_DR:串口数据寄存器,用于存储接收到的数据。
3. 状态标志位:
- ORE (Overrun Error):当接收缓冲区溢出时,该标志位会被置位,表示上一次数据还未被读取而新数据已经接收到。
- FE (Framing Error):当接收到无效的帧时,该标志位会被置位,表示接收到的数据帧格式错误。
- NE (Noise Error):当接收到噪声信号时,该标志位会被置位。
- PE (Parity Error):当接收到奇偶校验错误的数据时,该标志位会被置位。
2.2发送数据标志位、寄存器
接下来,我们来看发送数据的流程中涉及的标志位和寄存器:
1. 数据发送标志位:
- TXE(Transmit Data Register Empty):当发送缓冲区为空时,该标志位会被置位,表示可以发送新的数据。
2. 数据发送寄存器:
- USART_DR:串口数据寄存器,用于存储将要发送的数据。
3. 状态标志位:
- TC (Transmission Complete):当最后一个数据已经发送完成时,该标志位会被置位。
- TCIE (Transmission Complete Interrupt Enable):用于启用TC标志位的中断。
以上是最常用的与串口通信相关的标志位和寄存器,它们用于在STM32中实现串口接收和发送数据的过程。在编程过程中,我们可以通过读取/写入这些寄存器来实现串口通信功能。更详细的可参考嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置_stm32 串口寄存器-CSDN博客
3串口通信的接收发送的流程
3.1接收流程
当使用STM32串口接收外部传入数据时,需要按照以下步骤进行:
配置串口初始化参数:在使用串口进行数据收发之前,首先需要对串口进行初始化配置。这通常包括设置波特率、数据位、奇偶校验位、停止位和流控制等参数,这些参数应该与外部设备发送的数据保持一致。可以通过配置USART_InitTypeDef结构体和调用UART配置函数函数来完成参数配置。
/**
* @brief 配置嵌套向量中断控制器NVIC
* @param 无
* @retval 无
*/
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
/* 提示 NVIC_PriorityGroupConfig() 在整个工程只需要调用一次来配置优先级分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级*/
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置NVIC */
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief 配置串口参数
* @param 无
* @retval 无
*/
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
启用串口中断:为了在接收到新的数据时能够及时地处理它,需要启用串口的中断功能。定义中断函数来启用串口接收中断,这个函数将在接收到新数据时触发USART_RXNE中断,并调用相应的中断处理函数。见上方。
检测接收完成标志位:在中断处理函数中,首先需要检测USART_SR寄存器中的RXNE标志位,以确保已接收到新的数据。当RXNE标志位被置位时,表示接收缓冲区中已经有新的数据,可以通过USART_DR寄存器读取出来。
读取接收数据:在中断处理函数中,可以通过读取USART_DR寄存器来获取接收到的数据。一般情况下,可以将读取到的数据存储到一个缓冲区中,供主程序处理。读取完数据后,需要清除RXNE标志位,以允许接收下一个数据。
处理接收数据:在主程序中,可以根据接收到的数据进行相应的处理操作。通常情况下,可以使用if语句或switch语句对接收到的数据进行判断,并执行相应的处理操作。例如,可以将接收到的数据显示在LCD屏幕上或者将其用于控制外部设备。
3.2发送流程
当使用STM32的标准库函数来配置串口发送数据到外部时,可以按照以下步骤进行:
配置串口初始化参数:首先需要配置串口的初始化参数,包括波特率(Baud Rate)、数据位(Data Bits)、奇偶校验位(Parity)、停止位(Stop Bits)等。可以使用USART_InitTypeDef结构体来设置这些参数。然后,调用UART配置函数来进行串口的初始化配置。见3.1中的代码。
检测发送完成标志位:在发送数据之前,需要检测发送完成标志位,以确保上一次发送的数据已经全部被发送完毕。可以通过检测USART_SR寄存器中的TXE标志位来判断发送完成状态,如果TXE为1表示发送寄存器为空,可以继续发送下一个字节的数据。
写入发送数据:在检测到发送寄存器为空之后,可以将要发送的数据写入USART_DR寄存器。可以使用类似如下的代码进行写入。
检测发送完成标志位:在数据发送完成后,需要等待数据完全发送出去,并检测发送完成标志位。可以通过检测USART_SR寄存器中的TC标志位来判断发送完成状态,如果TC为1表示数据已经发送完毕。
最后,可以根据需要进行循环发送多个数据字节,或者编写相应的主程序代码来控制串口发送数据到外部。那么根据想要发送给串口的数据类型,就可以有以下的函数。
/***************** 发送一个字节 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到USART */
USART_SendData(pUSARTx,ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
/****************** 发送8位的数组 ************************/
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
{
uint8_t i;
for(i=0; i<num; i++)
{
/* 发送一个字节数据到USART */
Usart_SendByte(pUSARTx,array[i]);
}
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
}
/***************** 发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
unsigned int k=0;
do
{
Usart_SendByte( pUSARTx, *(str + k) );
k++;
} while(*(str + k)!='\0');
/* 等待发送完成 */
while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
{}
}
/***************** 发送一个16位数 **********************/
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
{
uint8_t temp_h, temp_l;
/* 取出高八位 */
temp_h = (ch&0XFF00)>>8;//通过移位取出高八位
/* 取出低八位 */
temp_l = ch&0XFF;
/* 发送高八位 */
USART_SendData(pUSARTx,temp_h);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
/* 发送低八位 */
USART_SendData(pUSARTx,temp_l);
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
注意:除了使用以上的方法进行发送接收数据,还可以通过重定义c库函数printf、scanf到串口,重定义后便可以使用printf、scanf、getchar等函数。但是使用这种方法串口配置的头文件中需要包含<stdio.h>,且创建工程时要在魔术棒页面中勾选Use Microlib选项。
4两种触发串口接收的方式
4.1查询方式
查询式接收是在主程序中不断地查询串口标志位,判断当前是否有新的数据到达。当发现有新数据到达时,主程序会读取串口接收缓冲区中的数据,进行处理(如解析数据包)后再继续执行主程序的其他任务。这种方式的优点是程序结构简单,不会对程序的实时性产生不利影响。但是,由于需要不断地查询串口标志位,会占用CPU的大量时间,降低了系统的效率。对于较为简单的项目,可以采用查询式接收。
4.2中断方式
中断式接收是在CPU处理其他任务时,由硬件触发串口中断,当中断发生时,单片机会暂停当前的任务,进入中断服务函数中进行数据接收和处理,完成后再返回主任务。这种方式的优点是可以提高系统效率,在串口接收数据时不需要占用CPU时间,运行速度更快。但是,由于需要接受中断信号,中断处理函数需要执行特定的任务,在编程时需要特别注意编写。对于需要实时响应的场景,建议使用中断方式接收。但其实如果中断函数执行时间过长也可能导致主进程运行,所以其实大型项目一般都是采用FreeRTOS来进行开发。
例子:两种方式可参考STM32-串口通信(串口的接收和发送)_stm32串口接收数据-CSDN博客
5数据包通信协议
实际使用串口时可能会涉及到数据包的传输发送,因为有较大数量的数据被打包,那么如果还是像前面这样传输数据就极有可能导致数据混乱,程序跑飞。所以为了保证数据的正确性,就使用了帧头帧尾来标识此为开头结尾。数据包通讯协议包含帧头、地址信息、数据类型、数据长度、数据块、校验码、帧尾。详情见下方。
帧头(Header):是数据包通讯协议中的固定字段,用于标识数据包的开始。一般情况下,帧头是一个字节的特定数值,例如 0xAA。
地址信息(Address):用于标识数据包的接收方地址或者数据来源地址。一般情况下,地址信息包括两个字节,例如 0x01 0x02,其中 0x01 表示高位地址,0x02 表示低位地址。
数据类型(Type):用于标识数据包中包含的数据的类型。例如,数据类型可以是传感器数据、控制命令、设置参数等。
数据长度(Length):用于标识数据包中动态长度的数据块的长度。可以使用一个字节或者两个字节来表示数据长度,具体取决于数据块的最大长度。
数据块(Data):是数据包中可变的数据内容。数据块的长度是由数据长度字段指定的。例如,如果数据长度字段指定的数据长度为 10,那么数据块中就包含了 10 个字节的数据。
校验码(Checksum):用于检测数据包在传输过程中是否发生了错误或者丢失。校验码的计算方法和具体实现取决于数据包通讯协议的设计。常见的校验方法包括CRC校验、和校验、异或校验等。
帧尾(Footer):用于标识数据包的结束。一般情况下,帧尾是一个字节的特定数值,例如 0x55。
typedef struct {
uint8_t header;
uint16_t address;
uint8_t type;
uint16_t length;
uint8_t *data;
uint8_t checksum;
uint8_t footer;
} DataPacket;
其中的header、address、type、length、checksum和footer都是数据包的固定字段,而data则是指向动态长度的数据块的指针。可以在UART_RxHandler()函数中,按照数据包协议的格式,依次从串口接收缓冲区中读取每个字段的数据,直到数据包完整地被接收到为止。
5.1数据包(hex数据)
以上面图中固定包长加帧头帧尾为例,发数据(代码详情查看STM32入门——串口接收数据包(协议带帧头帧尾)的编程实现方法_哔哩哔哩_bilibili):
uint8_t txd_buf[4] = {1,2,3,4};//自定义一个用于发送的数据包---4位
/***************** 发送一个数据包 **********************/
void Usart_Sendpack(USART_TypeDef * pUSARTx, uint8_t *ch)
{
Usart_SendByte(pUSARTx,0xFE);//帧头位0xFE,自己定
Usart_SendArray(pUSARTx,ch,4);//发送uint8_t txd_buf[4] = {1,2,3,4}四位数组
Usart_SendByte(pUSARTx,0xEF);//帧尾0xEF,自己定
}
收数据,用状态机思想来进行写 (代码详情查看STM32入门——串口接收数据包(协议带帧头帧尾)的编程实现方法_哔哩哔哩_bilibili) :
uint8_t rxd_buf[4];//定义一个用于接收外界数据包的空间---4位:如果是已知数据包长度可以弄小一点。但若不知道数据包长度不固定,那么最好长度长一点
uint8_t rxd_flag = 0;//接收完成标志
uint8_t rxd_index = 0;//接收字节索引
//******************串口中断服务函数---状态机*******************/
void DEBUG_USART_IRQHandler(void)
{
uint8_t recv_dat;
static uint8_t recv_state = 0;//使用状态变量来实现接收
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
recv_dat = USART_ReceiveData(DEBUG_USARTx);
//使用状态机接收数据包
switch(recv_state)
{
case 0://此时还处于触发中断状态但在检测帧头阶段
if(recv_dat == 0xFE)//检测到帧头,状态位置1,否则还是处于空闲帧状态
{
recv_state = 1;
rxd_index = 0; //清零,保证是从第0位开始存数
}
else
{
recv_state = 0 ;
}
break;
case 1:
rxd_buf[rxd_index] = recv_dat;
rxd_index++;
if(rxd_index>=4)//因为固定包长,所以需要判断接收到几位
{
recv_state = 2;
}
else
{
recv_state = 1;
}
break;
case 2:
if(recv_dat == 0xEF)
{
rxd_flag = 1;//一个数据包接收成功,回到主程序
recv_state = 0;//状态请0,准备接受下一个数据包
}
break;
}
}
}
主程序函数:
while(1)
{
/*固定包长数据包接收发送*/
if(rxd_flag == 1)//只要检测到rxd_flag为1就代表数据包接收完成
{
rxd_flag = 0;//清0数据包接收完成标志位
Usart_Sendpack(USART1,rxd_buf);//将数据包返回给上位机
}
}
实验效果:
5.2数据包(文本数据)
以上面图中非固定包长加帧头帧尾为例,发数据(代码详情查看STM32入门——串口接收数据包(协议带帧头帧尾)的编程实现方法_哔哩哔哩_bilibili):
/***************** 发送一个数据包(字符串) **********************/
void Usart_Sendpack(USART_TypeDef * pUSARTx, uint8_t *ch)
{
Usart_SendByte(pUSARTx,'@');//帧头位0xFE
Usart_SendArray(pUSARTx,ch,rxd_index);//发送uint8_t txd_buf[]非固定长度数组
Usart_SendByte(pUSARTx,'\r');//帧尾换行符
Usart_SendByte(pUSARTx,'\n');//帧尾换行符
rxd_index = 0;//发送完接收到的数据后清0不固定数组的位数
}
收数据,用状态机思想来进行写 (代码详情查看STM32入门——串口接收数据包(协议带帧头帧尾)的编程实现方法_哔哩哔哩_bilibili) :
//******************串口中断服务函数---状态机*******************/
void DEBUG_USART_IRQHandler(void)
{
uint8_t recv_dat;
static uint8_t recv_state = 0;//使用状态变量来实现接收
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
recv_dat = USART_ReceiveData(DEBUG_USARTx);
//使用状态机接收数据包
switch(recv_state)
{
case 0://此时还处于触发中断状态但在检测帧头阶段
if(recv_dat == '@')//检测到帧头,状态位置1,否则还是处于空闲帧状态
{
recv_state = 1;
rxd_index = 0; //清零,保证是从第0位开始存数
}
else
{
recv_state = 0 ;
}
break;
case 1://接收到帧头进行数据接收
if(recv_dat == '\r')//因为是不固定包长,所以用\r来判断,不能用数据位数
{
recv_state = 2;
}
else
{
rxd_buf[rxd_index] = recv_dat;
rxd_index++;
}
break;
case 2://数据包接收成功判断帧尾
if(recv_dat == '\n')//检测到换行符作为帧尾
{
rxd_flag = 1;//一个数据包接收成功,回到主程序
recv_state = 0;//状态请0,准备接受下一个数据包
}
break;
}
}
}
主函数:
int main(void)
{
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
USART_Config();
/* 发送一个字符串实验(两种方式发送字符串) */
//Usart_SendString( DEBUG_USARTx,"这是一个串口中断接收回显实验\n");
//printf("欢迎使用STM32开发板\n\n");//重定向后可用printf进行发送数据
while(1)
{
/*固定包长数据包接收发送*/
if(rxd_flag == 1)//只要检测到rxd_flag为1就代表数据包接收完成
{
rxd_flag = 0;//清0数据包接收完成标志位
Usart_Sendpack(USART1,rxd_buf);//将数据包返回给上位机
}
}
}
实验效果:
注意:如果此时外界传数据到stm32的速度非常快,那么有可能刚接收到一个数据包程序准备解析就又把rxd_buf中的数据给更新了。所以需要rxd_flag在解析完成后清0,并修改状态机的判断条件才可以。
主函数:
#include "stm32f10x.h"
#include "bsp_usart.h"
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
USART_Config();
/* 发送一个字符串实验(两种方式发送字符串) */
//Usart_SendString( DEBUG_USARTx,"这是一个串口中断接收回显实验\n");
//printf("欢迎使用STM32开发板\n\n");//重定向后可用printf进行发送数据
while(1)
{
/*固定包长数据包接收发送*/
if(rxd_flag == 1)//只要检测到rxd_flag为1就代表数据包接收完成
{
//rxd_flag = 0;//清0数据包接收完成标志位
Usart_Sendpack(USART1,rxd_buf);//将数据包返回给上位机---此处仅为演示已经接收到数据,一般只需要对接收到的数据包进行解析,不需要返回
rxd_flag = 0;//为保证上一个数据包收完再返回的流程走完,选择在发完后再清0数据包接收完成标志位,否则有可能接收下一个数据包。因为现实情况中可能需要在主程序中对数据包进行计算解析,所以如果中断不断接收数据可能会没解析完
}
}
}
状态机修改后的函数:
//******************串口中断服务函数---状态机*******************/
void DEBUG_USART_IRQHandler(void)
{
uint8_t recv_dat;
static uint8_t recv_state = 0;//使用状态变量来实现接收
if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
recv_dat = USART_ReceiveData(DEBUG_USARTx);
//使用状态机接收数据包
switch(recv_state)
{
case 0://此时还处于触发中断状态但在检测帧头阶段
// if(recv_dat == '@')//检测到帧头,状态位置1,否则还是处于空闲帧状态
if((recv_dat == '@')&& !rxd_flag)//检测到帧头,状态位置1,否则还是处于空闲帧状态
{
recv_state = 1;
rxd_index = 0; //清零,保证是从第0位开始存数
}
else
{
recv_state = 0 ;
}
break;
case 1://接收到帧头进行数据接收
if(recv_dat == '\r')//因为是不固定包长,所以用\r来判断,不能用数据位数
{
recv_state = 2;
}
else
{
rxd_buf[rxd_index] = recv_dat;
rxd_index++;
}
break;
case 2://数据包接收成功判断帧尾
if(recv_dat == '\n')//检测到换行符作为帧尾
{
rxd_flag = 1;//一个数据包接收成功,回到主程序
recv_state = 0;//状态请0,准备接受下一个数据包
}
break;
}
}
}