第9章 USART串口

本文详细介绍了STM32中的USART串口通信,包括其理论知识如通信接口、电平标准、波特率及时序,以及硬件结构。此外,还讲解了USART的初始化、发送接收函数,并探讨了数据包的HEX和文本格式,以及如何处理数据包的接收。同时提到了CH340芯片在USB转串口中的应用和注意事项。
摘要由CSDN通过智能技术生成

USART理论部分

通信接口

  • 通信的目的:将一个设备的数据传送到另一个设备,拓展硬件系统

  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
    在这里插入图片描述

  • USART的引脚
    1.TX(Transmit Exchange)是数据发送脚
    2.RX(Receive Exchange)是数据接收脚

  • I2C的引脚
    1.SCL(Serial Clock)是时钟
    2.SDA(Serial Data)是数据

  • SPI的引脚
    1.SCLK(Serial Clock)是时钟
    2.MOSI(Master Output Slave Input)是主机输出数据脚
    3.MISO(Master Input Slave Output)是主机输入数据脚
    4.CS(Chip Select)是片选,用于指定通信的对象

  • Can的引脚
    这两个是差分数据脚,用两个引脚表示一个差分数据

  • USB通信
    1.DP(Data Positive)或者是D+
    2.DM(Data Minus)或者是D-
    这两个也是一对差分数据脚

  • 1.全双工就是指通信双方能够同时进行双向通信,一般来说,全双工的通信都有两根通信线,比如串口,一根TX发送,一根RX接收,SPI,一根MOSI发送,一根MISO接收,发送线路和接收线路互不影响,全双工,剩下的这些,I2C、CAN和USB,都只有一根数据线,CAN和USB两根差分线也是组合成一根数据线的,所以都是半双工
    2.单工是指在数据只能从一个设备到另一个设备,而不能反着来

  • 时钟特性,比如你发送一个波形,高电平然后低电平,接收方怎么知道你是10还是1100呢,这就需要一个时钟信号来告诉接收方,你什么时候需要采集数据
    1.时钟特性分为同步和异步,这里I2C和SPI有单独的时钟线,所以他们是同步的,接收方可以在时钟信号的指引下进行采样,剩下的串口、CAN和USB没有时钟线,所以需要双方约定一个采样频率,这就是异步通信,并且还需要加一些帧头帧尾等,进行采样位置的对齐

  • 电平特性,上面三个(USART、I2C、SPI)都是单端型号,也就是它们引脚的高低电平都是对GND的电压差,所以单端信号通信的双方必须要共地,就是把GND接在一起,所以说,这里通信的引脚,前三个还应该加一个GND引脚,不接GND是没办法通信的,之后CAN和USB是差分信号,它是靠两个差分引脚的电压差来传输信号的,是差分信号,在通信的时候,可以不需要GND,不过USB协议里面也有一些地方需要单端信号,所以USB还是需要共地的,使用差分信号可以极大地提高抗干扰特性,所以差分信号一般传输速度和距离都会非常高,性能也是很不错的

  • 设备特性,串口和USB属于点对点的通信,中间三个是可以在总线上挂载多个设备的,点对点通信就相当于老师找你去办公室谈话,只有两个人,直接传输数据就可以了,多设备就相当于老师在教室里,面对所有同学谈话,需要有一个寻址的过程,以确定通信的对象

串口通信

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块相互通信,极大地扩展了单片机地应用范围,增强了单片机系统地硬件实力
  • USB转串口模块,上面有个芯片,型号是CH340,这个芯片可以把串口协议转换为USB协议,它一边是USB口,可以插在电脑上,另一边是串口地引脚,可以和支持串口地芯片连接在一起,这样就能实现串口和电脑地通信了
  • 陀螺仪传感器模块,可以测量加速度、加速度这些姿态参数,左右各有四个引脚,一边是串口的引脚,另一边是I2C的引脚,
  • 蓝牙串口模块,4个脚是串口通信的引脚,上面的芯片可以和手机互联,实现手机遥控单片机的功能

