FOC学习笔记-串口通信

实现串口IDLE+DMA双缓冲区+环形缓冲区

1.CbueMX配置

模式配置

DMA配置

参数配置

2.环形缓冲区

这部分代码参考于netube99/RingBuffer: 基于C语言开发的环形缓冲区

RingBuffer.h

#ifndef __RING_BUFFER_H_ #define __RING_BUFFER_H_ #include "stdint.h" /** * @brief 返回值定义 */ #define RING_BUFFER_SUCCESS 0x01 #define RING_BUFFER_ERROR 0x00 /** * @brief 环形缓冲区结构体 */ typedef struct { uint32_t Head; // 操作头指针 uint32_t Tail; // 操作尾指针 uint32_t Length; // 已储存的数据量 uint8_t* ArrayAddr; // 缓冲区储存数组基地址 uint32_t MaxLength; // 缓冲区最大可储存数据量 } RingBuffer_TypeDef; /** * @brief 初始化新缓冲区 * @param rb_handle 待初始化的缓冲区结构体句柄 * @param buffer_addr 外部定义的缓冲区数组,类型必须为 uint8_t * @param buffer_size 外部定义的缓冲区数组空间 * @return 返回缓冲区初始化的结果 */ uint8_t RB_Init(RingBuffer_TypeDef* rb_handle, uint8_t* buffer_addr, uint32_t buffer_size); /** * @brief 从头指针开始删除指定长度的数据 * @param rb_handle 缓冲区结构体句柄 * @param Length 要删除的长度 * @return 返回删除指定长度数据结果 */ uint8_t RB_Delete(RingBuffer_TypeDef* rb_handle, uint32_t Length); /** * @brief 向缓冲区尾指针写一个字节 * @param rb_handle 缓冲区结构体句柄 * @param data 要写入的字节 * @return 返回写入结果 */ uint8_t RB_WriteByte(RingBuffer_TypeDef* rb_handle, uint8_t data); /** * @brief 向缓冲区尾指针写指定长度数据 * @param rb_handle 缓冲区结构体句柄 * @param input_addr 要写入的字符串地址 * @param write_Length 要写入的字符串长度 * @return 返回写入结果 */ uint8_t RB_WriteString(RingBuffer_TypeDef* rb_handle, uint8_t* input_addr, uint32_t write_Length); /** * @brief 从缓冲区头指针读一个字节 * @param rb_handle 缓冲区结构体句柄 * @param output_addr 读取到的字节地址 * @return 返回读取结果 */ uint8_t RB_ReadByte(RingBuffer_TypeDef* rb_handle, uint8_t* output_addr); /** * @brief 从缓冲区头指针读指定长度数据 * @param rb_handle 缓冲区结构体句柄 * @param output_addr 读取到的字符串地址 * @param read_Length 要读取的字符串长度 * @return 返回读取结果 */ uint8_t RB_ReadString(RingBuffer_TypeDef* rb_handle, uint8_t* output_addr, uint32_t read_Length); /** * @brief 获取缓冲区里已储存的数据长度 * @param rb_handle 缓冲区结构体句柄 * @return 返回缓冲区里已储存的数据长度 */ uint32_t RB_GetLength(RingBuffer_TypeDef* rb_handle); /** * @brief 获取缓冲区可用储存空间 * @param rb_handle 缓冲区结构体句柄 * @return 返回缓冲区可用储存空间 */ uint32_t RB_GetFreeSize(RingBuffer_TypeDef* rb_handle); #endif

RingBuffer.c

