串口通讯

串口通讯

概念

什么是串口通讯与并口通讯的区别?

  1. 数据传输方式不同 串口通讯:串行传输,即一位一位地传输数据,数据传输速率较慢。 并口通讯:并行传输,即同时传输多个数据位,数据传输速率较快。

  2. 数据线数量不同 串口通讯:只有两根数据线(TXD和RXD),因此连接简单,但速率较慢。 并口通讯:需要多根数据线(8根或16根),因此连接较为复杂,但速率较快。

  3. 传输距离和数据速率限制不同 串口通讯:适用于短距离通讯,传输速率一般在115200bps以下。 并口通讯:适用于长距离通讯,传输速率可达数百Mbps。

  4. 常用设备不同 串口通讯:串口通讯常用于单片机、调试工具、传感器等设备之间的通讯。 并口通讯:并口通讯常用于打印机、扫描仪、显示器等设备之间的通讯。 综上所述,串口通讯和并口通讯各有优缺点,应根据实际需要选择合适的通讯方式。串口通讯适用于短距离、低速率的通讯,而并口通讯适用于长距离、高速率的通讯。

UART就是串口通讯吗

UART全称是通用异步收发器,是串口的一种,但不能说串口通讯就是UART,只是UART可以说是最常用的串口通讯方式。

UART与USART的区别

  1. 通讯方式不同 UART:UART是异步通讯协议,使用一个引脚用于传输数据,即TX(发送)和RX(接收)引脚。 USART:USART可以同时支持异步和同步通讯,可以通过配置来选择使用异步或同步通讯。同步通讯使用时钟引脚,异步通讯使用TX和RX引脚。

  2. 数据传输速率不同 UART:UART通常支持的速率较低,一般在1Mbps以下。 USART:USART支持的速率较高,可以达到数十Mbps。

  3. 数据格式不同 UART:UART只支持固定的数据格式,如8个数据位、无校验位、1个停止位。 USART:USART可以通过配置支持不同的数据格式,如5-9个数据位、奇偶校验位、1-2个停止位等。

  4. 功能不同 UART:UART只能支持基本的数据传输功能,不支持高级功能,如硬件流控制、多主机通讯等。 USART:USART支持多种高级功能,如硬件流控制、多主机通讯、自动波特率检测等。 综上所述,UART和USART虽然都是串口通讯协议,但是它们的通讯方式、数据传输速率、数据格式和功能都有所不同。在选择使用时需要根据实际需求选择合适的协议。

UART与RS485 RS232的关系

UART,是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),既然是“器”,显然,它就是个设备而已,要完成一个特定的功能的硬件。

RS232/RS485,是两种不同的电气协议,也就是说,是对电气特性以及物理特性的规定,作用于数据的传输通路上,它并不内含对数据的处理方式。比如,最显著的特征是:RS232使用3-15v有效电平,而UART,因为对电气特性并没有规定,所以直接使用CPU使用的电平,就是所谓的TTL电平(可能在0~3.3V之间)。更具体的,电气的特性也决定了线路的连接方式,比如RS232,规定用电平表示数据,因此线路就是单线路的,用两根线才能达到全双工的目的;而RS485, 使用差分电平表示数据,因此,必须用两根线才能达到传输数据的基本要求,要实现全双工,必需用4根线。但是,无论使用RS232还是RS485,它们与UART是相对独立的,但是由于电气特性的差别,必须要有专用的器件和UART接驳

从某种意义上,可以说,线路上存在的仅仅是电流,RS232/RS485规定了这些电流在什么样的线路上流动和流动的样式;在UART那里,电流才被解释和组装成数据,并变成CPU可直接读写的形式。

编程时候,无论是232还是485就是单纯的用UART进行通讯实验。232和485只是在硬件电路图上存在区别。

也可以从以下几个方面总结:

  1. 信号电平不同 UART使用TTL电平,RS232使用正负12V的电平,而RS485使用差分信号,即两条信号线分别传输正负电平。

  2. 通信距离不同 UART的通信距离较短,一般在几米范围内;RS232的通信距离可达15米左右;而RS485的通信距离可达1200米。

  3. 传输速率不同 UART的传输速率一般较低,最高只能达到115200bps;RS232的传输速率可达115200bps;而RS485的传输速率可达10Mbps。

  4. 支持设备数量不同 UART只支持点对点通信,即只能连接两个设备;RS232支持一对一或一对多的连接方式,可以连接多个设备;而RS485支持多对多的连接方式,可以连接多个设备。

  5. 网络拓扑结构不同 UART没有特定的网络拓扑结构;RS232是点对点或星型拓扑结构;而RS485是总线型拓扑结构。

