当前位置: 首页 > news >正文

STM32入门之USART串口部分

本篇文章主要介绍STM32的通信部分的串口通信功能。利用通信功能我们可以实现不同设备之间的信息交互,大大提高了设备使用的效率,那么常见的通信接口主要有5种。

  • USART(串口通信),引脚:TXD,RXD 双工模式:全双工 时钟特性:异步 电平特性:单端 设备特性:点对点
  • IIC,引脚:SCL,SDA 双工模式:半双工 时钟特性:同步 电平特性:单端 设备特性:多设备
  • SPI,引脚:SCLK,MOSI,MISO,CS 双工模式:全双工 时钟特性:同步 电平特性:单端 设备特性:多设备
  • CAN,引脚:CAN_H,CAN_L 双工模式:半双工 时钟特性:异步 电平特性:差分 设备特性:多设备
  • USB,引脚:DP,DM 双工模式:半双工 时钟特性:异步 电平特性:差分 设备特性:点对点 

什么是双工模式?

双工模式就是看通信协议的通信线有几条。如果发送信号线与接收信号线是分开的,工作起来互不影响,这种模式就是全双工模式。如果通信协议的通信线只有一条,也就是说发送数据与接收数据都是在同一条通信线上进行的,这种模式就是半双工模式。还有一种模式是单工模式,这种模式只能接收数据或者只能发送数据。

什么是时钟特性?

时钟特性就是在传输数据时采样的时钟频率是否一致。怎么判断是同步还是异步呢?主要就是看通信协议是否有时钟线,如果有时钟线,那么这种协议的时钟特性就是同步的,如果没有,那么就是异步的,也就是说,在异步的情况下,需要发送数据方与接收数据方共同约定好一个时钟采样频率,以确保数据能够以一致的采样频率被准确读取,当然还有一些数据帧头与数据帧尾的对齐。

什么是电平特性?

电平特性就是指通信数据线的电平高低是对于GND而言还是对于差分引脚的电压差而言。如果是单端,那么数据线引脚的高低电平就是与GND的差值来决定的。如果是差分那么就是考差分引脚的电压差来决定的。差分信号可以大大提高信号抗干扰能力与传输距离。

什么是设备特性?

设备特性就是指通信双方是一对一还是一对多。点对点只允许一个设备与另一个设备之间通信。多设备允许一个设备与多个设备之间进行通信。

那么通信协议大概就介绍到这里了,这篇文章我们主要来学习USART(串口)部分。

一般简单的串口通信是4条线,分别为VCC,GND,TXD,RXD。

如果是串口与串口之间的通信,那么设备1的TXD要与设备2的RXD连接,设备1的RXD要与设备2的TXD连接。这里还有一个电平标准需要注意,电平标准就是高低电平标准,串口的电平标准一般有3种,当通信双方的电平标准不一样是,还需要加电平转化芯片,双方才能进行通信。

  • TTL电平:规定5.0V或者3.3V为高电平(逻辑1),0V为低电平。
  • RS232电平:规定-3~-15V为高电平(逻辑1),3~15V为低电平。
  • RS485电平:规定两线压差为+2~+6V为高电平(逻辑1),-2~-6为低电平。

像单片机这样的小型设备一般就是用TTL电平标准,后面两种电平标准都是用于需要高电压供电工作的。

接下来了解一下串口的数据帧吧。

首先了解一下波特率的概念,波特率是指1s传输码元的个数,单位为pbs/s。还有一种描述通信速率的概念是比特率,也就是1s传输比特的个数。对于二进制调制来说,1个码元就是1个比特,所以比特率和波特率是一样的,但是对于其他的一些进制来说,就不能划等号了。

数据帧一般是有10位或者9位组成的。分别为起始位,数据位,校验位,停止位。

  • 起始位:规定为逻辑电平0
  • 数据位:由8位组成
  • 校验位:校验数据位
  • 停止位:规定为逻辑电平1

这里串口的默认电平为高电平,所以如果有数据来了,需要起始位来判断有数据过来了,所以这里需要把电平拉低。当数据结束时,停止位要把电平恢复为高电平,来为下一位数据的接受做准备。