硬件电路

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准版不一致时,需要加电平转接芯片

电平标准

  • 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
    1.TTL电平:+3.3V或+5V表示1,0V表示0
    2.RS232电平:-3~ -15V表示1,+3~+15表示0一般在大型机器上使用,由于环境可能比较恶劣,静电干扰比较大,所以这里电平的电压都比较大,而且允许波动的范围也很大
    3.RS485电平:两线压差+2~ +6表示1,-2~ -6表示0(差分信号) 差分信号抗干扰能力非常强,使用RS485标准,通信距离可以达到上千米,而上面这两种电平,最远只能达到几十米,再远就传不了了
  • 在硬件电路上,协议规定是,一个设备使用TX发送高低电平,另一个设别使用RX接收高低电平,在线路中使用TTL电平,因为STM32是3.3V的器件所以如果线路对地是3.3V,就代表了发送了逻辑1,如果线路对地是0V,就代表了发送了逻辑0

串口参数及时序

  • 波特率:串口通信的速率
  • 起始位:标志一个数据帧的开始,固定为低电平
  • 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
  • 校验位:用于数据验证,根据数据位计算得来
  • 停止位:用于数据帧间隔,固定为高电平
    在这里插入图片描述
    这两个时序图,这就是串口发送一个字节得格式,这个格式是串口协议规定的,串口中,每一个字节都装载在一个数据帧里面,每个数据帧都由起始位、数据位和停止位组成,这里数据位有8个,代表一个字节的8位,在右边这个数据帧里面,还可以在数据位的最后,加一个奇偶校验位,这样数据位就是9位,其中有效载荷是前8位,代表一个字节,校验位跟在有效载荷后面,占1位,这就是串口数据帧的整体结构。

首先串口的空闲状态时是高电平,也就是没有数据传输的时候,引脚必须要置高电平,作为空闲状态,然后需要传输的时候,必须要先发送一个起始位,这个起始位必须是低电平,来打破空闲状态的高电平,产生一个下降沿,这个下降沿就告诉接收设备,这一帧数据要开始了,如果没有起始位,那当我发送8个1时候,是不是数据线就一直都是高电平,没有任何波动,对吧,这样,接收方怎么知道我发送数据了呢,所以这里必须要有一个固定为低电平的起始位,产生下降沿,来告诉接收设备,我要发送数据了,同理,在一个字节数据发送完成后,必须要有一个停止位,这个停止位的作用是,用于数据帧间隔,固定为高电平,同时这个停止位,也是为下一个起始位做准备的,如果没有停止位,那当我数据最后一位是0的时候,下次再发送新的一帧,是不是就没法产生下降沿了,对吧,这就是起始位和停止位的作用,起始位固定为0,产生下降沿,表示传输开始,停止位固定为1,把引脚恢复成高电平,方便下一次的下降沿,如果没有数据了,正好引脚也为高电平,代表空闲状态,然后继续看中间的数据位,这里数据位表示数据帧的有效载荷,1为高电平,0为低电平,低位先行,比如我要发送一个字节,是0x0F,那就首先把0F转换为二进制,就是00001111,然后低位先行,所以数据要从低位开始发送,也就是11110000,像这样,依次发送到引脚上,所以最终引脚的波形就是这样的
在这里插入图片描述

这里串口使用的是一种叫奇偶校验的数据验证方法,奇偶校验可以判断数据传输是不是出错了,如果数据出错了,可以选择丢弃或者要求重传,校验可以选择三种方式,无校验、奇校验和偶校验,无校验,就是不需要校验位,波形就是左边这个,起始位、数据位、停止位,总共3个部分,奇校验和偶校验的波形就是右边这个,起始位、数据位、校验位、停止位、总共4个部分,如果使用了奇校验,那么包括校验位在内的9位数据会出现奇数个1,比如如果你传输00001111,目前总共4个1,是偶数个,那么校验位就需要再补个1,连同校验位就是000011111,总共5个1,保证1为奇数,如果数据是00001110,此时3个1,是奇数个,那么校验位就补一个0,发送方,在发送数据后,会补一个校验位,保证1的个数为奇数,接收方,在接收数据后,会验证数据位和校验位,如果1的个数还是奇数,就认为数据没有出错

