UART开发基础

目录

前言

同步传输与异步传输

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_ITHAL_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结合,并且学习面向对象的编程思想,相信学到精髓后对你的工作也有很大帮助,面对一些大型的项目也有代码思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sakabu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值