数据位一般是指有效载荷位,也就是8位。这里就是我们数据存储的地方,是低位先行原则。也就是数据为0X0F(00001111)时,数据位由低至高位存储,也就是11110000。

这里的校验位是用来判断数据是否正确的。我们可以配置参数为无校验,奇校验,偶校验。

奇校验确保了1的个数为奇数,如果接收方检测到数据有偶数个1时,则数据传输出现了错误,可以选择抛弃这个数据或者是重新传输。

偶校验反之。不过这种校验方法有局限性,当有偶数个数据传输出现错误时,数据的奇偶性是不变的,这个时候这种方法就不管用了。

接下来我们就来具体看一下串口通信的实际传输波形吧。

b3c5ea68aad3411a9a9a0004f7dfd416.jpg

 上图可以看出,串口实际上就是把电平不断置1置0来实现数据读取,通讯的。如果发送数据为0x55,那么串口的波形就应该是10101010(低位先行原则)。算上校验位的话,这些数据一共是有11位的,1个起始位,8个数据位,1个校验位,1个停止位。当然这里的停止位时间是我们可以配置的(0.5位1位,1.5位,2位),这个参数就表示两个数据帧的时间间隔了。

这里STM32的USART外设可以分为两大部分,发送数据与接收数据。TXD引脚会把数据寄存器里的数据自动转化为协议规定的波形(如上图)输出,然后RXD引脚可以接受TXD发送的数据波形,然后解码为数据。

STM32F103c8t6的串口资源为USART1(挂载在APB2总线上),USART2(挂载在APB1总线上),USART3(挂载在APB2总线上),

01a8c78b0f844993adf1e377508a6df4.jpg

 首先就是先来理一下这个框图了。

这里我们一般只会用到TX引脚与RX引脚,其他引脚先不管。

当发送数据时,TDR位就会存储数据,然后这里电路检测到数据后,就会判断发送数据位寄存器是否移位,把数据搬运到移位寄存器里面,如果移位了,这里会有一个TXE标志位置1,没有就置0。然后移位寄存器就会在发送控制器的控制下把数据从TX引脚发送出去。同时,当数据移位后,就代表下一个数据就会覆盖上一个数据。

当接收数据时,在接收器控制下,接受移位寄存器会从RX引脚读取到数据,此时也是一样的,如果移位寄存器已经完成把数据搬运到RDR这个寄存器里面,也就是完成了移位这个过程(RXNE位就会置1),那么接受移位寄存器就会有新数据覆盖原来的数据。

2bd60219ce5c4738b59302c3da9d176a.jpg

 接下来了解一下硬件流控制,这也是USATR的一个小功能,可以保证数据接收的准确性,通俗来说就是确保设备准备好之后才会进行数据传输。这个功能怎么用呢?

硬件流控有两个引脚,分别是nRTS和nCTS,nRTS是用来发送高低电平信号的,准备好了就发送低电平,未准备好就发送高电平。nCTS是用来接收信号的,也就是接收高低电平信号。只有在接收到了低电平信号才会发送数据。

假设这里设备1发送数据,设备2接收数据。那么设备1的nCTS就接到设备2的nRTS引脚上,只有在设备1的nCTS接收到了来自设备2的nRTS的低电平信号时,设备1才会发送数据。

接下来看一下右上角的时钟信号的电路吧。

这个时钟信号是用来驱动发送数据寄存器移位的寄存器每移一个位,时钟就运行了一个周期。这里的时钟信号只支持输出,不支持输入,所以两个串口是不能进行同步通信的,不过这里的时钟信号可以用来兼容别的协议,比如SPI。另外,这里的时钟也可以用来计算发送设备的波特率。

然后是唤醒单元,这部分用来实现多设备之间的通讯。之前说过串口通信一边是点对点的通信,但是这个单元就可以实现在一条总线上挂载多个设备,当你想实现多设备的通信时,就给串口写一个指定地址,然后指定设备就会被唤醒开始工作。

