UARA串口开发基础
目录
一、同步传输和异步传输
1.1、概念与示例
1.2、UART 协议与操作方法
1.2.1、UART 协议
1.2.2、STM32H5 UART 硬件结构
1.2.3、RS485 协议
1.3、UART 编程
1.3.1、三种编程方式
1.3.2、查询方式
1.3.3、中断方式
1.3.4、DMA方式
1.3.5、效率最高的 UART 编程方法
1.3.6、使用IDLE+DMA接收要点
1.4、UART+RTOS编程
1.5、结构体面对对象编程
一、同步传输和异步传输
1.1、概念与示例
- 同步:朋友打电话说到我家吃饭,我在家里等他们
- 异步:朋友没有提前打招呼,突然就到我家来了
它们的差别在于:有没有使用一种方法“实现约好时间”。
在电子产品中,使用同步传输时,一般涉及两个信号:
- 时钟信号:用来通知对方要读取数据了
- 数据信号:用来传输数据

① 起始信号:解码器发出一个 9ms 的低电平、4.5ms 的高电平,用来同时对方说"开始了"

③ 接收方、发送方都遵守这样的约定,就可以使用一条线传输数据
两者差别:
1.2、UART 协议与操作方法
1.2.1、UART 协议

数据传输流程如下:
- 平时数据线处于“空闭”状态(1 状态)。
- 当要发送数据时,UART 改变 TxD 数据线的状态(变为 0 状态)并维持 1 位的时间──这样接收方检测到开始位后,再等待 1.5 位的时间就开始一位一位地检测数据线的状态 得到所传输的数据。
- UART 一帧中可以有 5、6、7 或 8 位的数据,发送方一位一位地改变数据线的状态将它 们发送出去,首先发送最低位。
- 如果使用较验功能,UART 在发送完数据位后,还要发送 1 个较验位。有两种较验方法:奇较验、偶较验──数据位连同较验位中,“1”的数目等于奇数或偶数。
- 最后,发送停止位,数据线恢复到“空闭”状态(1 状态)。停止位的长度有 3 种:1 位、 1.5 位、2 位。

1.2.2、STM32H5 UART 硬件结构
FIFO:大大减少数据查询方式的情况
1.2.3、RS485 协议

1.3、UART 编程
1.3.1、三种编程方式
- 查询方式
- 中断方式
- DMA 方式