串口波形,是用示波器实测的,操作方法是,把探头的GND接在负极,探头接在发送设备的TX引脚,然后发送数据,就可以捕捉到这些波形了。

波特率是9600,所以每一位的时间就是1/9600,大概是104us,没发送数据的时候,是空闲状态高电平,数据帧开始,先发送起始位,产生下降沿,代表数据帧开始,数据0x55转为二进制(01010101),低位先行,就是依次发送10101010,然后,这个参数是8位数据,1位停止,无校验,由于没有校验位,所以之后就是停止位,把引脚置回高电平,这样一个数据帧就完成了。
在这里插入图片描述
在STM32中,这个根据字节数据翻转高低电平,是由USART外设自动完成的,不用我们操心。

USART简介

  • USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
  • 自带波特率发生器,最高达4.5Mbits/s
    这个波特率发生器,就是用来配置波特率的, 它其实就是一个分频器,比如我们APB2总线给个72MHz的频率,然后波特率发生器进行一个分频,得到我们想要的波特率时钟,最后在这个时钟下,进行收发,就是我们指定的通信波特率
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2),这些就是STM32 USART支持配置的参数了,这个数据位长度有8位和9位,是包含奇偶校验位的长度,一般不需要校验就选8位,需要校验就选 9位,在进行连续发送时,停止位长度决定了帧的间隔,我们最常用的就是1位停止位,其他的很少用。
  • 可选校验位(无校验/奇校验/偶校验)
  • 以上这些所有的参数,都是可以通过配置寄存器来完成的。
  • 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
    这个同步模式,就是多了个时钟CLK的输出;硬件流控制,这个是,比如A设备有个TX向B设备的RX发送数据,A设备一直在发,发的太快了,B处理不过来,如果没有硬件流控制,那B就只能抛弃新数据或者覆盖原数据了,如果有硬件流控制,在硬件电路上,会多出一根线,如果B没准备号接收,就置高电平,如果准备好了,就置低电平,A接收到了B反馈的准备信号,就只会在B准备好的时候,才发数据,如果B没准备号,那数据就不会发送出去,硬件流控制,STM32也是有的,不过我们一般不用;DMA,是这个串口支持DMA进行数据转运,如果有大量的数据进行收发,可以使用DMA转运数据,减轻CPU的负担;最后,智能卡、IrDA、LIN,这些是其他的一些协议,因为这些协议和串口是非常的像,所以STM32就对WSART加了一些小改动,就能兼容这么多协议了。
  • STM32F103C8T6 USART资源:USART1、USART2、USART3,其中这里USART1是APB2总线上的设备,剩下的都是APB1总线的设备,这个就开启时钟的时候注意一下。

USART基本结构

在这里插入图片描述
最左边这里是波特率发生器,用于产生约定的通信速率,时钟来源是PCLK2或1,经过波特率发生器分频后,产生的时钟通过发送控制器和接收控制器。发送控制器和接收控制器,用来控制发送移位和接收移位,之后,由发送数据寄存器和发送移位寄存器这两个寄存器的配合,将数据一位一位地移出去,通过GPIO口的复用输出,输出到TX引脚,产生串口协议规定的波形,这里画了几个右移的符号,就是代表这个移位寄存器是往右移的,是低位先行,当数据由数据寄存器转到移位寄存器时,会置一个TXE的标志位,我们判断这个标志位,就可以知道是不是可以写下一个数据了,然后接收部分也是类似的,RX引脚的波形,通过GPIO输入,在接收控制器的控制下,一位一位地移入接收移位寄存器,这里画了右移的符号,也是右移的,因为是低位先行,所以要从左边开始移进来,移完一帧数据后,数据就会统一转运到接收数据寄存器,在转移的同时,置一个RXNE标志位,我们检查这个标志位,就可以知道是不是收到数据了,同时这个标志位也可以去申请中断,这样就可以在收到数据时,直接进入中断函数,然后快速地读取和保存数据,那右边这实际上有四个寄存器,但是在软件层面,只有一个DR寄存器可以供我们读写,写入DR时,数据走上面这条路,进行发送,读取DR时,数据走下面这条路,进行接收,这就是USART进行串口数据收发的过程,最后右下角,是一个开关控制,就是配置完成之后,用Cmd开启一下外设,这个也是常规操作了。