#include <stdint.h> #include <string.h> #include "RingBuffer.h" /** * @brief 初始化新缓冲区 * @param rb_handle 待初始化的缓冲区结构体句柄 * @param buffer_addr 外部定义的缓冲区数组,类型必须为 uint8_t * @param buffer_size 外部定义的缓冲区数组空间 * @return 返回缓冲区初始化的结果 */ uint8_t RB_Init(RingBuffer_TypeDef* rb_handle, uint8_t* buffer_addr, uint32_t buffer_size) { // 缓冲区数组空间必须大于2且小于数据类型最大值 if (buffer_size < 2 || buffer_size == 0xFFFFFFFF) { return RING_BUFFER_ERROR; // 初始化失败 } rb_handle->Head = 0; // 复位头指针 rb_handle->Tail = 0; // 复位尾指针 rb_handle->Length = 0; // 复位已存储数据长度 rb_handle->ArrayAddr = buffer_addr; // 缓冲区储存数组基地址 rb_handle->MaxLength = buffer_size; // 缓冲区最大可储存数据量 return RING_BUFFER_SUCCESS; // 缓冲区初始化成功 } /** * @brief 从头指针开始删除指定长度的数据 * @param rb_handle 缓冲区结构体句柄 * @param Length 要删除的长度 * @return 返回删除指定长度数据结果 */ uint8_t RB_Delete(RingBuffer_TypeDef* rb_handle, uint32_t Length) { if (rb_handle->Length < Length) { return RING_BUFFER_ERROR; // 已储存的数据量小于需删除的数据量 } else { if ((rb_handle->Head + Length) >= rb_handle->MaxLength) { rb_handle->Head = Length - (rb_handle->MaxLength - rb_handle->Head); } else { rb_handle->Head += Length; // 头指针向前推进,抛弃数据 } rb_handle->Length -= Length; // 重新记录有效数据长度 return RING_BUFFER_SUCCESS; // 已储存的数据量小于需删除的数据量 } } /** * @brief 向缓冲区尾部写一个字节 * @param rb_handle 缓冲区结构体句柄 * @param data 要写入的字节 * @return 返回缓冲区写字节的结果 * @arg RING_BUFFER_SUCCESS: 写入成功 * @arg RING_BUFFER_ERROR: 写入失败 */ uint8_t RB_WriteByte(RingBuffer_TypeDef* rb_handle, uint8_t data) { // 缓冲区数组已满,产生覆盖错误 if (rb_handle->Length == (rb_handle->MaxLength)) { return RING_BUFFER_ERROR; // 缓冲区已满 } else { *(rb_handle->ArrayAddr + rb_handle->Tail) = data; // 基地址+偏移量,存放数据 rb_handle->Length++; // 数据量计数+1 rb_handle->Tail++; // 尾指针后移 } // 如果尾指针超越了数组末尾,尾指针指向缓冲区数组开头,形成闭环 if (rb_handle->Tail > (rb_handle->MaxLength - 1)) { rb_handle->Tail = 0; } return RING_BUFFER_SUCCESS; } /** * @brief 从缓冲区头指针读取一个字节 * @param rb_handle 缓冲区结构体句柄 * @param output_addr 读取的字节保存地址 * @return 返回读取状态 * @arg RING_BUFFER_SUCCESS: 读取成功 * @arg RING_BUFFER_ERROR: 读取失败 */ uint8_t RB_ReadByte(RingBuffer_TypeDef* rb_handle, uint8_t* output_addr) { if (rb_handle->Length != 0) { // 有数据未读出 *output_addr = *(rb_handle->ArrayAddr + rb_handle->Head); // 读取数据 rb_handle->Head++; rb_handle->Length--; // 数据量计数-1 // 如果头指针超越了数组末尾,头指针指向数组开头,形成闭环 if (rb_handle->Head > (rb_handle->MaxLength - 1)) { rb_handle->Head = 0; } return RING_BUFFER_SUCCESS; } return RING_BUFFER_ERROR; } /** * @brief 向缓冲区尾指针写指定长度的数据 * @param rb_handle 缓冲区结构体句柄 * @param input_addr 待写入数据的基地址 * @param write_Length 要写入的字节数 * @return 返回缓冲区尾部写指定长度字节的结果 * @arg RING_BUFFER_SUCCESS: 写入成功 * @arg RING_BUFFER_ERROR: 写入失败 */ uint8_t RB_WriteString(RingBuffer_TypeDef* rb_handle, uint8_t* input_addr, uint32_t write_Length) { // 可写入长度,剩余长度 uint32_t write_size_a = 0, write_size_b = 0; // 如果不够存储空间存放新数据,返回错误 // 注意:写成 (Length + write_Length) > MaxLength 会在 write_Length 接近 UINT32_MAX 时先溢出, // 从而绕过检查并产生超大 memcpy;这里改成先算剩余空间,避免加法溢出 if (write_Length > (rb_handle->MaxLength - rb_handle->Length)) { return RING_BUFFER_ERROR; } else { // 如果顺序可用长度小于需写入的长度,需要将数据拆成两次分别写入 if ((rb_handle->MaxLength - rb_handle->Tail) < write_Length) { write_size_a = rb_handle->MaxLength - rb_handle->Tail; // 缓冲区尾部可用空间 write_size_b = write_Length - write_size_a; // 剩余数据量 // 分别拷贝a、b段数据到储存数组中 memcpy(rb_handle->ArrayAddr + rb_handle->Tail, input_addr, write_size_a); memcpy(rb_handle->ArrayAddr, input_addr + write_size_a, write_size_b); rb_handle->Length += write_Length; // 记录新存储了多少数据量 rb_handle->Tail = write_size_b; // 重新定位尾指针位置 } else { write_size_a = write_Length; // 从尾指针开始写数据 memcpy(rb_handle->ArrayAddr + rb_handle->Tail, input_addr, write_size_a); rb_handle->Length += write_Length; // 记录新存储了多少数据量 rb_handle->Tail += write_size_a; // 重新定位尾指针位置 if (rb_handle->Tail == rb_handle->MaxLength) { rb_handle->Tail = 0; // 如果写入数据后尾指针刚好写到数组尾部,则回到开头,防止越位 } } return RING_BUFFER_SUCCESS; } } /** * @brief 从缓冲区头指针读指定长度的数据,保存到指定的地址 * @param rb_handle 缓冲区结构体句柄 * @param output_addr 读取的数据保存地址 * @param read_Length 要读取的字节数 * @return 返回缓冲区头指针读指定长度字节的结果 * @arg RING_BUFFER_SUCCESS: 读取成功 * @arg RING_BUFFER_ERROR: 读取失败 */ uint8_t RB_ReadString(RingBuffer_TypeDef* rb_handle, uint8_t* output_addr, uint32_t read_Length) { // 可读取长度,剩余长度 uint32_t Read_size_a = 0, Read_size_b = 0; // 如果读取长度大于已储存数据量,返回错误 if (read_Length > rb_handle->Length) { return RING_BUFFER_ERROR; } else { // 读取长度大于缓冲区头指针到数组末尾的距离,需要将数据拆成两次分别读取 if (read_Length > (rb_handle->MaxLength - rb_handle->Head)) { Read_size_a = rb_handle->MaxLength - rb_handle->Head; // 缓冲区头部可用空间 Read_size_b = read_Length - Read_size_a; // 剩余长度 memcpy(output_addr, rb_handle->ArrayAddr + rb_handle->Head, Read_size_a); memcpy(output_addr + Read_size_a, rb_handle->ArrayAddr, Read_size_b); rb_handle->Length -= read_Length; // 记录剩余数据量 rb_handle->Head = Read_size_b; // 重新定位头指针位置 } else { Read_size_a = read_Length; memcpy(output_addr, rb_handle->ArrayAddr + rb_handle->Head, Read_size_a); rb_handle->Length -= read_Length; // 记录剩余数据量 rb_handle->Head += Read_size_a; // 重新定位头指针位置 if (rb_handle->Head == rb_handle->MaxLength) rb_handle->Head = 0; // 如果读取数据后头指针刚好写到数组尾部,则回到开头,防止越位 } return RING_BUFFER_SUCCESS; } } /** * @brief 获取缓冲区里已储存的数据长度 * @param rb_handle 缓冲区结构体句柄 * @return 返回缓冲区里已储存的数据长度 */ uint32_t RB_GetLength(RingBuffer_TypeDef* rb_handle) { return rb_handle->Length; } /** * @brief 获取缓冲区可用储存空间 * @param rb_handle 缓冲区结构体句柄 * @return 返回缓冲区可用储存空间 */ uint32_t RB_GetFreeSize(RingBuffer_TypeDef* rb_handle) { return (rb_handle->MaxLength - rb_handle->Length); }