ModuBus

规定数据帧结构,是软件层面的协议,是一个应用层的协议,modbus可用于232和485,也就是说使用ModuBus的前提是完成UART的驱动

常用的串行通讯协议

单工通信:数据只能沿一个方向传输

半双工通信:数据可以沿两个方向传输,但需要分时进行

全双工通信:数据可以同时进行双向传输

同步通信:共用同一时钟信号

异步通信:没有时钟信号,通过在数据信号中加入起始位和停止位等一些同步信号

通信接口
接口引脚
数据同步方式
数据传输方向

UART (通用异步收发器)

TXD:发送端 RXD:接收端 GND:公共地

异步通信

全双工

1-wire

DQ:发送/接收端

异步通信

半双工

IIC

SCL:同步时钟 SDA:数据输入/输出端

同步通信

半双工

SPI

SCK:同步时钟 MISO:主机输入,从机输出 MOSI:主机输出,从机输入 CS:片选信号

同步通信

全双工

奇偶校验

要发送的字节是0x1a,二进制表示为0001 1010。

  • 采用奇校验,则在数据后补上个0,数据变为0001 1010 0,数据中1的个数为奇数个(3个)

  • 采用偶校验,则在数据后补上个1,数据变为0001 1010 1,数据中1的个数为偶数个(4个)

    接收方通过计算数据中1个数是否满足奇偶性来确定数据是否有错。

    需要将WordLength选择为UART_WORDLENGTH_9B。因为使用奇偶校验时,数据帧的总长度会增加一位,因此需要将WordLength设置为9位,以保证正确的接收和发送

停止位校验

停止位校验必须有,可以有0.5、1、1.5、2.0个位,保持逻辑1电平。但是在STM32的HAL库中似乎只能选择1个或者2个位

USART_DR 寄存器

USART_DR 串口数据(Data)寄存器;这是一个双寄存器,包含了TDR和RDR,对它读操作,读取的是RDR寄存器的值,对它的写操作,实际上是写到TDR寄存器的;当向该寄存器写数据的时候,串口就会自动发送,当收到收据的时候,也是存在该寄存器内。

接收与传送过程数据框图

如下图,在这个过程我们其实操作的只有USART_DR 寄存器剩下的有硬件来完成

USART_DR寄存器只有8位,因此1次只能接收8位,1个字节的数据,且接收导数据需要立即将其从DR寄存器读出,否则后续的数据会自动传至DR寄存器,覆盖原有数据,造成数据丢失,所以通常使用中断方式进行数据接收

UART的驱动

确定使用的串口号及对应的引脚

STM32F103ZET6共有3个USART,2个UART,这些数据可在选型手册获取,也可在对应芯片的数据手册的第一页获取,不过在USART的描述上似乎不太准确,这边直接说有5个USART显然是不准确的

查看参考手册8.3.3小节,可得到USART的复用功能重映射,下面以USART1为例

不过此处可能只有USART1-3的引脚,UART4,UART5没有,需要查看数据手册,手动搜索获得,值得注意的是在F103ZET6的数据手册的翻译版中由于翻译错误,将UART4,UART5写作USART4,USART5,因此需要搜索USART4,USART5,英文原版没有这个错误

USART的中断类型

USART1==只有一个中断向量==,但是可以通过配置USART的寄存器来选择不同的中断,也就是说可以将多个事件都链接到这个中断向量上。可以查看参考手册25.4章节

在HAL库stm32f1xx_hal_uart.h进行了宏定义

#define UART_IT_PE                       ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_PEIE))
#define UART_IT_TXE                      ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TXEIE))
#define UART_IT_TC                       ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TCIE))
#define UART_IT_RXNE                     ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_RXNEIE))
#define UART_IT_IDLE                     ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_IDLEIE))

#define UART_IT_LBD                      ((uint32_t)(UART_CR2_REG_INDEX << 28U | USART_CR2_LBDIE))

#define UART_IT_CTS                      ((uint32_t)(UART_CR3_REG_INDEX << 28U | USART_CR3_CTSIE))
#define UART_IT_ERR                      ((uint32_t)(UART_CR3_REG_INDEX << 28U | USART_CR3_EIE))

USART的中断触发机制

具体的可以查看USART_CR1寄存器,正如手册所描述的设置对应位为1,就会触发USART中断向量,调用服务函数

以接收中断为例,当USART_SR中的RXNE为’1’时,产生USART中断

而USART_SR是状态寄存器,描述如下