波特率发生器

  • 发送器和接收器的波特率由波特率寄存器BRR里的分频系数DIV确定
    在这里插入图片描述
    波特率和分频系数的关系,可以由这个计算公式进行计算
    举个例子,比如我要配置USART1为9600的波特率,那如何配置这个BRR寄存器呢,我们代入公式,就是9600 = 72M / (16 * DIV),其中USART1的时钟是72M,最终写到寄存器还需要转换成二进制。
    不过,我们用库函数配置的话,就非常方便,需要多少波特率,直接写就行了,库函数会自动帮我们算。

CH340的注意事项

  • 很多人迷惑的这个第5脚,板子上标的是VCC,但从原理图可以看到,它是通向了CH340芯片上的VCC上,所以这个第5脚,实际上是CH340的电源输入脚,一般我们这个模块的排针会有一个跳线帽,这个跳线帽需要插在4、5脚,或者5、6脚上,也有文字说明,如下图
    在这里插入图片描述
    所以这个跳线帽,是用来选择通信电平的,也是给CH340芯片供电的,所以最好不要拿掉,如果你拿掉了,就相当于这整个芯片,没有供电,不过神奇的是,江科大试了试,即使 把跳线帽拔掉,不给芯片供电,这个串口还是能正常工作。我们的stm32通信需要3.3V,所以把跳线帽插在这里的4、5脚上就行了,所以供电就只剩下一个5V脚了

USART代码部分

接线图

在这里插入图片描述
下面这个是USB转串口的模块,这里有个跳线帽,上面有说过,要插在VCC和3V3这两个引脚上,选择通信的TTL电平为3.3V,然后通信引脚,TXD和RXD,要接在STM32的PA9和PA10口,从引脚定义表可以知道,USART1的TX是PA9,RX是PA10,计划用USART1进行通信,所以就选用这两个脚,如果选用USART2或者3的话,就要找一下,然后是TX和RX交叉连接,两个设备之间要把负极接在一起,进行共地,一般多个系统之间互连,都要进行共地,这样电平才能有高低的参考。

初始化

  • 看基本结构图
    第一步,开启时钟,把需要用的USART和GPIO的时钟打开
    第二步,GPIO初始化,把TX配置成复用输出,RX配置成输入
    第三步,配置USART,直接使用一个结构体,就可以把这里所有的参数都配置好了
    第四步,如果你只需要发送的功能,就直接开启USART,初始化就结束了,如果需要接收的功能,可能还需要配置中断,那就再开启USART之前,再加上ITConfig和NVIC的代码就行了
  • 初始化完成之后,如果要发送数据,调用一个发送函数就行了,如果要接收数据,就调用一个接收的函数,如果要获取发送和接收的状态,就调用获取标志位的函数,这就是USART外设的使用思路。
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

SendData,发送数据,ReceiveData,接收数据,这两个函数,在我们发送和接收的时候会用到,SendData就是写DR寄存器,ReceiveData就是读DR寄存器,DR寄存器内部有4个寄存器,控制发送与接收,执行细节在上面,这里程序上就非常简单了,写DR就是发送,读DR就是接收,至于怎么产生波形,怎么判断输入,软件一概不管。