3.串口驱动

bsp_uart.h

#ifndef __BSP_UART_H__ #define __BSP_UART_H__ #include "stm32g4xx_hal.h" #include "RingBuffer.h" extern DMA_HandleTypeDef hdma_usart3_rx; extern DMA_HandleTypeDef hdma_usart3_tx; // 接收 DMA 双缓冲区大小(必须为偶数:前半区 + 后半区),中断响应慢时加大本宏并同步加大 RX_RB_BUF_SIZE #define RX_DMA_BUF_SIZE 256 // 接收环形缓冲区大小(需大于 RX_DMA_BUF_SIZE) #define RX_RB_BUF_SIZE 512 // 发送环形缓冲区大小 #define TX_RB_BUF_SIZE 512 /** * @brief USART3 接收 DMA 双缓冲结构体 */ typedef struct { uint16_t DMA_RB_Pos; // DMA 缓冲区中已搬运到环形缓冲区的位置 uint16_t Rx_DMA_CurrPos; // DMA 缓冲区当前写入位置 uint16_t Rx_FrameLen; // 自上次空闲中断以来收到的字节数(当前帧长度) uint8_t Rx_DMA_Buf[RX_DMA_BUF_SIZE]; // DMA 双缓冲区:前半区 + 后半区 } UART_Rx_DMA_TypeDef; extern UART_Rx_DMA_TypeDef Uart3_Rx_DMA_t; /** * @brief USART3 发送 DMA 双缓冲结构体 */ typedef struct { uint16_t Tx_Len; // 本次 DMA 正在发送的数据长度(发送完成后才从环形缓冲区删除) uint8_t TxRB_Buf[TX_RB_BUF_SIZE]; // 发送环形缓冲区存储空间 } UART_Tx_DMA_TypeDef; extern UART_Tx_DMA_TypeDef Uart3_Tx_DMA_t; // 接收环形缓冲区 extern RingBuffer_TypeDef Uart3_RxRB_t; // 发送环形缓冲区 extern RingBuffer_TypeDef Uart3_TxRB_t; /** * @brief 初始化 USART3:DMA 双缓冲接收 + 空闲中断 + 收发环形缓冲区 * @note 必须在 MX_USART3_UART_Init() 之后调用 * @note HAL_UARTEx_ReceiveToIdle_DMA() 返回非 HAL_OK(例如 RxState 不是 READY)时接收不会启动, * 此时需要检查 MX_USART3_UART_Init() 是否已执行、或是否有一次接收还没结束 */ void Uart3_Init(void); /** * @brief 非阻塞发送:数据拷贝进发送环形缓冲区后立即返回,由 DMA 自动发完 * @param buf 数据地址 * @param len 数据长度(不能超过 TX_RB_BUF_SIZE) * @return RING_BUFFER_SUCCESS:已写入; RING_BUFFER_ERROR:参数错误或发送缓冲区空间不足 * @note 返回后 buf 可以立即复用/释放;空间不足时整条不写入,应用可稍后重试 */ uint8_t Uart3_Send(const uint8_t* buf, uint16_t len); /** * @brief 单字节非阻塞发送(串口打印的底层出口) * @param ch 待发送字节 * @return RING_BUFFER_SUCCESS:已写入发送缓冲区; RING_BUFFER_ERROR:发送缓冲区满,该字节被丢弃 * @note 不阻塞:打印量太大时中间的字符会被丢掉,避免在中断里长时间等待 */ uint8_t Uart3_SendByte(uint8_t ch); /** * @brief USART3 空闲中断回调:计算当前 DMA 已接收但尚未处理的数据,写入环形缓冲区 * @param huart UART 句柄 * @note HAL 本身没有这个回调,由 HAL_UARTEx_RxEventCallback() 在空闲事件时调用 */ void HAL_UART_RxIdleCallback(UART_HandleTypeDef* huart); /** * @brief 一帧接收完成(弱函数,应用层可重定义) * @param len 本帧长度(自上次空闲中断以来收到的字节数) * @note 在三个接收回调把数据写入环形缓冲区之后调用,可直接 RB_ReadString() 取这一帧 * @note 约定:重定义时必须把整帧从 Uart3_RxRB_t 里读干净(包括超长帧要丢弃的剩余部分), * 否则残留数据会顶在下一帧前面,之后每次解析都会错位 */ void Uart3_RxFrameCallback(uint16_t len); #endif