852639f1d67a4ba0ba8527b450b398ec.jpg

 下面就是了解一下波特率发生器吧。

这个波特率发生器就是用来控制数据的移位与发送、接收的。波特率发生器是通过对应的APB总线上分频而得来的,这里可以配置一个分频系数来得到发送器和接收器的波特率,然后这个频率在通过16分频来得到一个控制发送器和接收器的时钟,这里后面会讲到为什么要16分频。这里的TE与RT标志位是用来使能波特率的,如果TE置1,发送器波特率就有效,RE置1,接收器波特率就有效。

29d8048f17e24873a04117d2cbb52ec3.jpg

 配置串口的总体框图如上。

这里的APB1/2通过波特率发生器分频后,得到一个时钟信号来控制发送器和接收器。发送器控制发送移位寄存器发送数据,接收器控制接收移位寄存器接受数据。然后通过GPIO口把数据通向对应的引脚。

发送数据很简单,只需要在时钟驱动下不断翻转电平就行了,但是接受数据就要严谨一点了,因为在实际的数据的传输过程中会有噪声影响,导致接受到的电平信号与原本发送的电平信号有差异,这个时候我们就要进行多次采样,然后取一个比较中间的信号,这样接受到的数据就会比较准确了。667cafa8567545c99fc86ca9d259fa1e.jpg

 具体做法如上,我们对一个数据帧进行16次采样,然后取一个比较靠近中间的采样信号,大概就是8.9.10这三个采样点,只要起始位的采样点在中间位,那么接下来的数据采样点也会在中间值,这样就确保了采样数据的准确性。这里对噪声也有一定的处理办法。这里怎么确定是起始位呢?解决办法就是对3.5.7进行一组采样判断,8.9.10进行一组采样判断,如果这两组采样中0的信号大于等于2次,就认为是起始位了。与此同时,如果0的信号采样为2次,那么会有一个噪声标志位置1,让我们知道接受到了噪声。为3次时这个噪声标志位为0,也就是没有噪声信号耦合。

3c541b6c8af14532a0899737aeae216e.jpg

 波特率公式如上图。

不难得知,采样频率=APB(1/2)÷分频系数,波特率=采样频率÷16。为什么要除以16呢,因为每采样16次才发送一位数据,所以这里波特率是小于采样频率的。

对应到上面的框图,分频之后还要除以16来得到一个时钟信号控制发送器和接收器。也就是说,这里的波特率和发送器、接收器的时钟频率是相等的。


接下来具体看一下代码部分吧。

串口通讯大致可以分为发送与接受两个部分。

  • 发送----初始化串口、调用库函数发送数据、电脑端显示数据

其中比较重要、基础的部分在初始化部分。首先确定我们要使用的资源是串口1,所以开启APB2时钟下的串口1时钟,然后对应引脚定义表得知串口1的TX引脚为PA9,RX引脚为PA10,所以我们需要开启GPIOA的Pin9与Pin10引脚的时钟,配置相关的结构体参数。PA9为发送引脚,设置为复用推挽输出模式。

初始化GPIO后,接下来初始化串口1,与GPIO一样利用结构体来配置相关参数。串口的相关参数有波特率,硬件流控制,串口模式选择(TX/RX/TX|RX),校验位,停止位,字节长度。

这里选择9600bps,不使用硬件流,TX(发送模式),没有校验位,1位停止位,8位。

最后使能一下串口1即可。这便是串口的初始化部分。

接下来是数据的发送,我们利用一个函数将其封装好,这里直接调用库函数USART_SendData(USART1,Byte)即可,最后要等待一下TXE标志位置1,该标志位置1后说明数据移位完毕,可以发送下一个数据了。该标志位不用手动清零,因为对DR寄存器的写操作会使该标志位清零,也就是说,每次调用发生函数(写操作)就会清零。因为该函数是一个字节一个字节的发送的,所以如果要发送多字节就需要利用for循环依次发生数据。