void Serial_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	//这里USART1是APB2的外设,其他的都是APB1的外设
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	//然后还需要开启一下GPIO的时钟,USART1的TX是PA9.RX是PA10,所以开启GPIOA的时钟
	GPIO_InitTypeDef GPIO_InitStructure;
	/*首先是引脚的选择
		1. TX引脚是USART外设控制的输出脚,所以要用复用推挽输出
		2. RX引脚是USART外设数据输入脚,所以要选择输入模式,
		输入模式并不分什么普通输入、复用输入,
		一根线只能有一个输出,但可以有多个输入,
		所以输入脚,外设和GPIO都可以同时用,
		一般RX配置是浮空输入或者上拉输入
		因为串口波形空闲状态是高电平,所以不适用下拉输入*/
	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);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600; //直接写出波特率数值
	/*写完波特率数值之后,USART_Init这个函数内部会自动
	  算好9600对应的分频系数,然后写入BRR寄存器*/
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不使用流控,所以使用None
	USART_InitStructure.USART_Mode = USART_Mode_Tx; //发送模式
	/*串口模式,这里可以选择 Tx发送模式和 Rx接收模式,
	  如果你既需要发送又需要接收,那就用或符号把 Tx和 Rx或起来:
	  USART_Mode_Tx | USART_Mode_Rx*/
	USART_InitStructure.USART_Parity = USART_Parity_No; 
	//校验位,no无校验,Odd奇校验,Even偶校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1; 
	//停止位,这个参数可以选择0.5、1、1.5、2
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; 
	//字长,这个参数可以选择8位或者9位,我们不需要校验,所以字长就选8位
	USART_Init(USART1, &USART_InitStructure);
	
	USART_Cmd(USART1, ENABLE);
}

发送函数

void Serial_SendByte(uint8_t Byte)
{
	/*赋值给DR寄存器,因为这是写入 DR,所以数据最终通向 TDR(发送数据寄存器),
	TDR再传递给发送移位寄存器,最后一位一位地把数据移出到 TX引脚,完成数据的发送*/
	USART_SendData(USART1, Byte);
	/*写完之后,还需要等待一下,等 TDR的数据转移到移位寄存器了,我们才能放心,
	要不然数据还在 TDR进行等待,我们再写入数据,就会产生数据覆盖,
	所以在发送之后,我们还需要等待一下标志位*/
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
	//发送数据寄存器空标志位
	/*然后我们要等待 TXE置1,所以还是套一个 while循环,
	如果 TXE标志位==RESET,就一直循环,直到SET,结束等待*/
	
	/*来自数据手册
	* TXE:发送数据寄存器空(Transmission complete)
	* 当包含有数据的一帧发送完成时,并且TXE = 1时,由硬件将该位置‘1’。
	    如果USART_CR1中的TCIE为‘1’,则产生中断。由软件序列清除该位(先读USART_SR,然后写入USART_DR)。
		TC位也可以通过写入‘0’来清除,只有在多缓存通讯中才推荐这种清除程序。
	* 0:发送还未完成
	  1:发送完成。
	*/
}

接收函数

  • 对于串口接收来说,可以使用查询和中断两种方法,如果使用查询,那初始化就结束了,如果使用中断,那还需要在这里开启中断,配置NVIC。
  • 查询的流程是,在主函数里不断判断RXNE标志位,如果置1了,就说明收到数据了,那再调用ReceiveData,读取DR寄存器,这样就行了。
uint8_t RxData;
while(1)
{
	if(USART_GetFlagStatus(USART1, USART_FLAG)RXNE) == SET)
	{
		RxData = USART_ReceiveData(USART1);
		//目前接收到的一个字节就已经再RxData里了
	}
}
  • 使用中断来进行串口接收,在初始化中加入下面的代码
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	//开启 RXNE标志位到 NVIC的输出
	//之后就是配置 NVIC了,先分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//再初始化NVIC的USART1通道
	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前面加上这一串的代码

