UART开发基础

news2024/10/1 20:00:54

目录

前言

同步传输与异步传输

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2166163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

L2 Loss介绍及梯度计算说明

L1 Loss介绍及梯度计算说明-CSDN博客 L2 Loss&#xff08;MS&#xff0c;也称为均方误差损失或平方误差损失&#xff09;是一种常用的损失函数&#xff0c;广泛应用于回归任务中。它通过计算预测值与真实值之间的平方差来评估模型的性能。 1. L2 Loss 的定义 L2 Loss 的数…

Python | Leetcode Python题解之第437题路径总和III

题目&#xff1a; 题解&#xff1a; class Solution:def pathSum(self, root: TreeNode, targetSum: int) -> int:prefix collections.defaultdict(int)prefix[0] 1def dfs(root, curr):if not root:return 0ret 0curr root.valret prefix[curr - targetSum]prefix[cu…

Golang | Leetcode Golang题解之第436题寻找右区间

题目&#xff1a; 题解&#xff1a; func findRightInterval(intervals [][]int) []int {n : len(intervals)type pair struct{ x, i int }starts : make([]pair, n)ends : make([]pair, n)for i, p : range intervals {starts[i] pair{p[0], i}ends[i] pair{p[1], i}}sort.…

第四届工业母机高质量发展论坛在浙江温岭召开

9月24日&#xff0c;由工业和信息化部产业发展促进中心&#xff08;以下简称产促中心&#xff09;主办的“第四届工业母机高质量发展论坛”在浙江温岭成功召开。 中国工程院院士周济、郭东明、王国庆&#xff0c;工业和信息化部装备工业一司一级巡视员苗长兴&#xff0c;中国企…

C语言 | Leetcode C语言题解之第437题路径总和III

题目&#xff1a; 题解&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ //递归遍历树节点&#xff0c;判断是否为有效路径 int dfs(struct TreeNode * root, int ta…

C++ -函数重载-详解

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【C】 欢迎点赞&#x1f44d;收藏⭐关注❤️ C -函数重载-详解 1.是什么2.怎么用2.1示例 3.原理3.1C/C编译链接过程3.2函数名修饰规则3.3过程1.调用函数的过程2.编译阶段的函数调用 总结 1.是什么 如果在百度中搜索重载这…

Adobe Bridge简体中文版百度云下载与安装(附教程)

如大家所熟悉的&#xff0c;Adobe Bridge常常简称为BR&#xff0c;是一款数字资产管理软件&#xff0c;可以帮助用户浏览、组织、搜索和管理各种类型的媒体文件&#xff0c;如照片、音频、视频等。 Bridge发展至今有许多个版本&#xff0c;目前来说常用的版本有Bridge 2018、2…

2024东湖高新下半年水测公示名单啦

2024东湖高新下半年水测公示名单啦 公示时间9月13日-9月20日&#xff0c;快看看你过了没&#xff01;&#xff01; 东湖高新区报名水测共有2600多人&#xff0c;水测公示通过1201人&#xff0c;部分人员免考。 水测通过后就赶紧整理好申报材料&#xff0c;准备申报了&#xff…

2.1 HuggingFists系统架构(一)

系统架构 HuggingFists的前端主体开发语言为HtmlJavascript&#xff0c;后端的主体开发语言为Java。在算子部分有一定份额的Python代码&#xff0c;用于整合Python在数据处理方面强大能力。 功能架构 HuggingFists的功能架构如上&#xff0c;由下向上各层为&#xff1a; 数据存…

【程序大侠传】应用内存缓步攀升,告警如影随形

前序 在武侠编码的江湖中&#xff0c;内存泄漏犹如隐秘杀手&#xff0c;潜伏于应用程序的各个角落&#xff0c;悄无声息地吞噬着系统资源。若不及时发现和解决&#xff0c;必将导致内存枯竭&#xff0c;应用崩溃。 背景&#xff1a;内存泄漏的由来 内存泄漏&#xff0c;乃程序…

