UART全称为通用异步收发器,英文全称(Universal Asynchronous Receiver/Transmitter)。是一种串行、异步、全双工的通信协议。
一、使用方法
UART首先将接收到的并行数据转换成串行数据来传输,消息帧从一个低位起始位开始,后面是5-8个数据位,一个可用的奇偶位和一个或几个高低停止位。
数据传输模拟图
发送过程:接收器发现开始位时它就知道数据准备发送,并尝试与发送器时钟频率同步。如果选择了奇偶,UART就在数据位后面加上奇偶位。奇偶位可用来帮助校验错误。
接收过程:UART从消息帧中去掉起始位和结束位,对进来的字节进行奇偶校验,并将数据字节从串行转换成并行。UART也产生额外的信号来指示发送和接收的状态。例如,如果产生一个奇偶错误,UART就置位奇偶标志。
在UART通讯协议中信号线上的状态位高电平表示‘1’,低电平代表‘0’,特点是:通信线路简单,只要一对传输线就可以实现双向通信,大大降低了成本,但传输速度慢,传输距离短。
二、UART数据协议
在串口通信中,最主要的是数据流以及波特率。在传输过程中,需要关注的是起始位、数据位、校验位、停止位、波特率。
起始位:通信线路空闲时为“1”,当检测到下降沿时,认为数据开始传输;
有效数据位:传输开始后传递需要发送和接收的数据值,可以是指令或者数据;
校验位:一般为奇偶校验,即通过来校验传输数据中‘1’的个数为奇数个(奇校验)或者是偶数个(偶校验)来表示传输的数据正确与否;
停止位:数据传输结束后,传输线恢复成‘1’状态;
波特率:1秒内传输信号的状态数(波形数)。比特率:1秒内传输数据的bit。如果是一个波形,能表示N个bit,那么,波特率 * N = 比特率。
三、STM32CubeMX配置USART
在CubeMX新建项目选择STM32F103C6开发板,配置USART1,配置使用Asynchronous异步模式,如下图:
实现中断方式需要选择NVIC配置,将Enabled选项勾选,如下图:
实现DMA方式,需要在usart1中作如下设置:
四、代码实现
/*定义一个huart1变量*/ UART_HandleTypeDef huart1; /* USART1 init function */ void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } /*PA9为复用 推挽输出模式、PA10为浮空输入模式*/ void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(uartHandle->Instance==USART1) { /* USER CODE BEGIN USART1_MspInit 0 */ /* USER CODE END USART1_MspInit 0 */ /* USART1 clock enable */ __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /**USART1 GPIO Configuration PA9 ------> USART1_TX PA10 ------> USART1_RX */ GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } }
1、轮询收发方式
在主函数中调用HAL_UART_Transmit即可在串口调试助手显示出我们定义的字符串变量。
/*函数原型HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)*/ /* USER CODE BEGIN 1 */ extern UART_HandleTypeDef huart1; char *str1 = "I from www.100ask.net\r\n"; char *message = "I am DshanMCU-F103\r\n"; char c; /* USER CODE END 1 */ HAL_UART_Transmit(&huart1, (uint8_t *)message, strlen(message), 100); HAL_UART_Transmit(&huart1, (uint8_t *)str1, strlen(str1), 100);
实验现象
2、中断收发方式
中断方式实现串口通信需要对NVIC进行设置,对内置函数不能之间修改,我们需要自己作函数定义,通过使用环形缓冲区来实现多数据的传输,避免数据丢失,将数据存储入环形缓冲区,从而实现串口通信。
uart.c
/* USER CODE BEGIN 0 */ #include "circle_buffer.h" /* USER CODE END 0 */ /* USER CODE BEGIN 1 */ static volatile int g_tx_cplt = 0; static volatile int g_rx_cplt = 0; static uint8_t g_RecvBuf[100]; static uint8_t g_RecvChar; static circle_buf g_uart1_rx_bufs; void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { g_tx_cplt=1; } void Wait_Tx_Complete(void) { while(g_tx_cplt == 0); g_tx_cplt = 0; } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { g_rx_cplt=1; circle_buf_write(&g_uart1_rx_bufs, g_RecvChar); /*re-enable rxne interrupt*/ HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1); } void Wait_Rx_Complete(void) { while(g_rx_cplt == 0); g_rx_cplt = 0; } void StartUARTRecv(void) { /*init circle_buffer 用于存储数据 */ circle_buf_init(&g_uart1_rx_bufs, 100, g_RecvBuf); /*enable rxne interrupt*/ HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1); } int UARTGetChar(uint8_t *pVal) { return circle_buf_read(&g_uart1_rx_bufs, pVal); } /* USER CODE END 1 */
circle_buffer.h
#ifndef _CIRCLE_BUF_H #define _CIRCLE_BUF_H #include <stdint.h> typedef struct circle_huf{ uint32_t r; uint32_t w; uint32_t len; uint8_t *buf; }circle_buf,*p_circle_buf; void circle_buf_init(p_circle_buf pCircleBuf,uint32_t len,uint8_t *buf); int circle_buf_read(p_circle_buf pCircleBuf,uint8_t *pVal); int circle_buf_write(p_circle_buf pCircleBuf,uint8_t val); #endif
circle_buffer.c
#include "stdint.h" #include "circle_buffer.h" /* 环形缓冲区初始化,起始为空 三个参数分别是指向环形缓冲区结构体的指针 缓冲区长度 指向缓冲区内存的的指针 配置传入的长度和缓冲区地址赋值给结构体成员变量 */ void circle_buf_init(p_circle_buf pCircleBuf,uint32_t len,uint8_t *buf) { pCircleBuf->r = pCircleBuf->w = 0; pCircleBuf->len = len; pCircleBuf->buf = buf; } /* 函数功能:从环形缓冲区中读取数据的函数 参数:指向环形缓冲区结构体的指针,存储读取数据的指针pVal 功能解释:r!=w时 有数据可读,读取当前位置数据,读指针向后移动一位 成功读取返回为0,返回-1缓冲区为空 **/ int circle_buf_read(p_circle_buf pCircleBuf,uint8_t *pVal) { if(pCircleBuf->r != pCircleBuf->w){ *pVal = pCircleBuf->buf[pCircleBuf->r]; pCircleBuf->r++; if(pCircleBuf->r == pCircleBuf ->len) pCircleBuf->r=0; return 0; } else { return -1; } } /** 函数功能:向缓冲区写入数据函数 参数:指向缓冲区的指针 写入的数据val 功能解释:如果next_w==w,表示缓冲区已满,无法写入返回-1 若返回0表示写入成功 **/ int circle_buf_write(p_circle_buf pCircleBuf,uint8_t val) { uint32_t next_w; next_w = pCircleBuf->w + 1; if(next_w == pCircleBuf->len) next_w = 0; if(next_w != pCircleBuf->r) { pCircleBuf->buf[pCircleBuf->w] = val; pCircleBuf->w = next_w; return 0; } else { return -1; } }
main.c
/* USER CODE BEGIN 1 */ extern UART_HandleTypeDef huart1; char *str2 = "please enter a char:\r\n"; char c; /* USER CODE END 1 */ /* USER CODE BEGIN PV */ /*函数声明*/ void Wait_Tx_Complete(void); void Wait_Rx_Complete(void); void StartUARTRecv(void); int UARTGetChar(uint8_t *pVal); /* USER CODE END PV */ /*enable rxne interrupt*/ StartUARTRecv(); while (1) { HAL_UART_Transmit_IT(&huart1, (uint8_t *)str2, strlen(str2)); /*wait for tc*/ Wait_Tx_Complete(); while(UARTGetChar(&c) != 0); c+=1; HAL_UART_Transmit(&huart1, &c, 1, 100); HAL_UART_Transmit(&huart1, "\r\n", 2, 100); /* USER CODE END WHILE */ }
实验现象
3、DMA方式
只是简单使用DMA方式的话,在配置好USART的DMA设置之后,更改函数HAL_UART_Transmit_IT为HAL_UART_Transmit_DMA就可以实现函数功能了。使用DMA方式配合IDLE空闲函数的话,能够实现它的功能。
IDLE,空闲的定义是:总线上在一个字节的时间内没有再接收到数据。 UART 的 IDLE 中断何时发生?RxD 引脚一开始就是空闲的啊,难道 IDLE 中断一直产生? 不是的。当我们使能 IDLE 中断后,它并不会立刻产生,而是:至少收到 1 个数据后,发现 在一个字节的时间里,都没有接收到新数据,才会产生 IDLE 中断。
uart.c
/* USER CODE BEGIN 1 */ static volatile int g_tx_cplt = 0; static volatile int g_rx_cplt = 0; //static uint8_t g_RecvChar; static uint8_t g_RecvTmpBuf[10]; static uint8_t g_RecvBuf[100]; static circle_buf g_uart1_rx_bufs; void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { g_tx_cplt = 1; } void Wait_Tx_Complete(void) { while (g_tx_cplt == 0); g_tx_cplt = 0; } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { g_rx_cplt = 1; /* put data to circle buffer */ for (int i = 0; i < 10; i++) { circle_buf_write(&g_uart1_rx_bufs, g_RecvTmpBuf[i]); } /* re-use dma+idle to recv */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10); } void Wait_Rx_Complete(void) { while (g_rx_cplt == 0); g_rx_cplt = 0; } void StartUART1Recv(void) { /* init circle buffer */ circle_buf_init(&g_uart1_rx_bufs, 100, g_RecvBuf); /* use dma+idle to recv */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10); } int UART1GetChar(uint8_t *pVal) { return circle_buf_read(&g_uart1_rx_bufs, pVal); } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { for (int i = 0; i < Size; i++) { circle_buf_write(&g_uart1_rx_bufs, g_RecvTmpBuf[i]); } /* re-use dma+idle to recv */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10); } /* USER CODE END 1 */
main.c
int main(void) { /* USER CODE BEGIN 1 */ extern UART_HandleTypeDef huart1; char *str1 = "I from www.100ask.net\r\n"; char *message = "I am DshanMCU-F103\r\n"; char *str2 = "please enter a char:\r\n"; char c; /* USER CODE END 1 */ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_DMA_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); /* Infinite loop */ /* USER CODE BEGIN WHILE */ /*enable rxne interrupt*/ StartUART1Recv(); HAL_UART_Transmit(&huart1, (uint8_t *)message, strlen(message), 100); HAL_UART_Transmit(&huart1, (uint8_t *)str1, strlen(str1), 100); while (1) { HAL_UART_Transmit_DMA(&huart1, (uint8_t *)str2, strlen(str2)); while(0 != UART1GetChar(&c)); c+=1; HAL_UART_Transmit(&huart1, &c, 1, 100); HAL_UART_Transmit(&huart1, "\r\n", 2, 100); }
实验现象
五、总结
①.UART协议的优点
-
简单的硬件需求:只需要两根数据线,一个用来发送(TX),一个用来接收(RX),由于是异步通信,不需要额外的时钟信号线,进一步减少了硬件需求。
-
全双工通信能力:UART允许设备在一根线上发送数据,同时在另一根线上接收数据,实现全双工通信,提高了数据传输效率。
-
配置灵活性:用户可以根据需求配置不同的波特率和数据格式,提高了灵活的通信速率和适应不同通信场景的能力。
-
错误检测和处理机制:UART通过奇偶校验机制能够检测单个比特的错误,确保数据传输的正确性,并能够指示帧错误、奇偶校验错误等多种错误状态,有助于及时发现并处理通信中的问题。
-
中断驱动的操作方式:支持中断驱动的操作模式,可以在不占用CPU的情况下进行数据传输,从而释放CPU资源执行其他任务。
②.UART协议的缺点
-
数据传输速率较低:与诸如SPI或I2C等其他串行通信协议相比,UART的数据传输速率通常较低,这限制了其在高速数据传输需求场合的应用。
-
数据丢失问题:当接收端的数据缓冲区已满而无法及时处理新数据时,可能会发生数据丢失现象,这在一些高速或高容量数据传输应用中可能成为瓶颈。