这样,RXNE标志位一旦置1了,就会向NVIC申请中断,之后,我们可以在中断函数里接收数据,中断函数的名字,可以看一下启动文件,即USART1_IRQHandler,复制,然后在 Serial.c 中写中断函数

uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)//先判断标志位
	{
		Serial_RxData = USART_ReceiveData(USART1);//先读取
		Serial_RxFlag = 1;//读完之后,置个自己的标志位
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除标志位
	}
}
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)//实现了读后自动清除的功能
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}

uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;
}
//到这里,中断接收和变量的封装,就完成了

接着写主函数中的函数

uint8_t RxData;
while(1)
{
	if(Serial_GetRxFlag() == 1)
	{
		RxData = Serial_GetRxData();
	}
}

USART串口数据包

HEX数据包

首先,数据包的作用是,把一个个单独的数据给打包起来,方便我们进行多字节的数据通信。上面的学习中,发送一个字节、接收一个字节,都没问题,但在实际应用中,我们可能需要把多个字节打包为一个整体进行发送,比如说,我们有个陀螺仪传感器,需要用串口发送数据到STM32,陀螺仪的数据,比如X轴一个字节、Y轴一个字节、Z轴一个字节,总共3个数据,需要连续不断地发送,当你像这样,XYZXYZXYZ连续发送的时候,就会出现一个问题,就是接收方,它不知道这数据哪个对应X、哪个对应Y、哪个对应Z,因为接收方可能会从任意位置开始接收,所以会出现数据错位的现象,这个时候,我们就需要研究一种方式,把这个数据进行分割,把XYZ这一批数据分割开,分成一个个数据包,这样再接收的时候,就知道了,数据包的第一个数据就是X、第二个是Y、第三个是Z,这就是数据包的任务,就是把属于同一批的数据进行打包和分割。

HEX数据包分割方法

我们的数据包,通常使用的是额外添加包头包尾这种方式,它们的数据包格式,可以是用户根据需求,自己规定的,也可以是你买一个模块,别的开发者规定的
在这里插入图片描述

  • 第一个问题,包头包尾和数据载荷重复的问题,这里定义FF为包头,FE为包尾,如果我传输的数据本身就是FF和FE怎么办呢,那这个问题确实存在,如果数据和包头包尾重复,可能会引起误判,对应这个问题我们又如下几种解决方法。
    1.限制载荷数据的范围,如果可以的话,我们可以在发送的时候,对数据进行限幅,比如XYZ,3个数据,变化范围都可以是0 ~ 100,那就好办了,我们可以在载荷中只发送0 ~ 100的数据,这样就不会和包头包尾重复了。
    2.如果无法避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要我们通过包头包尾对齐了数据,我们就可以严格知道,哪个数据应该是包头包尾,哪个数据应该是载荷数据,在接收载荷数据的时候,我们并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐,这样,在经过几个数据包的对齐之后,剩下的数据包应该就不会出现问题了
    3.增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态,比如我们使用FF、FE作为包头,FD、FC作为包尾,这样也可以避免载荷数据和包头包尾重复的情况发生
  • 第二个问题,这个包头包尾并不是全部都需要的,比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是,一个包头FF,加4个数据,这样也是可以的,当检测到FF,开始接收,收够4个字节后,置标志位,一个数据包接收完成,这样也可以,不过这样的话,载荷和包头重复的问题会更严重一些,比如最严重的情况下,我载荷全是FF,包头也是FF,那你肯定不知道哪个是包头了,而加上了FE作为包尾,无论数据怎么变化,都是可以分辨出包头包尾的。
  • 第三个问题,就是固定包长和可变包长的选择问题,对应HEX数据包来说,如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误,如果你又会重复,又选择可变包长,那数据很容易就乱套了。
  • 最后一个问题,就是个各种数据转换位字节流的问题,这里数据包都是一个字节一个字节组成的,如果你想发送16位的整型数据、32位的整型数据,float、double,甚至是结构体,其实都没问题,因为它们内部其实都是由一个字节一个字节组成的,只需要用一个uint8_t的指针指向它,把它们当作一个字节数组发送就行了。