bsp_uart.c

#include "bsp_uart.h" #include "usart.h" /** * @brief USART3 接收环形缓冲区 */ static uint8_t Uart3_RB_Buf[RX_RB_BUF_SIZE]; UART_Rx_DMA_TypeDef Uart3_Rx_DMA_t = { 0 }; UART_Tx_DMA_TypeDef Uart3_Tx_DMA_t = { 0 }; RingBuffer_TypeDef Uart3_RxRB_t = { 0 }; RingBuffer_TypeDef Uart3_TxRB_t = { 0 }; /* 内部函数声明 */ static void Uart3_TxStart(void); /** * @brief 初始化 USART3 */ void Uart3_Init(void) { // 初始化接收环形缓冲区,缓冲区需大于DMA缓冲区 RB_Init(&Uart3_RxRB_t, Uart3_RB_Buf, RX_RB_BUF_SIZE); // 初始化发送环形缓冲区(放在启动接收之前:否则刚启动就收到帧时,回显用的发送缓冲区还没准备好) RB_Init(&Uart3_TxRB_t, Uart3_Tx_DMA_t.TxRB_Buf, TX_RB_BUF_SIZE); Uart3_Tx_DMA_t.Tx_Len = 0; // 复位接收状态 Uart3_Rx_DMA_t.DMA_RB_Pos = 0; Uart3_Rx_DMA_t.Rx_DMA_CurrPos = 0; Uart3_Rx_DMA_t.Rx_FrameLen = 0; // 启动 DMA 双缓冲接收 + 空闲中断 HAL_UARTEx_ReceiveToIdle_DMA(&huart3, Uart3_Rx_DMA_t.Rx_DMA_Buf, RX_DMA_BUF_SIZE); } /** * @brief DMA 接受半缓冲区回调,防止在全满中断时数据还在接受导致数据丢失 * @note 此时前半缓冲区也被空闲中断写入环形缓冲区,后半缓冲区继续接收 */ void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef* huart) { uint16_t prev_pos; // DMA缓冲区已处理数据的位置 if (huart->Instance == USART3) { prev_pos = Uart3_Rx_DMA_t.DMA_RB_Pos; // 前prev_pos位置已经被空闲中断(半DMA缓冲区前面部分触发)或者全满中断处理过了,只需处理剩余DMA数据,即prev_pos到RX_DMA_BUF_SIZE/2的数据 if (prev_pos < (RX_DMA_BUF_SIZE / 2)) { uint16_t len = (RX_DMA_BUF_SIZE / 2) - prev_pos; // 只统计真正写进环形缓冲区的字节:缓冲区满时写不进去的不能算进帧长,否则下一帧会错位 if (RB_WriteString(&Uart3_RxRB_t, &Uart3_Rx_DMA_t.Rx_DMA_Buf[prev_pos], len) == RING_BUFFER_SUCCESS) { Uart3_Rx_DMA_t.Rx_FrameLen += len; } // 更新DMA缓冲区已处理数据的位置 Uart3_Rx_DMA_t.DMA_RB_Pos = RX_DMA_BUF_SIZE / 2; } // DMA 缓冲区当前写入位置(以 DMA 计数器为准,与下面两个回调保持一致) Uart3_Rx_DMA_t.Rx_DMA_CurrPos = RX_DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart3_rx); } } /** * @brief DMA 接收传输完成回调(TC) * @note 此时后半缓冲区写入环形缓冲区 * @note 以 DMA 计数器计算当前位置:回绕后位置回到起点,不能用固定的末尾位置 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) { uint16_t prev_pos; // DMA缓冲区已处理数据的位置 uint16_t curr_pos; // DMA缓冲区当前写入位置 if (huart->Instance == USART3) { // 计算当前DMA缓冲区的偏移量,循环模式下 DMA 不会停:整个缓冲区收满只是"事件时刻",进中断时 NDTR 早已不是 0 curr_pos = RX_DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart3_rx); prev_pos = Uart3_Rx_DMA_t.DMA_RB_Pos; Uart3_Rx_DMA_t.Rx_DMA_CurrPos = curr_pos; if (curr_pos < prev_pos) { // DMA 已经回绕,先把DMA缓冲区的末尾剩余数据写入 uint16_t len = RX_DMA_BUF_SIZE - prev_pos; if (RB_WriteString(&Uart3_RxRB_t, &Uart3_Rx_DMA_t.Rx_DMA_Buf[prev_pos], len) == RING_BUFFER_SUCCESS) { Uart3_Rx_DMA_t.Rx_FrameLen += len; } prev_pos = 0; } // 后半缓冲区写入环形缓冲区 if (curr_pos > prev_pos) { uint16_t len = curr_pos - prev_pos; if (RB_WriteString(&Uart3_RxRB_t, &Uart3_Rx_DMA_t.Rx_DMA_Buf[prev_pos], len) == RING_BUFFER_SUCCESS) { Uart3_Rx_DMA_t.Rx_FrameLen += len; } } // 更新DMA缓冲区已处理数据的位置 Uart3_Rx_DMA_t.DMA_RB_Pos = curr_pos; } } /** * @brief USART3 空闲中断回调 * @note 当检测到线路空闲时,计算当前 DMA 已接收但尚未处理的数据,将其写入环形缓冲区 * @note HAL 本身没有这个回调,由 HAL_UARTEx_RxEventCallback() 在空闲事件时调用 */ void HAL_UART_RxIdleCallback(UART_HandleTypeDef* huart) { uint16_t prev_pos; // DMA缓冲区已处理数据的位置 uint16_t curr_pos; // DMA缓冲区当前写入位置 if (huart->Instance == USART3) { // 计算当前DMA缓冲区的偏移量 curr_pos = RX_DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart3_rx); prev_pos = Uart3_Rx_DMA_t.DMA_RB_Pos; Uart3_Rx_DMA_t.Rx_DMA_CurrPos = curr_pos; if (curr_pos > prev_pos) { uint16_t len = curr_pos - prev_pos; if (RB_WriteString(&Uart3_RxRB_t, &Uart3_Rx_DMA_t.Rx_DMA_Buf[prev_pos], len) == RING_BUFFER_SUCCESS) { Uart3_Rx_DMA_t.Rx_FrameLen += len; } } // 更新DMA缓冲区已处理数据的位置 Uart3_Rx_DMA_t.DMA_RB_Pos = curr_pos; } } /** * @brief UART 接收事件回调,把 HAL 的三种接收事件分发给上面三个回调 * @param huart UART 句柄 * @param Size HAL 传入的已接收数据位置 * @note TOIDLE 模式(ReceiveToIdle_DMA)下三种事件都进本函数,由 huart->RxEventType 区分 * @note 不使用 HAL 传入的 Size:空闲恰好发生在回绕点上时 HAL 会传入整个缓冲区大小 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size) { (void)Size; if (huart->Instance != USART3) { return; } if (huart->RxEventType == HAL_UART_RXEVENT_HT) { // 前半区收满 HAL_UART_RxHalfCpltCallback(huart); } else if (huart->RxEventType == HAL_UART_RXEVENT_TC) { // 后半区收满 HAL_UART_RxCpltCallback(huart); } else if (huart->RxEventType == HAL_UART_RXEVENT_IDLE) { // 总线空闲,一帧接收结束 HAL_UART_RxIdleCallback(huart); if (Uart3_Rx_DMA_t.Rx_FrameLen > 0) { Uart3_RxFrameCallback(Uart3_Rx_DMA_t.Rx_FrameLen); } Uart3_Rx_DMA_t.Rx_FrameLen = 0; } } /** * @brief 一帧接收完成,默认空实现,应用层可重定义 * @param len 本帧长度 */ __weak void Uart3_RxFrameCallback(uint16_t len) { (void)len; } /** * @brief 出错后重新启动接收/发送 */ void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart) { if (huart->Instance != USART3) { return; } if (huart->RxState != HAL_UART_STATE_BUSY_RX) { Uart3_Rx_DMA_t.DMA_RB_Pos = 0; Uart3_Rx_DMA_t.Rx_DMA_CurrPos = 0; Uart3_Rx_DMA_t.Rx_FrameLen = 0; RB_Delete(&Uart3_RxRB_t, RB_GetLength(&Uart3_RxRB_t)); // 丢掉未接收完的残留 HAL_UARTEx_ReceiveToIdle_DMA(&huart3, Uart3_Rx_DMA_t.Rx_DMA_Buf, RX_DMA_BUF_SIZE); } if (huart->gState != HAL_UART_STATE_BUSY_TX) { Uart3_Tx_DMA_t.Tx_Len = 0; Uart3_TxStart(); } } /** * @brief 启动一段 DMA 发送,环形缓冲区回绕时只发送连续的一段 */ static void Uart3_TxStart(void) { uint16_t len = Uart3_TxRB_t.Length; uint16_t cont = Uart3_TxRB_t.MaxLength - Uart3_TxRB_t.Head; // 头指针到缓冲区末尾的连续长度 if ((len == 0) || (huart3.gState != HAL_UART_STATE_READY)) { return; // 无数据,或上一段还在发送(由发送完成中断续发) } if (len > cont) { len = cont; // 先发到头指针到缓冲区末尾这一段,剩下的一段由发送完成中断续发 } if (HAL_UART_Transmit_DMA(&huart3, &Uart3_TxRB_t.ArrayAddr[Uart3_TxRB_t.Head], len) == HAL_OK) { Uart3_Tx_DMA_t.Tx_Len = len; } } /** * @brief DMA 发送完成回调:删除已发送完的数据,并续发环形缓冲区里剩下的数据 * @note 已发送的数据必须在这里才删除:DMA 发送期间一直在读环形缓冲区,提前删除会被新数据覆盖 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart) { if (huart->Instance != USART3) { return; } RB_Delete(&Uart3_TxRB_t, Uart3_Tx_DMA_t.Tx_Len); // 释放本次已经发送完的数据 Uart3_Tx_DMA_t.Tx_Len = 0; Uart3_TxStart(); // 环形缓冲区里还有数据则继续发送 } /** * @brief 非阻塞发送:数据拷贝进发送环形缓冲区后立即返回,由 DMA 自动发完 * @param buf 数据地址 * @param len 数据长度(不能超过 TX_RB_BUF_SIZE) * @return RING_BUFFER_SUCCESS:已写入; RING_BUFFER_ERROR:参数错误或发送缓冲区空间不足 */ uint8_t Uart3_Send(const uint8_t* buf, uint16_t len) { uint8_t ret; uint32_t irq; if ((buf == NULL) || (len == 0) || (len > TX_RB_BUF_SIZE)) { return RING_BUFFER_ERROR; // 参数错误:空指针 / 空数据 / 超过发送缓冲区容量 } irq = __get_PRIMASK(); __disable_irq(); // 与发送完成中断互斥:中断里会删除已发数据并更新长度计数 ret = RB_WriteString(&Uart3_TxRB_t, (uint8_t*)buf, len); __set_PRIMASK(irq); if (ret == RING_BUFFER_SUCCESS) { Uart3_TxStart(); // 上一段已发完则立即启动发送 } return ret; } /** * @brief 单字节非阻塞发送(Uart3_Send 的单字节封装,串口打印用) * @param ch 待发送字节 * @return RING_BUFFER_SUCCESS:已写入; RING_BUFFER_ERROR:发送缓冲区满,该字节被丢弃 */ uint8_t Uart3_SendByte(uint8_t ch) { return Uart3_Send(&ch, 1); }