TensorRT-LLM保姆级教程(三)-使用Triton推理服务框架部署模型

随着大模型的爆火&#xff0c;投入到生产环境的模型参数量规模也变得越来越大&#xff08;从数十亿参数到千亿参数规模&#xff09;&#xff0c;从而导致大模型的推理成本急剧增加。因此&#xff0c;市面上也出现了很多的推理框架&#xff0c;用于降低模型推理延迟以及提升模型…

redis哨兵启动出现 +sdown master mymaster 192.168.x.x

场景&#xff1a; 搭建好哨兵之后&#xff0c;哨兵一启动&#xff0c;过了30秒就会判断master sdown&#xff0c;但是检查配置是没有问题。 日志&#xff1a; Redis-master启动日志&#xff1a;没看到任何异常&#xff0c;所以master无异常 Redis-哨兵启动日志&#xff1a; …

深度学习技术概览

一、深度学习技术概览 深度学习&#xff0c;作为机器学习的一个分支&#xff0c;其核心在于通过构建多层神经网络模型来模拟人脑的学习过程。与传统的机器学习算法相比&#xff0c;深度学习能够自动从原始数据中提取高级抽象特征&#xff0c;而无需人工进行复杂的特征工程。这…

Java项目实战II基于Java+Spring Boot+MySQL的网上摄影工作室(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者 一、前言 在数字化时代&#xff0c;摄影艺术已不再局限于传统媒介&#xff0c;而是借助互联网平台绽放新的光彩…

基于遗传优化算法的多AGV栅格地图路径规划matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 栅格地图表示 4.2 路径编码 4.3 目标函数 5.完整程序 1.程序功能描述 基于遗传优化算法的多AGV栅格地图路径规划matlab仿真&#xff0c;分别测试单个AGC的路径规划和多个AGV的路径规划…

虹科技术分享 | CAN XL总线测试与译码

CAN XL是第三代控制器局域网协议&#xff0c;建立在经典CAN和CAN FD网络的基础上&#xff0c;并支持向后兼容。它面向车载网络&#xff0c;使用单个差模总线连接多个控制器和传感器。由于高度的耐用性和对布线需求最小的总线拓扑结构&#xff0c;控制器局域网协议越来越多地进入…

虚拟社交的新时代:探索Facebook的元宇宙愿景

随着技术的不断进步&#xff0c;社交媒体的形态也在悄然变化。Facebook&#xff08;现名Meta&#xff09;正站在这一变革的前沿&#xff0c;积极探索元宇宙的愿景。元宇宙不仅是虚拟现实&#xff08;VR&#xff09;和增强现实&#xff08;AR&#xff09;的结合&#xff0c;更是…

Spring Boot房屋租赁系统:技术架构解析

2 关键技术简介 2.1 JAVA技术 Java是一种多用途并且强大的编程语言&#xff0c;可用于开发运行在移动设备、台式计算机以及服务器端的软件。Java已及其流行。Java只要编写一次&#xff0c;无论什么地方都可以运行启动[1]。 Java语言是应用很广泛的语言&#xff0c;用它编写出的…

【JVM原理】运行时数据区(内存结构)

JVM &#xff08;Java Virtual Machine&#xff09;原理 文章目录 四、运行时数据区&#xff08;内存结构&#xff09;4-1 线程私有区域程序计数器&#xff08;program counter Register&#xff09;本地方法栈&#xff08;Native Method Stacks&#xff09;Java 虚拟机栈&…

Python办公自动化教程(004):PDF添加水印

1.4 PDF文档水印添加 【1】安装库 pip install reportlab pip install PyPDF2【2】代码 import iofrom PyPDF2 import PdfWriter, PdfReader from reportlab.lib import pagesizes # 页面样式 from reportlab.lib.units import cm from reportlab.pdfbase import pdfmetric…