当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位。如果 USART_CR1寄存器中的RXNEIE为1,则产生中断。

USART1的驱动与回调机制

初始化与回调

HAL_UART_Init()函数会调用HAL_UART_MspInit()

  • 只得注意的是,这里有两个缓存区

  • 一个是给HAL库接收函数用的g_rx_buffer,缓存大小为1字节,只是为了在接收到数据后直接从DR寄存器将数据取出来存到这个缓存区

    另一个是我们真正处理数据用的数据缓存g_usart_rx_buf,缓存大小一般大于1字节,会在g_rx_buffer中的数据合法的情况下将数据存入g_usart_rx_buf

    之所以这样是因为,数据接收需要协议,HAL库的UART_Receive_IT函数只是单纯的将数据进行读取,不涉及到到数据协议,我们需要区分一次数据读取是否完成,就需要有数据帧的概念,即如何去判断一帧数据的完结,这就涉及到数据协议的概念,在回调函数HAL_UART_RxCpltCallback中就是根据数据协议的要求将数据存到g_usart_rx_buf

/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];

/*  接收状态
 *  bit15,      接收完成标志
 *  bit14,      接收到0x0d
 *  bit13~0,    接收到的有效字节数目
*/
uint16_t g_usart_rx_sta = 0;

uint8_t g_rx_buffer[RXBUFFERSIZE];  /* HAL库使用的串口接收缓冲 */

UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */

/**
 * @brief       串口X初始化函数
 * @param       baudrate: 波特率, 根据自己需要设置波特率值
 * @note        注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
 *              这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
 * @retval
 */
void usart_init(uint32_t baudrate)
{
    /*UART 初始化设置*/
    g_uart1_handle.Instance = USART_UX;                                       /* USART_UX */
    g_uart1_handle.Init.BaudRate = baudrate;                                  /* 波特率 */
    g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                      /* 字长为8位数据格式 */
    g_uart1_handle.Init.StopBits = UART_STOPBITS_1;                           /* 一个停止位 */
    g_uart1_handle.Init.Parity = UART_PARITY_NONE;                            /* 无奇偶校验位 */
    g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /* 无硬件流控 */
    g_uart1_handle.Init.Mode = UART_MODE_TX_RX;                               /* 收发模式 */
    HAL_UART_Init(&g_uart1_handle);                                           /* HAL_UART_Init()会使能UART1 */

    /* 该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量 */
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE); 
}

/**
 * @brief       UART底层初始化函数
 * @param       huart: UART句柄类型指针
 * @note        此函数会被HAL_UART_Init()调用
 *              完成时钟使能,引脚配置,中断配置
 * @retval
 */
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1,进行串口1 MSP初始化 */
    {
        USART_TX_GPIO_CLK_ENABLE();                             /* 使能串口TX脚时钟 */
        USART_RX_GPIO_CLK_ENABLE();                             /* 使能串口RX脚时钟 */
        USART_UX_CLK_ENABLE();                                  /* 使能串口时钟 */

        gpio_init_struct.Pin = USART_TX_GPIO_PIN;               /* 串口发送引脚号 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* IO速度设置为高速 */
        HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
                
        gpio_init_struct.Pin = USART_RX_GPIO_PIN;               /* 串口RX脚 模式设置 */
        gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;    
        HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);   /* 串口RX脚 必须设置成输入模式 */
        
#if USART_EN_RX
        HAL_NVIC_EnableIRQ(USART_UX_IRQn);                      /* 使能USART1中断通道 */
        HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3);              /* 组2,最低优先级:抢占优先级3,子优先级3 */
#endif
    }
}
  • HAL_UART_Init():该函数是针对应用程序层的,用于对UART外设进行配置和初始化。它会根据用户传入的参数对UART的各个寄存器进行配置,包括波特率、数据位、停止位、校验等参数。

  • HAL_UART_MspInit():该函数是针对底层的,用于初始化UART外设的底层硬件资源,如GPIO、DMA、NVIC等。它会对UART所需要的所有底层硬件资源进行初始化、配置和启用,以确保UART能够正常工作。

因此,HAL_UART_Init()HAL_UART_MspInit()两个函数本质上是不同的。HAL_UART_MspInit()函数在HAL_UART_Init()函数中被调用,确保底层硬件资源的正常初始化。这种分离的设计可以使得应用程序层与底层硬件资源之间进行解耦,从而提高代码的可维护性和可移植性。

值得注意的是:

1、开启接收中断

HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);