app_uart.c和app_uart.h的应用层实现,发送函数调用Uart3_SendByte或者Uart3_Send即可,接受函数Uart3_RxFrameCallback实现调用RB_ReadString函数读取缓冲区即可。

main.c函数初始化调用Uart3_Init()函数。

相关新闻

端侧AI芯片选型指南:AX8850架构、算力与工具链实战解析

端侧AI芯片选型指南:AX8850架构、算力与工具链实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:08:45 阅读更多 →
RuoYi-Vue-Plus 实战指南:多租户后台管理系统从零跑通到选型判断

RuoYi-Vue-Plus 实战指南:多租户后台管理系统从零跑通到选型判断

RuoYi-Vue-Plus 实战指南&#xff1a;多租户后台管理系统从零跑通到选型判断 【免费下载链接】RuoYi-Vue-Plus 多租户后台管理系统 重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、WarmFlow、SpringDoc、Hutool、OSS 定期同步 项目地址: https://gitcode.com/GitHub_Tr…

2026/10/21 2:08:45 阅读更多 →
Simulink搭建AUTOSAR模型的工程实践与RTE一致性避坑指南

Simulink搭建AUTOSAR模型的工程实践与RTE一致性避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:08:45 阅读更多 →

最新新闻

Quartz动态设置并发数量

