当前位置: 首页 > news >正文

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、概念与示例

使用生活例子来说明什么是同步、异步:
  • 同步:朋友打电话说到我家吃饭,我在家里等他们
  • 异步:朋友没有提前打招呼,突然就到我家来了

它们的差别在于:有没有使用一种方法“实现约好时间”。

在电子产品中,使用同步传输时,一般涉及两个信号:                                                                   

  • 时钟信号:用来通知对方要读取数据了
  • 数据信号:用来传输数据
同步传输示例如下:
① 时钟信号:打电话,起约定作用
② 数据信号:传输数据

使用异步信号传输数据时,双方遵守相同的约定:
① 起始信号:发送方可以通知接收方"注意了,我要开始传输数据了"
② 数据的表示:怎么表示逻辑 1,怎么表示逻辑 0。
以红外遥控器解码器为例,它向单片机发出的数据格式如下:

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

② 表示一位数据
逻辑 1:0.56ms 的低电平+1.69ms 的高电平
逻辑 0:0.56ms 的低电平+0.56ms 的高电平

③ 接收方、发送方都遵守这样的约定,就可以使用一条线传输数据 

两者差别: 

1.2、UART 协议与操作方法

1.2.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 逻辑电平对应的波形。

 1.2.2、STM32H5 UART 硬件结构

FIFO:大大减少数据查询方式的情况 

1.2.3、RS485 协议

使用 RS485 协议传输数据时,电路图如下:
        RS485 协议里,使用 A、B 差分信号线传输数据:两线间的电压差为+(2 至 6)V 表示
逻辑 1,电压差为-(2 至 6)V 时表示逻辑 0。它是半双工的传输方式:MCU1 要发送数据
时,从 TxD 引脚把数据发送给电平转换芯片 MAX13487EESA,它把 TxD 的信号转换为差分信
号传递给另一个电平转换芯片 MAX13487EESA,进而转换为 TTL 电平通过 RO 发送到 MCU2 的
RxD 引脚。MCU2 要给 MCU1 发送数据的话,必须等待差分信号线处于空闲状态。
        对于软件而言,使用 RS485 跟普通的 UART 没有区别。

1.3、UART 编程

 1.3.1、三种编程方式

        结合 UART 硬件结构,有 3 种编程方法:
  • 查询方式
  • 中断方式
  • DMA 方式
① 查询方式:
要发送数据时,先把数据写入 TDR 寄存器,然后判断 TDR 为空再返回。当然也可以先 判断 TDR 为空,再写入。 要读取数据时,先判断 RDR 非空,再读取 RDR 得到数据。
② 中断方式:
使用中断方式,效率更高,并且可以在接收数据时避免数据丢失。
要发送数据时,使能“TXE”中断(发送寄存器空中断)。在 TXE 中断处理函数里,从 程序的发送 buffer 里取出一个数据,写入 TDR。等再次发生 TXE 中断时,再从程序的发送 buffer 里取出下一个数据写入 TDR。 对于接收数据,在一开始就使能“RXNE”中断(接收寄存器非空)。这样,UART 接收 到一个数据就会触发中断,在中断程序里读取 RDR 得到数据,存入程序的接收 buffer。当 程序向读取串口数据时,它直接读取接收 buffer 即可。 这里涉及的“发送 buffer”、“接收 buffer”,特别适合使用“环形 buffer”。
③ DMA 方式:
使用中断方式时,在传输、接收数据时,会发生中断,还需要 CPU 执行中断处理函数。 有另外一种方法:DMA(Direct Memory Access),它可以直接在 2 个设备之间传递数据, 无需 CPU 参与。
设置好 DMA(源、目的、地址增减方向、每次读取数据的长度、读取次数)后,DMA 就
会自动地在 SRAM 和 UART 之间传递数据:
① 发送时:DMA 从 SRAM 得到数据,写入 UART 的 TDR 寄存器
② 接收时:DMA 从 UART 的 RDR 寄存器得到数据,写到 SRAM 去
③ 指定的数据传输完毕后,触发 DMA 中断;在数据传输过程中,没有中断,CPU 无需处理。

 使用函数:

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 中断。

        我们使用 DMA 接收数据时,确实可以提高 CPU 的效率,但是“无法预知要接收多少数据”,而我们想尽快处理接收到的数据。怎么办?比如我想读取 100 字节的数据,但是接 收到 60 字节后对方就不再发送数据了,怎么办?我们怎么判断数据传输中止了?可以使用 IDLE 中断。
        那么停止DMA传输的条件有:
  1. 接收完指定数量的数据了,比如收到了 100 字节的数据了,HAL_UART_RxCpltCallback 被调用
  2. 总线空闲了:HAL_UARTEx_RxEventCallback 被调用
  3. 发生了错误:HAL_UART_ErrorCallback 被调用
使用 IDLE 状态来接收的函数有:

使用示例:

此例子建立在以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接收要点

  1. 对于发送:使用“HAL_UART_Transmit_DMA”函数
  2. 对于接收:一开始就调用“HAL_UARTEx_ReceiveToIdle_DMA”启动接收
  3. 在回调函数“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.

相关文章:

  • PCB硬件电路设计_pcb布线设计
  • SpringAI集成本地部署DeepSeek大模型服务(Ollama)
  • Android开发,实现一个简约又好看的登录页
  • 深入理解java线程池
  • [RoarCTF 2019]Easy Calc 详解
  • 空洞/膨胀卷积
  • clangd-vscode配置
  • 网络安全之红队LLM的大模型自动化越狱
  • LinuxAgent开源程序是一款智能运维助手,通过接入 DeepSeek API 实现对 Linux 终端的自然语言控制,帮助用户更高效地进行系统运维工作
  • 遗传算法实现单货架库位优化
  • 在Linux系统中安装Anaconda的完整指南
  • vue3代码规范管理;基于vite和vue3、 eslint、prettier、stylelint、husky规范;git触发eslint校验
  • JavaWeb:vueaxios
  • 光触发RFID:破解物流、电力、资产管理三大领域的“不可能三角”
  • 基于 Python 的实现:居民用电量数据分析与可视化
  • 基于SpringBoot的食物营养分析与推荐网站系统
  • 22.晶振的信号与布局布线处理
  • 安卓基础(泛型)
  • 跨语言哈希一致性:C# 与 Java 的 MD5 之战?
  • 搭建speak yarn集群:从零开始的详细指南
  • 太好玩了!坐进大卫·霍克尼的敞篷车进入他画笔下的四季
  • “上海-日喀则”直飞航线正式通航,将于5月1日开启首航
  • 日本大米价格连续16周上涨,再创最高纪录
  • 见证上海援藏30年成果,萨迦非遗珍品展来沪
  • 李公明|一周画记:哈佛打响第一枪
  • 别让心脏“饿肚子”,心肌缺血全解析