目录
前言
同步传输与异步传输
1.概念与示例
2.差别
UART协议与操作方法
1.UART协议
2.STM32H5 UART硬件结构
3.RS485协议
UART编程
1.三种编程方式
2.查询方式
3.中断方式
4.DMA 方式
效率最高的UART编程方法
1.IDLE中断
2.DMA 发送/DMA+IDLE 接收
在RTOS里使用UART
1.程序框架
2.编写程序
面向对象封装UART
前言
本文介绍串口常用的三种编程方式、效率最高的编程方法,并将RTOS和串口的操作结合起来,同时锻炼面向对象的编程方式;在学习的时候,我见到了之前从未见过的函数封装方式,我相信这对我以后的工作有很大的帮助。
同步传输与异步传输
1.概念与示例
使用生活例子来说明什么是同步、异步:
① 同步:朋友打电话说到我家吃饭,我在家里等他们
② 异步:朋友没有提前打招呼,突然就到我家来了
它们的差别在于:有没有使用一种方法“实现约好时间”。
在电子产品中,使用同步传输时,一般涉及两个信号:
① 时钟信号:用来通知对方要读取数据了
② 数据信号:用来传输数据
(I2C和SPI协议就是同步传输,他们都有一条时钟线,“约定好时间”)
同步传输示例如下:
异步传输示例如下:
使用异步信号传输数据时,双方遵守相同的约定:
① 起始信号:发送方可以通知接收方"注意了,我要开始传输数据了"
② 数据的表示:怎么表示逻辑 1,怎么表示逻辑 0。
以红外遥控器解码器为例,它向单片机发出的数据格式如下:
① 起始信号:解码器发出一个 9ms 的低电平、4.5ms 的高电平,用来同时对方说"开始了"
② 表示一位数据
逻辑 1:0.56ms 的低电平+1.69ms 的高电平
逻辑 0:0.56ms 的低电平+0.56ms 的高电平
③ 接收方、发送方都遵守这样的约定,就可以使用一条线传输数据
2.差别
UART协议与操作方法
注意:(以下内容是比较基础的知识,学过32的同学都比较了解串口的内部原理和相关寄存器的操作,但会简单介绍RS485协议,有需要自行跳转)
1.UART协议
通用异步收发器简称 UART,即“Universal Asynchronous Receiver Transmitter”,它用来传输串行数据:发送数据时,CPU 将并行数据写入 UART,UART 按照一定的格式在一根电线上串行发出;接收数据时,UART 检测另一根电线上的信号,将串行数据收集放在缓冲区中,CPU 即可读取 UART 获得这些数据。UART 之间以全双工方式传输数据,最精简的连线方法只有三根电线:TxD 用于发送数据,RxD 用于接收数据,GND 用于给双方提供参考电平,连线如图所示:
UART 使用标准的 TTL/CMOS 逻辑电平(0~5V、0~3.3V、0~2.5V 或 0~1.8V 四种)来表示数据,高电平表示 1,低电平表示 0。进行长距离传输时,为了增强数据的抗干扰能力、提高传输长度,通常将 TTL/CMOS 逻辑电平转换为 RS-232 逻辑电平,3~12V 表示 0,-3~-12V 表示 1。
TxD、RxD 数据线以“位”为最小单位传输数据。帧(frame)由具有完整意义的、不可分割的若干位组成,它包含开始位、数据位、较验位(需要的话)和停止位。发送数据之前,UART 之间要约定好数据的传输速率(即每位所占据的时间,其倒数称为波特率)、数据的传输格式(即有多少个数据位、是否使用较验位、是奇较验还是偶较验、有多少个停止位)。
数据传输流程如下:
1) 平时数据线处于“空闭”状态(1 状态)。
2) 当要发送数据时,UART 改变 TxD 数据线的状态(变为 0 状态)并维持 1 位的时间──这样接收方 检测到开始位后,再等待 1.5 位的时间就开始一位一位地检测数据线的状态得到所传输的数据。
3) UART 一帧中可以有 5、6、7 或 8 位的数据,发送方一位一位地改变数据线的状态将它们发送 出去,首先发送最低位。
4) 如果使用较验功能,UART 在发送完数据位后,还要发送 1 个较验位。有两种较验方法:奇较 验、偶较验──数据位连同较验位中,“1”的数目等于奇数或偶数。
5) 最后,发送停止位,数据线恢复到“空闭”状态(1 状态)。停止位的长度有 3 种:1 位、1.5 位、2 位。
下图演示了 UART 使用 7 个数据位、偶较验、2 个停止位的格式传输字符“A”(二进制值为 0b01000001)时,TTL/CMOS 逻辑电平、RS-232 逻辑电平对应的波形。
双方约定了“传输一 bit 数据的时间”,就可以算出 1 秒内能传输多少 bit 数据,这被称为“比特率”,又经常被称为“波特率”。两者有什么关系?
假设发送方 A 能精确控制信号的电压,接收方 B 也能精确识别电压,双方如此约定:
那么要传输一个字节的数据,比如 0x78,它的二进制数为 0b01,11,10,00,只需要传输 4 次(假设 1ms 改变一次电压,假设先传输低位):
① 第 1ms,A 设置电压为 0V,B 识别出电压后,认为收到了 bit1 为 0、bit0 为 0
② 第 2ms,A 设置电压为 1.6V,B 识别出电压后,认为收到了 bit3 为 1、bit2 为 0
③ 第 3ms,A 设置电压为 2.4V,B 识别出电压后,认为收到了 bit5 为 1、bit4 为 1
④ 第 4ms,A 设置电压为 0.8V,B 识别出电压后,认为收到了 bit7 为 0、bit6 为 1
只需要 4ms,就传输了 4 个状态,但是传输了 8bit 数据:波特率*2=比特率。
假设发送方 A 精确控制信号电压的能力比较差,只能保证 0~0.7V、1.8~3.3V 的电压比较稳定;接收方 B 识别电压的能力也不够精确,只能保证可以识别出 0~0.7V、1.8~3.3V 的电压,于是双方约定:
那么要传输一个字节的数据,比如 0x78,它的二进制数为 0b01111000,需要传输 8 次。
8ms,传输 8 个状态,传输了 8bit 数据:波特率=比特率。
得出结论:波特率:1 秒内传输信号的状态数(波形数)。比特率:1 秒内传输数据的 bit 数。如果一个波形,能表示 N 个 bit,那么:波特率 * N = 比特率。
2.STM32H5 UART硬件结构
CPU往TDR寄存器里写数据,也可以从RDR寄存器读出数据;数据再发到对应的移位寄存器,一位一位的发送出去。
CPU什么时候读/写数据?由状态寄存器ISR决定,当发送寄存器TDR空,就可以写数据,接收寄存器RDR非空,就可以读数据。
FIFO是缓冲区;以写数据为例子:如果没有FIFO,CPU写数据的时候只能一个字节一个字节写,有了FIFO,假设它的深度为64字节,那么可以一股脑的写入64字节的数据,FIFO遵循先进先出的原则,注意的是:数据还是通过移位寄存器一位一位发出去的,只不过可以节省CPU的资源,不用频繁的判断发送寄存器是否空。
3.RS485协议
使用 RS485 协议传输数据时,电路图如下:
RS485 协议里,使用 A、B 差分信号线传输数据:两线间的电压差为+(2 至 6)V 表示逻辑 1,电压差为-(2 至 6)V 时表示逻辑 0。它是半双工的传输方式(TTL协议的串口是全双工):MCU1 要发送数据时,从 TxD 引脚把数据发送给电平转换芯片 MAX13487EESA,它把 TxD 的信号转换为差分信号传递给另一个电平转换芯片 MAX13487EESA,进而转换为 TTL 电平通过 RO 发送到 MCU2 的RxD 引脚。MCU2 要给 MCU1 发送数据的话,必须等待差分信号线处于空闲状态。
对于软件而言,使用 RS485 跟普通的 UART 没有区别。
UART编程
使用的板子为STM32H5系列,将串口2和串口4相连,进行串口编程实验。
1.三种编程方式
结合 UART 硬件结构,有 3 种编程方法:
① 查询方式:
要发送数据时,先把数据写入 TDR 寄存器,然后判断 TDR 为空再返回。当然也可以先判断 TDR 为空,再写入。
要读取数据时,先判断 RDR 非空,再读取 RDR 得到数据。
② 中断方式:
使用中断方式,效率更高,并且可以在接收数据时避免数据丢失。
要发送数据时,使能“TXE”中断(发送寄存器空中断)。在 TXE 中断处理函数里,从程序的发送 buffer 里取出一个数据,写入 TDR。等再次发生 TXE 中断时,再从程序的发送buffer 里取出下一个数据写入 TDR。
对于接收数据,在一开始就使能“RXNE”中断(接收寄存器非空)。这样,UART 接收到一个数据就会触发中断,在中断程序里读取 RDR 得到数据,存入程序的接收 buffer。当程序向读取串口数据时,它直接读取接收 buffer 即可。
这里涉及的“发送 buffer”、“接收 buffer”,特别适合使用“环形 buffer”(学过FreeRTOS的自然会联想到队列)。
③ DMA 方式:
使用中断方式时,在传输、接收数据时,会发生中断,还需要 CPU 执行中断处理函数。有另外一种方法:DMA(Direct Memory Access),它可以直接在 2 个设备之间传递数据,无需 CPU 参与。框图如下:
设置好 DMA(源、目的、地址增减方向、每次读取数据的长度、读取次数)后,DMA 就会自动地在 SRAM 和 UART 之间传递数据:
① 发送时:DMA 从 SRAM 得到数据,写入 UART 的 TDR 寄存器
② 接收时:DMA 从 UART 的 RDR 寄存器得到数据,写到 SRAM 去
③ 指定的数据传输完毕后,触发 DMA 中断;在数据传输过程中,没有中断,CPU 无需处理
函数如下:
2.查询方式
缺点:发送数据时要死等发送完毕,接收数据时容易丢失。
cubemx配置
串口2和4都使能FIFO模式,异步通信,其余默认。
实验代码是基于FreeRTOS的,创建两个任务,一个发送任务,一个接收任务。
app_freertos.c
xTaskCreate(UART2_TxTaskFunction,"uart2_tx_task",200,NULL,osPriorityNormal,NULL);
xTaskCreate(UART4_RxTaskFunction,"uart4_rx_task",200,NULL,osPriorityNormal,NULL);
/*发送任务*/
static void UART2_TxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
while (1)
{
/* 发送数据 */
HAL_UART_Transmit(&huart2, &c, 1, 100);
vTaskDelay(500);
c++;
}
}
/*接收任务*/
static void UART4_RxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
int cnt = 0;
char buf[100];
HAL_StatusTypeDef err;
while (1)
{
/* 接收数据 */
/* 接收成功返回 0 */
err = HAL_UART_Receive(&huart4, &c, 1, 100);
/* 在OLED上显示出来 */
if (!err)
{
sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);
Draw_String(0, 0, buf, 0x0000ff00, 0);
}
}
}
可以看到使用查询方式的时候结合RTOS还是比较简单的,因为两个任务都可以及时得到运行。缺点也很明显了。
3.中断方式
缺点:需要是事先调用接收函数,才能通过中断接收数据,易丢失。
cubemx配置
在上一个配置的基础上,串口2和串口4使能NVIC中断
当我们调用 HAL_UART_Transmit_IT 和 HAL_UART_Receive_IT 函数的时候,它只是去开启这个发送中断和接收中断,至于什么时候发送完成?什么时候接收到了数据?需要我们自己去判断,它提供了对应的回调函数。
在usart.c里,我们可以自己定义这两个函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
注意: 回调函数是在中断里被调用的,是在中断退出前被调用的,后续改造回调函数的时候,用到的队列和信号量操作,如果是在中断里,需要加上FromISR后缀,否则会导致代码崩溃!!!
usart.c
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)
{
/*要么标志位被置1,表发送完成;要么超时退出*/
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;
}
}
代码写的有点笨,主要理解回调函数的作用,可以自行在回调函数里进行自己想要的操作
app_freertos.c
static void 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 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);
/*wait函数返回0表示成功*/
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);
}
}
}
代码很简单,效率相比查询方式来说要高。 HAL_UART_AbortReceive_IT 函数的作用是中止一个正在进行的 UART 接收操作,中止接收操作后,相关的资源(如缓冲区、标志位等)会被重置,准备好进行后续的操作。对于发送失败,我们并没有进行操作。
4.DMA 方式
本节讲的是传统 DMA 方式,不涉及“idle 中断”,它会在后面讲解。
缺点:需要是事先调用接收函数,才能通过中断接收数据,易丢失。
cubemx配置
数据传输的三要素:源,目的,长度。这里的触发方式是串口2的发送引脚,自然是从内存中拿取数据,发送到外面,那么源地址就是内存,拿一个数据就要递增一次(每次递增一个字节) ,目的地址是外设,是移位寄存器,所以目的地址不变。
串口4同理:
大家可以自己推理一下。
usart.c 的代码不变,DMA传输完成后会发起DMA中断,回调函数还是一样的。
看看app_freertos.c的代码:
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);
}
}
}
只是后缀变了,中止函数的名字可能有些区别。但是效率要比前两个都高。
效率最高的UART编程方法
1.IDLE中断
IDLE,空闲的定义是:总线上在一个字节的时间内没有再接收到数据。
UART 的 IDLE 中断何时发生?RxD 引脚一开始就是空闲的啊,难道 IDLE 中断一直产生?
不是的。当我们使能 IDLE 中断后,它并不会立刻产生,而是:至少收到 1 个数据后,发现在一个字节的时间里,都没有接收到新数据,才会产生 IDLE 中断。
我们使用 DMA 接收数据时,确实可以提高 CPU 的效率,但是“无法预知要接收多少数据”,而我们想尽快处理接收到的数据。怎么办?比如我想读取 100 字节的数据,但是接收到 60 字节后对方就不再发送数据了,怎么办?我们怎么判断数据传输中止了?可以使用 IDLE 中断。在这种情况下,DMA 传输结束的条件有 3 个:
① 接收完指定数量的数据了,比如收到了 100 字节的数据了,HAL_UART_RxCpltCallback 被调 用
② 总线空闲了:HAL_UARTEx_RxEventCallback 被调用
③ 发生了错误:HAL_UART_ErrorCallback 被调用
使用 IDLE 状态来接收的函数有:
2.DMA 发送/DMA+IDLE 接收
要点有 3 个:
① 对于发送:使用“HAL_UART_Transmit_DMA”函数
② 对于接收:一开始就调用“HAL_UARTEx_ReceiveToIdle_DMA”启动接收
③ 在回调函数“HAL_UART_RxCpltCallback”或“HAL_UARTEx_RxEventCallback”里读取、存储数 据后,再次调用“HAL_UARTEx_ReceiveToIdle_DMA”启动接收
在RTOS里使用UART
1.程序框架
本程序的重点在于如何高效地接收数据:
① 使用 DMA+IDLE 中断的方式接收数据,它会把数据存入临时缓冲区;
② 在回调函数里:把临时缓冲器的数据写入队列,然后再次使能 DMA
③ APP 读取队列:如果队列里没有数据则阻塞。
框架如下:
2.编写程序
在usart.c中,我们可以自己定义那三个回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
我们将usart.c改造一下,之前等待完成的函数写的太丑了,我们用信号量来代替。
注意:我们以串口2为例子,实现串口2的发送和接收函数,串口4同理,最后可以实现串口2给串口4发,串口4给串口2发。
static uint8_t g_uart2_rx_buff[100];
static SemaphoreHandle_t g_UART2_Tx_Semphore;
static QueueHandle_t g_xUART2_Rx_Queue;
/*发送完成回调函数*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
/*释放一个信号量,代替之前的wait函数*/
xSemaphoreGiveFromISR(g_UART2_Tx_Semphore, NULL);
}
/*在回调函数里判断是哪个串口,这里演示一遍后面不再演示*/
if(huart == &huart4)
{
}
}
/*接收完成回调函数*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
/*如果接收到了100字节的数据,写队列*/
for(int i=0; i<100; i++)
{
xQueueSendFromISR(g_xUART2_Rx_Queue, &g_uart2_rx_buff[i], NULL);
}
/*重启DMA和IDLE中断*/
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buff, 100);
}
}
/*IDLE回调函数*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart2)
{
/*如果发生IDLE中断,写队列*/
for(int i=0; i<Size; i++)
{
xQueueSendFromISR(g_xUART2_Rx_Queue, &g_uart2_rx_buff[i], NULL);
}
/*重启DMA和IDLE中断*/
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buff, 100);
}
}
/*error回调函数*/
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
/*重启DMA和IDLE中断*/
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buff, 100);
}
}
/*UART2相关操作*/
int UART2_GetData(struct UART_Device *pDev, uint8_t *data, int timeout)
{
/*读到数据返回pdPASS*/
if(xQueueReceive(g_xUART2_Rx_Queue, data, timeout) == pdPASS)
return 0;
else
return -1;
}
/*初始化串口2相关的信号量和队列,启动串口*/
int UART2_Rx_Start(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
/*避免多次创建*/
if(g_xUART2_Rx_Queue == NULL)
{
g_xUART2_Rx_Queue = xQueueCreate(200, 1);
g_UART2_Tx_Semphore = xSemaphoreCreateBinary();
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, g_uart2_rx_buff, 100);
}
return 0;
}
int UART2_Send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
HAL_UART_Transmit_DMA(&huart2, datas, len);
/*等待1个信号量,回调函数在中断里被调用,在中断里Give Mutex会出错,所以使用信号量*/
if(pdTRUE == xSemaphoreTake(g_UART2_Tx_Semphore, timeout))
return 0;
else
return -1;
}
注意我之前说的,回调函数是在中断里被调用的,而在中断里,释放互斥量会导致程序崩溃,见下面官方的解释:
所以用信号量来代替wait函数,回调函数里队列的操作也要加上FromISR后缀,至此实现了将UART和RTOS结合起来。
再来看看app_freertos.c,
static void UART2_TxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
while (1)
{
/* send data */
HAL_UART_Transmit_DMA(&huart2, &c, 1);
Wait_UART2_Tx_Complete(100);
vTaskDelay(500);
c++;
}
}
static void UART4_RxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
int cnt = 0;
char buf[100];
HAL_StatusTypeDef err;
/*初始化和启动函数*/
UART4_Rx_Start();
while (1)
{
/* 接收数据 */
err = UART4_GetData(&c);
if (err == 0)
{
sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);
Draw_String(0, 0, buf, 0x0000ff00, 0);
}
else
{
HAL_UART_DMAStop(&huart4);
}
}
}
面向对象封装UART
我们使用多个 UART:UART2、UART4,以初始化为例,有如下函数:
void UART2_Rx_Start(void);
void UART4_Rx_Start(void);
对于使用者而言,非常不友好:当 UART 数量增多,他需要记住、使用多个函数名;当更换某个 UART,他需要修改多处代码。比如对于如下代码,当需要更换为 UART4 时,需要修改第 1、3 行代码为 UART4 的函数:
/*01*/ uart2_init(115200, 'N', 8, 1);
/*02*/ char *str = “hello”;
/*03*/ uart2_sendp(str, strlen(str), 100);
重点:把 UART 的操作封装为结构体,可以解决这个问题。UART 的操作主要有 3 个函数:初始化、发送数据、接收数据。那么可以抽象出如下结构体:
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);
};
本节为 UART2、UART4 分别构造一个“struct UART_Device”结构体,比如:
struct UART_Device g_uart2_dev = {"uart2", uart2_init, uart2_send, uart2_recvbyte};
struct UART_Device g_uart4_dev = {"uart4", uart4_init, uart4_send, uart4_recvbyte};
使用时,示例代码如下:
/*01*/ struct UART_Device *pDev = &g_uart2_dev;
/*02*/ pDev->Init(pDev, 115200, 'N', 8, 1);
/*03*/ char *str = “www.100ask.net”;
/*04*/ pDev->Send(pDev, str, strlen(str), 100);
如果要更换串口,只需要修改第 1 行代码,让它指向 g_uart4_dev 即可:这就是面向对象编程的优点。
示例:
uart_device.c
#include <stdio.h>
#include <string.h>
#include "uart_device.h"
/*在 usart.c 里定义结构体,不把串口的函数暴露出来*/
extern struct UART_Device g_uart2_dev;
extern struct UART_Device g_uart4_dev;
/*定义一个指针数组,里面存放串口的结构体指针*/
static struct UART_Device *g_uart_devices[] = {&g_uart2_dev, &g_uart4_dev};
/*在app_freertos.c里调用,获得串口的结构体指针*/
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))//相等返回值为0
return g_uart_devices[i];//返回名字符合的元素的指针
}
/*没找到*/
return NULL;
}
uart_device.h
在usart.c最后给结构体赋值,不把函数暴露出去
app_freertos.c
static void UART_TxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
struct UART_Device *pdev = GetUARTDevice("uart2");//获得串口指针
pdev->Init(pdev, 115200, 'N', 8, 1);//调用结构体中的初始化函数
while (1)
{
/* send data */
pdev->Send(pdev, &c, 1, 100);//调用结构体中的发送函数
vTaskDelay(500);
c++;
}
}
static void UART_RxTaskFunction( void *pvParameters )
{
uint8_t c = 0;
int cnt = 0;
char buf[100];
int err;
struct UART_Device *pdev = GetUARTDevice("uart4");//获得串口指针
pdev->Init(pdev, 115200, 'N', 8, 1);//调用结构体中的初始化函数
while (1)
{
err = pdev->RecvByte(pdev, &c, 200);//得到数据存入c中
if (err == 0)
{
sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);
Draw_String(0, 0, buf, 0x0000ff00, 0);
}
else
{
//HAL_UART_DMAStop(&huart4);
}
}
}
在这之前要先配置一下cubemx
现在是串口2发送数据给串口4,想要反过来呢?
很简单,获取串口指针的时候改个名字就行了。
以上就是UART编程的全部内容,重点还是和RTOS结合,并且学习面向对象的编程思想,相信学到精髓后对你的工作也有很大帮助,面对一些大型的项目也有代码思路。