文本数据包

文本数据包和HEX数据包,就分别对应了文本模式和HEX这两种模式,在HEX数据包里面,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的,就是文本格式,但实际上,每个文字字符背后,其实都还是一个字节的HEX数据。
在这里插入图片描述
由于数据译码成了字符形式,这就会存在大量的字符可以作为包头包尾,可以有效避免载荷和包头包尾重复的问题,比如这里规定的是以“@”作为包头,以“\r\n”,也就是换行,这两个字符作为包尾,在载荷数据中间可以出现处理包头包尾的任意字符,这很容易做到,所以文本数据包基本不用担心载荷和包头包尾重复的问题,使用非常灵活,可变包场、各种字母、符号、数字,都可以随意使用,当我们接收到载荷数据之后,得到的就是一个字符串,在软件中再对字符串进行操作和判断,就可以实现各种指令控制的功能了,而且字符串数据包表达的意思很明显,可以把字符串数据包直接打印到串口助手上,什么指令、什么数据,一眼就能看明白,所以这个文本数据包,通常会以换行作为包尾,这样在打印的时候,就可以一行一行地显示了,非常方便。

那HEX数据包和文本数据包这两种对比下来,其实也是各有优缺点,HEX数据包,优点是,传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据,比如一些使用串口通信的陀螺仪、温湿度传感器,缺点就是灵活性不足、载荷容易和包头包尾重复。而文本数据包,优点是,数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,比如蓝牙模块常用的AT指令,CNC和3D打印常用的G代码,都是文本数据包的格式,那缺点就是解析效率低,比如发送一个数100,HEX数据包就是一个字节100,完事,文本数据包就得是三个字节的字符,‘1’,‘0’,‘0’,收到之后还要把字符转换成数据,才能得到100,所以说,我们需要根据实际场景来选择和设计数据包格式。

数据包接收

其实数据包的发送非常简单,在HEX数据包这里,如果我想发送一个数据包,就定义一个数组,填充数据,然后用上面写过的SendArray,一发就完事了。
文本数据包这里也很简单,写一个字符串,然后调用SendString,一发送,也完事了。
江科大演示了固定包长HEX数据包的接收方法,和可变包长文本数据包的接收方法。

HEX数据包接收

在这里插入图片描述
在上面的代码可以知道,每收到一个字节,程序都会进一遍中断,在中断函数里,我们可以拿到这一个字节,但拿到之后,我们就得退出中断了,所以,每拿到一个数据,都是一个独立的过程,而对于数据包来说,很明显,它具有前后关联性,包头之后是数据,数据之后是包尾,对于包头、数据、包尾这三种状态,我们都需要由不同的处理逻辑,所以在程序中,我们需要设计一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维,就叫做“状态机”,在这里,就使用状态机的方法来接收一个数据包,要想设计一个好的状态机程序,画一个状态转移图是必要的。如下:
在这里插入图片描述
对于上面这样一个固定包长的HEX数据包来说,我们可以定义三个状态,第一个状态时等待包头,第二个状态是接收数据,第三个状态时等待包尾,每个状态需要用一个变量来标志一下,比如江科大这里用变量S来标志,三个状态依次是S=0、S=1、S=2,这一点类似于置标志位,只不过标志位只有0和1,而状态机时多标志位状态的一种方式,然后执行流程是最开始S=0时,收到一个数据,进中断,根据S=0,进入第一个状态的程序,判断数据是不是包头FF,之后置S=1,退出中断,结束,这样下次再进中断,根据S=1,就可以进行接收数据的程序了,接收到数据,就直接把它存在数组里,另外再用一个变量,记录收了多少个数据,收购了4个数据,就置S=2,也就是最后一个状态,判断数据是不是FE,正常情况下,应该时FE,这样就可以置S=0,回到最初的状态,开始下一个轮回。