1.首先判断是否已经在进行接收操作,如果是,则返回 HAL_BUSY,否则继续执行。 2.检查 pDataSize 参数是否为空或为0,如果是,则返回 HAL_ERROR。 3.对UART进行加锁(因为是多任务环境下,可能会有多个任务同时在使用UART,需要进行加锁)。 4.将用户提供的接收缓冲区指针 pData 和缓冲区大小 Size 存到UART句柄结构体变量的成员变量中。

这个缓存区大小需要根据需求自行设置 5.清除错误码,并将接收状态设置为 HAL_UART_STATE_BUSY_RX。 6.解锁UART。 7.使能UART的奇偶校验错误中断、帧错误中断、噪声错误中断、接收数据寄存器非空中断。 8.返回 HAL_OK。 该函数的主要作用是设置UART的接收参数,并使能相应的中断,等待数据到来。当数据到来时,会触发UART接收中断,中断服务程序会把接收到的数据存储到用户提供的接收缓冲区中,在中断服务程序执行完毕后,用户就可以在应用程序中读取接收缓冲区中的数据了。

2、与之类似的,开启发送

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

中断与回调

HAL库的串口接收嵌套了较多层,关系比较复杂,

  • void USART1_IRQHandler(void)是串口中断服务函数(这里宏定义重命名为USART_UX_IRQHandler),是当发生串口中断后首先调用的函数

void USART_UX_IRQHandler(void)
{
#if SYS_SUPPORT_OS                                                   /* 使用OS */
    OSIntEnter();
#endif
    HAL_UART_IRQHandler(&g_uart1_handle);                               /* 调用HAL库中断处理公用函数 */

    while (HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE) != HAL_OK)     /* 重新开启中断并接收数据 */
    {
        /* 如果出错会卡死在这里 */
    }

#if SYS_SUPPORT_OS                                                   /* 使用OS */
    OSIntExit();
#endif
}
  • 在中断调用HAL_UART_IRQHandler(&UART1_Handler);这是一个HAL库函数,该函数是一个HAL库函数,在该函数中调用UART_Receive_IT(huart);,在函数 UART_Receive_IT中会把数据保存在串口句柄的成员变量pRxBuffPtr缓存中,同时RxXferCount 计数器减 1(其实我没找到减1的语句)。如果我们设置 RxXferSize=10,那么当接收到 10 个字符之后, RxXferCount 会由 10 减到 0( RxXferCount 初始值等于 RxXferSize),这个时候再调用接收完成回调函数 HAL_UART_RxCpltCallback 进行处理。

  • 也就是说HAL_UART_RxCpltCallback是在中断接收完成之后处触发的。

总的来说用HAL库的回调去实现串口收发还是有点复杂,嵌套太多层,其实可以不使用这些回调函数,直接在中断向量服务函数实现自己的任务逻辑即可

不使用回调的串口中断服务

以下代码编写了一个接收协议,接收到的数据必须是0x0d(回车/r的ASCII码13,对应的控制字符CR) 0x0a(换行/n的ASCII码)结尾

void USART1_IRQHandler(void)
{
	u8 Res;
	HAL_StatusTypeDef err;
#if SYSTEM_SUPPORT_OS	 	//使用OS
	OSIntEnter();
#endif
	if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET))  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
		Res=USART1->DR;
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了0x0d
			{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了
			}
			else //还没收到0X0D
			{
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
				}
			}
		}
	}
	HAL_UART_IRQHandler(&UART1_Handler);
#if SYSTEM_SUPPORT_OS	 	//使用OS
	OSIntExit();
#endif
}

主要使用到了__HAL_UART_GET_FLAG(HANDLE, FLAG)函数

__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET

这段代码是一个宏定义,用于检查串口(UART)外设的指定标志位是否被设置。

具体代码如下:

#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))

该宏有两个参数:

HANDLE 指定了串口句柄:

其实就是前面初始化USART1的时候实例化的UART_HandleTypeDef型的结构体

FLAG 参数可以取以下值之一:

  • UART_FLAG_CTS:CTS 变化标志(UART4 和 UART5 不支持)

  • UART_FLAG_LBD:LIN 中断检测标志

  • UART_FLAG_TXE:发送寄存器为空标志

  • UART_FLAG_TC:传输完成标志

  • UART_FLAG_RXNE:接收寄存器非空标志

  • UART_FLAG_IDLE:空闲线检测标志

  • UART_FLAG_ORE:溢出错误标志

  • UART_FLAG_NE:噪声错误标志

  • UART_FLAG_FE:帧错误标志

串口数据的发送

Last updated