void  Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启串口1时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA时钟GPIO_InitTypeDef GPIO_InitStructure;//初始化GPIIOAGPIO_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;//初始化串口1USART_InitStructure.USART_BaudRate=9600;USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode=USART_Mode_Tx;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_Cmd(USART1,ENABLE);//使能串口1
}void Serial_Sendbyte(uint8_t Byte)//发送1个字节
{USART_SendData(USART1,Byte);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}void Serial_Sendstring(char *string)//发送一串字符
{for(uint8_t i=0;string[i]!=0;i++){Serial_Sendbyte(string[i]);}
}

如上图所示,接受模式选择为文本模式之后,0x3d被翻译为对应的ASCI值‘=’。

  • 发送+接受----初始化串口、调用库函数发送数据与接收数据、电脑端发送数据,STM32接收数据+STM32发送数据,电脑端接收数据

接受部分与发送部分大差不差,只是在初始化部分需要将串口的模式修改为TX|RX,其他部分不改变。

在数据接收部分,调用库函数USART_ReceiveData(USART1)接受数据即可。那么这里需要考虑一下在什么时间段接收数据呢?数据的发送时间是不确定的,因此程序需要一直访问是否有数据发送过来,这里我们可以用RXNE(RX移位寄存器非空)标志位来判断,该标志位说明RX移位寄存器接受到了数据并将数据移位到了DR寄存器中等待用户将其读出来。这里可以在主函数的while循环中一直判断,但是这样比较消耗资源,所以考虑用中断。

因此初始化还需要加上中断的初始化,并开启NVIC到串口1的中断 USART_ITConfig()。

在中断函数USART1_IRQHandler()中判断标志位是否置1,若置1则将数据读出来(对DR寄存器的读操作会将RXNE寄存器清零)即可。

然后利用函数封装接受一下数据即可。

void  Serial_Init(void)//初始化
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);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);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_InitTypeDef USART_InitStructure;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);//开启串口1的中断NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC结构体初始化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);
}
uint8_t Serial_GetRxFlag(void)//自定义RXNE标志位
{if(Serial_RxFlag==1){Serial_RxFlag=0;return 1;}return 0;
}uint8_t Serial_GetRxdata(void)//数据接受
{return Serial_Rxdata;
}void USART1_IRQHandler()//串口1中断函数
{if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)//若标志位置1{Serial_Rxdata= USART_ReceiveData(USART1);//读取数据Serial_RxFlag=1;//自定义标志位USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位(非必要)}
}

可以看到,电脑端发送数据12之后,STM32接收到了数据12,同时利用Serial_Sendbyte()将数据data发送到电脑端,可以看到串口助手的接收区显示数据12。

以上部分就是串口独立数据的收发功能介绍,接下来再介绍数据包的收发如何处理。所谓数据包就是一串连续相关的数据,比如x,y,z三个数据一连串的发送了过来,那么这串数据就是以三个为一组循环往复发送。如果有一组数据11 22 33 44 55 66发送了过来,我们如何判断那些数据为一组呢?这里就要我们要规定数据包的包头与包尾的相关信息了。如果我们的数据长度固定为3,包头信息固定为‘AA’,包尾信息固定为‘FF’,这个时候接收方就可以通过判断包头来对齐数据包,当数据数量达到了三个之后再判断是否接受到了包尾,完成一组数据的接受。这便要求我们在发送数据时要按照规定好的格式来发送。

  • 那么我们就先来介绍串口收发hex数据包。

发送数据与上文提到的一样,需要手动发送包头与包尾信息。

void Serial_sendpacket()//发送数据包
{Serial_Sendbyte(0xAA);Serial_Sendarr(Txpack,4);Serial_Sendbyte(0xFF);
}

接受数据包就需要利用到状态机思想。

在中断函数中,首先设置一个状态变量,初始化为0,每接受到一个数据就判断是否为包头,如果为包头,就将状态变量设置为1,接下来就开始接受数据,将接受到的数据存档。再另外设置一个数据计数器,如果计数器数量达到了3,就将状态变量设置为2,不要忘记把计数器清零。接下来继续判断包尾,如果是包尾,就立一个标志位(表示该组数据接受完毕),同时将状态变量设置为0.准备接受下一组数据的包头。