使用函数:
1.3.2、查询方式
/* 发送信息*/
HAL_UART_Transmit(&huart2, &c, 1, 100);/* 接收信号 */
err = HAL_UART_Receive(&huart4, &c, 1, 100);
这个方式是最狗屎的,因为需要一直访问寄存器,如果接收信息的代码不被一直执行就容易错过信息,要是移植访问寄存器又太废CPU了。
1.3.3、中断方式
这个方法中规中矩吧,如果在接收信息后再接收中断中重新开启接收中断就还是不错的接收信息方式,不过一直中断也不是个法子,主要看项目要求
static volatile int g_uart2_tx_complete = 0;
static volatile int g_uart4_rx_complete = 0;void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart2){g_uart2_tx_complete = 1;}
}int Wait_UART2_Tx_Complete(int timeout)
{while (g_uart2_tx_complete == 0 && timeout){vTaskDelay(1);timeout--;}if (timeout == 0)return -1;else{g_uart2_tx_complete = 0;return 0;}
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart4){g_uart4_rx_complete = 1;}
}int Wait_UART4_Rx_Complete(int timeout)
{while (g_uart4_rx_complete == 0 && timeout){vTaskDelay(1);timeout--;}if (timeout == 0)return -1;else{g_uart4_rx_complete = 0;return 0;}
}static void CH1_UART2_TxTaskFunction( void *pvParameters )
{uint8_t c = 0;while (1){/* 发送信息 */HAL_UART_Transmit_IT(&huart2, &c, 1);Wait_UART2_Tx_Complete(100);vTaskDelay(500);c++;}
}static void CH2_UART4_RxTaskFunction( void *pvParameters )
{uint8_t c = 0;int cnt = 0;char buf[100];HAL_StatusTypeDef err;while (1){/* 接受信息 */err = HAL_UART_Receive_IT(&huart4, &c, 1);if (Wait_UART4_Rx_Complete(10) == 0) {sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);Draw_String(0, 0, buf, 0x0000ff00, 0);}else{HAL_UART_AbortReceive_IT(&huart4);//关闭中断接收}}
}
1.3.4、DMA方式
可以转去使用函数的定义中,发送DMA方式最后还是会触发串口中断,不过DMA是不消耗CPU的,所以上面中断方式的等待函数我们还是可以继续使用的用来等待的!
static void CH1_UART2_TxTaskFunction( void *pvParameters )
{uint8_t c = 0;while (1){/* 发送信息 */HAL_UART_Transmit_DMA(&huart2, &c, 1);Wait_UART2_Tx_Complete(100);vTaskDelay(500);c++;}
}static void CH2_UART4_RxTaskFunction( void *pvParameters )
{uint8_t c = 0;int cnt = 0;char buf[100];HAL_StatusTypeDef err;while (1){/* 接受信息 */err = HAL_UART_Receive_DMA(&huart4, &c, 1);if (Wait_UART4_Rx_Complete(10) == 0) {sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);Draw_String(0, 0, buf, 0x0000ff00, 0);}else{HAL_UART_DMAStop(&huart4);}}
}
1.3.5、效率最高的 UART 编程方法
最有效率的方式就是利用IDLE中断,IDLE,空闲的定义是:总线上在一个字节的时间内没有再接收到数据。
UART 的 IDLE 中断何时发生?RxD 引脚一开始就是空闲的啊,难道 IDLE 中断一直产生?不是的。当我们使能 IDLE 中断后,它并不会立刻产生,而是:至少收到 1 个数据后,发现在一个字节的时间里,都没有接收到新数据,才会产生 IDLE 中断。
- 接收完指定数量的数据了,比如收到了 100 字节的数据了,HAL_UART_RxCpltCallback 被调用
- 总线空闲了:HAL_UARTEx_RxEventCallback 被调用
- 发生了错误:HAL_UART_ErrorCallback 被调用