文本数据包接收

在这里插入图片描述
同样也是利用状态机,定义三个状态。

USART串口数据包的代码部分

串口收发HEX数据包

/*在这里为了收发数据包,先定义两个缓存区的数组
  这四个数据只存储发送或接收的载荷数据,包头包尾就不存了*/
uint8_t Serial_TxPacket[4];				//FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;
初始化的代码都不需要改
/*调用SendPacket这个函数,TxPacket数组的四个数据,
  就会自动加上包头包尾发送出去*/
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);//发送包头
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);//发送包尾
}
/*在接收中断函数里,就需要用状态机来执行接收逻辑了
  接收数据包,然后把载荷数据存在RxPacket数组里*/
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//标志当前状态的静态变量S
	/*函数里面的静态变量生命周期是程序运行期间,
	  但是作用域是该函数,出了该函数就用不了了,但是还存在*/
	static uint8_t pRxPacket = 0;//用于指示接收几个数据了
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0)
		{
			if (RxData == 0xFF)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		/*只能用else if,不然在RxState == 0后,
		  RxState被赋值为1,就会立马进入RxState == 1
		  这样就会出现连续两个if都同时成立的情况*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			这里面RxState与pRxPacket不会被刷新的原因是,
			静态变量的生命周期与局部变量不一样
			if (pRxPacket >= 4)
			{
				RxState = 2;
			}
		}
		else if (RxState == 2)
		{
			if (RxData == 0xFE)
			{
				RxState = 0;
				/*代表一个数据包已经接收到了,可以置一个接收标志位*/
				Serial_RxFlag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

而在主函数里面

while(1)
{
	if(Serial_GetRxFlag() == 1)
	{
	}
}

在这个函数里面呢,存在一个问题,就是Serial_RxPacket这个数组里面,是被不断写入又不断读取的,有的时候,可能会造成混乱,比如你读出的过程太慢,前面两个数据刚读出来,等了一会儿,才继续往后读取数据,那这时候,后面的数据可能就会刷新为下一个数据包的数据,这个时候,需要根据情况进行更改。

  • 可以把这个缓冲数组在接受完成后赋给另一个数组,然后显示新的数组数据。

串口收发文本数据包

char Serial_RxPacket[100];	//接收的类型定义为char,用于接收字符
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);
		
		if (RxState == 0)
		{
			if (RxData == '@' && Serial_RxFlag == 0)
			  为了防止错位,添加上一句&& Serial_RxFlag == 0,
			  这样才执行接收,否则就是你发太快了,我还没处理完。
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		/*因为载荷数量并不确定,所以每次接收之前,得先判断是不是包尾*/
		else if (RxState == 1)
		{
			if (RxData == '\r')
			{
				RxState = 2;
			}
			else
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if (RxState == 2)
		{
			if (RxData == '\n')
			{
				RxState = 0;
				/*接收到之后,还需要给这个字符数组的最后
				  加一个字符串结束标志位,方便后续对字符串进行处理,
				  要不然OLED中的ShowString,它没有结束标志位,
				  就不知道这个字符串到底有多长了*/
				Serial_RxPacket[pRxPacket] = '\0';
				Serial_RxFlag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

为了防止错位,在中断函数那里添加上一句&& Serial_RxFlag == 0,这样才执行接收,否则就是你发太快了,我还没处理完。然后主函数这里

while(1)
{
	if(Serial_RxFlag == 1)
	{
		进行执行操作,操作完成之后
		RxFlag = 0;把RxFlag清0
	}
}

判断两个字符串是否相等,需要用到一个函数strcmp

/*判断字符串是不是等于我们规定的指令,
  判断字符串,可以调用C语言字符串的官方库*/
 #include <string.h>//这个库里面有很多字符串的处理函数
 if(strcmp(Serial_RxPacket,"LED_ON") == 0)//如果相等的话,函数返回0
 

FlyMcu串口下载&&STLINK Utility

见9-6

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值