void USART1_IRQHandler()
{static uint8_t Rxstate=0;static uint8_t data_p=0;if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET){uint8_t Rxdata=USART_ReceiveData(USART1);if(Rxstate==0)//等待包头{if(Rxdata==0xAA) Rxstate=1;}else if(Rxstate==1)//接收数据{Rxpack[data_p++]=Rxdata;if(data_p>=3){Rxstate=2;data_p=0;}					}else if(Rxstate==2)//等待包尾{if(Rxdata==0xFF){Rxstate=0;Serial_RxFlag=1;}					}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}
  • 接下来再介绍串口收发文本数据包(不规定数据长度)

上文介绍到的hex数据包数据长度是固定的,那么长度不固定的数据包如何做判断呢?其实都是换汤不换药,我们只需要在接收数据的同时判断是否收到了包尾,如果收到了包尾,那么就进入下一个状态即可。

这里规定包头为字符‘@’,包尾为字符‘\r\n’(换行符),也就是说,包尾有两个字符,在判断到字符‘\r’之后,继续判断字符‘\n’,字符‘\n’判断完毕之后就可以立一个标志位代表这组数据接受完毕了。把状态变量置0,准备接受下一组数据的包头。

void USART1_IRQHandler()
{static uint8_t Rxstate=0;static uint8_t data_p=0;if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET){uint8_t Rxdata=USART_ReceiveData(USART1);if(Rxstate==0)//等待包头{if(Rxdata=='@') {Rxstate=1;data_p=0;}}else if(Rxstate==1)//接收数据{if(Rxdata=='\r'){Rxstate=2;}else{Rxpack[data_p++]=Rxdata;}}else if(Rxstate==2)//等待包尾{if(Rxdata=='\n'){Rxstate=0;Rxpack[data_p]='\0';Serial_RxFlag=1;}					}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}

以上便是串口部分的所有介绍,若有错误请指正!

http://www.lryc.cn/news/624022.html

相关文章:

  • # C++ 中的 `string_view` 和 `span`:现代安全视图指南
  • 多墨智能-AI一键生成工作文档/流程图/思维导图
  • Transformer 面试题及详细答案120道(61-70)-- 解码与生成
  • Spring IOC 学习笔记
  • Spring 创建 Bean 的 8 种主要方式
  • Vue3 中的 ref、模板引用和 defineExpose 详解
  • 数据结构初阶(18)快速排序·深入优化探讨
  • 【深度学习-基础知识】单机多卡和多机多卡训练
  • oom 文件怎么导到visualvm分析家
  • 生成模型实战 | InfoGAN详解与实现
  • 停车位 车辆
  • AI出题人给出的Java后端面经(十七)(日更)
  • 【URP】[法线贴图]为什么主要是蓝色的?
  • YoloV9改进策略:Block改进-DCAFE,并行双坐标注意力机制,增强长程依赖与抗噪性-即插即用
  • LangChain4j
  • Java 学习笔记(基础篇4)
  • C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
  • JavaScript 性能优化实战:从评估到落地的全链路指南
  • SparkSQL性能优化实践指南
  • 第16节:自定义几何体 - 从顶点构建3D世界
  • 【FreeRTOS】刨根问底6: 应该如何防止任务栈溢出?
  • 【网络安全】Webshell的绕过——绕过动态检测引擎WAF-缓存绕过(Hash碰撞)
  • 什么是GD库?PHP中7大类64个GD库函数用法详解
  • 日语学习-日语知识点小记-进阶-JLPT-N1阶段蓝宝书,共120语法(3):21-30语法
  • 【AI论文】序曲(PRELUDE):一项旨在考察对长文本语境进行全局理解与推理能力的基准测试
  • PHP静态类self和static用法
  • 6-服务安全检测和防御技术
  • Tomcat Service 服务原理
  • Coin与Token的区别解析
  • java八股文-(spring cloud)微服务篇-参考回答