Quartz动态设置并发数量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 2:48:00 阅读更多 →
AI Agent ROI评估指南:从成本结构到收益测算全解析

AI Agent ROI评估指南:从成本结构到收益测算全解析

1. 先别急着算账&#xff0c;想清楚AI Agent的ROI和传统IT项目有什么不同这两年不少公司都开始折腾AI Agent&#xff0c;有的把它接到客服系统&#xff0c;有的让它帮着写代码&#xff0c;还有的用它处理合同和报表。但当你真坐下来算这笔账时&#xff0c;会发现一个尴尬的问题…

2026/10/21 2:48:00 阅读更多 →
基于卷积神经网络的食物图像识别实战:从ResNet迁移学习到部署

基于卷积神经网络的食物图像识别实战:从ResNet迁移学习到部署

简介&#xff1a;基于卷积神经网络的食物图像识别项目完整代码与数据包&#xff0c;面向具备Python基础并希望实践CNN图像分类的开发者与学习者。资源围绕TensorFlow与tf.keras构建食物识别模型&#xff0c;涵盖数据预处理、网络定义、训练评估与预测全流程&#xff0c;并引入迁…

2026/10/21 2:48:00 阅读更多 →
SECS-II/HSMS调试工具实战:从协议原理到模拟器应用

SECS-II/HSMS调试工具实战:从协议原理到模拟器应用