使用示例:
此例子建立在以100位数据为完整的接收上:HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);
//接收完成中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart2){/* write queue : g_uart4_rx_buf 100 bytes ==> queue */for (int i = 0; i < 100; i++){xQueueSendFromISR(g_xUART2_RX_Queue, (const void *)&g_uart2_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);} if (huart == &huart4){/* write queue : g_uart4_rx_buf 100 bytes ==> queue */for (int i = 0; i < 100; i++){xQueueSendFromISR(g_xUART4_RX_Queue, (const void *)&g_uart4_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100);}
}//空闲中断
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart == &huart2){/* write queue : g_uart4_rx_buf Size bytes ==> queue */for (int i = 0; i < Size; i++)//根据实际收到多少个数据进行写入队列{xQueueSendFromISR(g_xUART2_RX_Queue, (const void *)&g_uart2_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);}if (huart == &huart4){/* write queue : g_uart4_rx_buf Size bytes ==> queue */for (int i = 0; i < Size; i++){xQueueSendFromISR(g_xUART4_RX_Queue, (const void *)&g_uart4_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100);}
}
1.3.6、使用IDLE+DMA接收要点
- 对于发送:使用“HAL_UART_Transmit_DMA”函数
- 对于接收:一开始就调用“HAL_UARTEx_ReceiveToIdle_DMA”启动接收
- 在回调函数“HAL_UART_RxCpltCallback”或“HAL_UARTEx_RxEventCallback”里读取、 存储数据后,再次调用“HAL_UARTEx_ReceiveToIdle_DMA”启动接收
1.4、UART+RTOS编程
在上面的UART编程中都是使用一个标志位来配合串口发送信息数据的,为1时就是发送或者接收完成。这个方法其实还是不是很高效并且相对浪费CPU,因为我们程序在发送或者接收完成前都会去访问标志位,还有就是假如我们刚访问完标志位,此时还没有变1,上面的代码就是需要进行休眠1s,如果标志位在下0.5s就变1了,那我们岂不是浪费了时间吗,这样显的效率很低下,所以我们引进RTOS就可以很好解决这些缺点了!
发送函数我们使用信号量可以实现“实时”,大大提高了效率!
//串口发送完成中断回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart2){xSemaphoreGiveFromISR(g_UART2_TX_Semaphore, NULL);}
}int UART2_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{HAL_UART_Transmit_DMA(&huart2, datas, len);/* 等待DMA的串口发送完成 */if (pdTRUE == xSemaphoreTake(g_UART2_TX_Semaphore, timeout))return 0;elsereturn -1;
}
接收我们也可以使用队列进行储存信息数据,可以更加简单读取使用数据
/* 开启接收 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);//串口接收完成中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart == &huart2){/* write queue : g_uart4_rx_buf 100 bytes ==> queue */for (int i = 0; i < 100; i++){xQueueSendFromISR(g_xUART2_RX_Queue, (const void *)&g_uart2_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);} }
//串口接收提前完成空闲IDLE中断回调函数 Size-实际读取到的数量
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart == &huart2){/* write queue : g_uart4_rx_buf Size bytes ==> queue */for (int i = 0; i < Size; i++){xQueueSendFromISR(g_xUART2_RX_Queue, (const void *)&g_uart2_rx_buf[i], NULL);}/* re-start DMA+IDLE rx */HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);}
}/* 在队列中读取数据 */
int UART2_GetData(struct UART_Device *pdev, uint8_t *pData, int timeout)
{if (pdPASS == xQueueReceive(g_xUART2_RX_Queue, pData, timeout))return 0;elsereturn -1;
}
1.5、结构体面对对象编程
我们定义一个结构体,代表一个串口设备或者后面可以扩展为USB设备或者其他设备:
struct UART_Device {char *name;int (*Init)( struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit);int (*Send)( struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout);int (*RecvByte)( struct UART_Device *pDev, uint8_t *data, int timeout);int (*Flush)(struct UART_Device *pDev);
};
然后我们定义一个这个结构体类型的数组,方便我们可以根据设备信息(name)去寻找到这个设备的结构体
static struct UART_Device *g_uart_devices[] = {&g_uart2_dev, &g_uart4_dev, &g_usbserial_dev};
最后就是写一个函数可以根据设备的name在这个数组里面寻找是否有这个设备结构体,如果有的话就将其返回出来:
struct UART_Device *GetUARTDevice(char *name)
{int i = 0;for (i = 0; i < sizeof(g_uart_devices)/sizeof(g_uart_devices[0]); i++){if (!strcmp(name, g_uart_devices[i]->name))return g_uart_devices[i];}return NULL;
}
前面1.4我们已经实现了发送函数和从队列中获取数据函数,现在我们还需要实现initi函数(创建信号量和队列)还有队列信息数据清空函数(一直读取队列,知道为空为止):
int UART2_Flush(struct UART_Device *pDev)
{int cnt = 0;uint8_t data;while (1){if (pdPASS != xQueueReceive(g_xUART2_RX_Queue, &data, 0))break;cnt++;}return cnt;
}int UART2_Rx_Start(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{if (!g_xUART2_RX_Queue){g_xUART2_RX_Queue = xQueueCreate(200, 1);g_UART2_TX_Semaphore = xSemaphoreCreateBinary( );HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buf, 100);}return 0;
}
为了简单,串口2结构体的name就叫uart2,串口4结构体就叫uart4,因为这个项目后面还用到USB,所以后面也会有一个是关于USB设备的结构体在这个数组里面,并且叫做usb.