简介&#xff1a;面向半导体制造与MES系统集成场景&#xff0c;这份SECS-II HSMS调试工具模拟器适合设备工程师、上位机开发人员使用。它可模拟客户端与服务端&#xff0c;用于验证自定义程序是否符合SECS-II通信标准&#xff0c;减少现场联调时的错误与返工。压缩包为rar格式&…

2026/10/21 2:48:00 阅读更多 →
eNSP校园网拓扑设计实战:VLAN划分、DHCP配置与静态路由全解析

eNSP校园网拓扑设计实战:VLAN划分、DHCP配置与静态路由全解析

简介&#xff1a;面向计算机网络期末设计、课程设计及eNSP仿真练习的完整项目资料包&#xff0c;围绕某中学校园网络拓扑设计展开。方案包含网络拓扑选型、综合布线系统规划、教学楼/行政楼/宿舍接入设计&#xff0c;以及VLAN划分、IP地址分配、路由协议配置等模拟配置要点&…

2026/10/21 2:48:00 阅读更多 →
思科模拟器6.2:从安装到避坑的完整实战指南

思科模拟器6.2:从安装到避坑的完整实战指南

简介&#xff1a;这是一份思科模拟器6.2安装包&#xff0c;官方名称为Packet Tracer&#xff0c;面向网络技术学习者、思科认证考生以及开展网络实验教学的教师。软件以图形化方式搭建虚拟网络环境&#xff0c;可模拟路由器、交换机、无线接入点、防火墙等设备&#xff0c;支持…

2026/10/21 2:47:00 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →