说明
最近在看到一篇博文:地址,讲高效串口的实现,简单的说就是利用了DMA + 空闲中断 + 双缓冲 + 循环接收方式,实际音频上面也是双缓冲这样的实现方式,只不过串口这边一直没有做相应的优化,现在有时间就优化下串行的驱动框架代码
硬件平台
- STM32H743VIT6
- BSP:HAL库
代码实现
代码拥有三个接口文件:
SERIAL_Port
实现串行数据的处理接收、发送
/**
* @file SERIAL_Port.h
*
* @date 2022年10月18日 10:20:26 星期二
*
* @author Copyright (c) 2022 aron566 <aron566@163.com>.
*
* @brief 串口操作接口.
*
* @version v1.0.0
*/
#ifndef _SERIAL_PORT_H
#define _SERIAL_PORT_H
/** Includes -----------------------------------------------------------------*/
#include <stdint.h> /**< need definition of uint8_t */
#include <stddef.h> /**< need definition of NULL */
#include <stdbool.h>/**< need definition of BOOL */
#include <stdio.h> /**< if need printf */
#include <stdlib.h>
#include <string.h>
// #include <limits.h> /**< need variable max value */
// #include <stdalign.h> /**< need alignof */
// #include <stdarg.h> /**< need va_start */
/** Private includes ---------------------------------------------------------*/
#include "CircularQueue.h"
/** Use C compiler -----------------------------------------------------------*/
#ifdef __cplusplus ///< use C compiler
extern "C" {
#endif
/** Private defines ----------------------------------------------------------*/
#define USE_USB_CDC 0
/** Exported typedefines -----------------------------------------------------*/
/* 串口端口号 */
typedef enum
{
SERIAL_PORT_UART_0 = 0,
SERIAL_PORT_UART_1,
SERIAL_PORT_UART_2,
SERIAL_PORT_UART_3,
SERIAL_PORT_UART_4,
SERIAL_PORT_UART_5,
SERIAL_PORT_UART_6,
SERIAL_PORT_UART_7,
SERIAL_PORT_UART_8,
SERIAL_PORT_USB_CDC_0,
SERIAL_PORT_USB_CDC_1,
SERIAL_PORT_USB_CDC_2,
SERIAL_PORT_NUM_MAX,
}SERIAL_PORT_SERIAL_NUM_Typedef_t;
/* 串口状态 */
typedef enum
{
SERIAL_PORT_READY = 0,
SERIAL_PORT_TX_IDEL = 1 << 0,
SERIAL_PORT_RX_IDEL = 1 << 1,
SERIAL_PORT_RX_TX_IDEL= 3,
SERIAL_PORT_TX_BUSY = 1 << 2,
SERIAL_PORT_RX_BUSY = 1 << 3,
SERIAL_PORT_RX_TX_BUSY= 12,
SERIAL_PORT_ERROR = 1 << 4,
}SERIAL_STATE_Typedef_t;
/** Exported constants -------------------------------------------------------*/
/** Exported macros-----------------------------------------------------------*/
/** Exported variables -------------------------------------------------------*/
/** Exported functions prototypes --------------------------------------------*/
/**
* @brief 串口初始化
*
*/
void Serial_Port_Init(void);
/**
* @brief 串口反初始化
*
*/
void Serial_Port_DeInit(void);
/**
* @brief 串口数据发送
*
* @param Serial_Num 串口号
* @param Data 数据,阻塞时间为0时不可使用局部变量
* @param Len 数据长度
* @param Block_Time 阻塞时间
* @return true 调用发送成功
* @return false 调用发送失败
*/
bool Serial_Port_Transmit_Data(SERIAL_PORT_SERIAL_NUM_Typedef_t Serial_Num, const uint8_t *Data, uint32_t Len, uint32_t Block_Time);
/**
* @brief 获取串口空闲状态
*
* @param Serial_Num 串口号
* @return SERIAL_STATE_Typedef_t 状态
*/
SERIAL_STATE_Typedef_t Serial_Port_Get_Idel_State(SERIAL_PORT_SERIAL_NUM_Typedef_t Serial_Num);
/**
* @brief 获取环形句柄
*
* @param Serial_Num 串口号
* @return CQ_handleTypeDef* 环形句柄
*/
CQ_handleTypeDef *Serial_Port_Get_CQ(SERIAL_PORT_SERIAL_NUM_Typedef_t Serial_Num);
/**
* @brief 串口半中断
*
* @param Arg 串口句柄参数
*/
void Serial_Port_Half_IT_CallBack(void *Arg);
/**
* @brief 串口完成中断
*
* @param Arg 串口句柄参数
*/
void Serial_Port_Complete_IT_CallBack(void *Arg);
/**
* @brief 串口中断
*
* @param Arg 串口句柄参数
* @param Size 数据大小字节
*/
void Serial_Port_IT_CallBack(void *Arg, uint32_t Size);
/**
* @brief CDC串口完成中断
*
* @param Arg CDC句柄参数
* @param Data 数据
* @param Len 数据长度
* @param EPNum 端口号
*/
void Serial_Port_Complete_CDC_IT_CallBack(void *Arg, const uint8_t *Data, uint32_t Len, uint8_t EPNum);
/**
* @brief 设置串口波特率
*
* @param Serial_Num 串口号
* @param BaudRate 波特率
*/
void Serial_Port_Set_BaudRate(SERIAL_PORT_SERIAL_NUM_Typedef_t Serial_Num, uint32_t BaudRate);
#ifdef __cplusplus ///<end extern c
}
#endif
#endif
/******************************** End of file *********************************/
CircularQueue
实现环形缓冲区
utilities
实现部分宏定义接口
测试
将SERIAL_Port.c
文件中,回环测试打开
#define USE_LOOPBACK 1 /**< 是否使用数据回环打印 */
移植
如果使用STM32平台直接配置串口后,移植以上三个文件即可,如果是其他平台,需要实现以下接口
/* 串口设备对象 */
typedef struct Serial_Port_Handle
{
/* 串口句柄 */
void *pSerial_Handle;
/* 数据缓存 */
CQ_handleTypeDef *pCQ_Handle;
uint8_t *pReceive_Buffer;
uint32_t Buffer_Size;
/* 接收数据大小累积,每次满中断置0 */
uint32_t Last_Rec_Data_Size_Cnt;
/* 阻塞 */
void *pSemaphoreId;
void (*pLock_CallBack)(void *pSemaphoreId);
void (*pUnLock_CallBack)(void *pSemaphoreId);
/* 初始化,反初始化 */
void (*pInit)(void *pSerial_Port_Handle);
void (*pDeInit)(void *pSerial_Port_Handle);
/* 发送,接收接口 */
bool (*pSend_Data_Start)(void *pSerial_Port_Handle, const uint8_t *Data, uint32_t Len, uint32_t BlockTime);
bool (*pReceive_Data_Start)(void *pSerial_Port_Handle, uint8_t *Buffer, uint32_t Len, uint32_t BlockTime);
/* 启动接口 */
bool (*pStart)(void *pSerial_Port_Handle);
/* 获取状态 */
SERIAL_STATE_Typedef_t (*pGet_Serial_State)(void *pSerial_Port_Handle);
/* 设置波特率 */
bool (*pSet_BaudRate)(void *pSerial_Port_Handle, uint32_t BaudRate);
/* 中断回调接口 */
void (*pReceive_Half_IT)(void *pSerial_Port_Handle);
void (*pReceive_Complete_IT)(void *pSerial_Port_Handle);
void (*pSerial_IT)(void *pSerial_Port_Handle);
/* USB CDC私有 */
uint8_t INepNum; /**< 主机接收端点地址 */
uint8_t OUTepNum; /**< 主机发送端点地址 */
void (*pSerial_Rec_IT)(void *pSerial_Port_Handle, const uint8_t *Data, uint32_t Len);
}SERIAL_PORT_HANDLE_Typedef_t;
可以参考以下
/* 初始化串口 */
SERIAL_PORT_HANDLE_Typedef_t *pSerial_Device = &Serial_Device_List[SERIAL_PORT_UART_2];
pSerial_Device->pSerial_Handle = &huart2;
pSerial_Device->pCQ_Handle = cb_create(1024);
if(NULL == pSerial_Device->pCQ_Handle)
{
printf("Serial Port Uart 2 Malloc Buf Faild.\r\n");
return;
}
pSerial_Device->pReceive_Buffer = Uart2_DMA_Buf;//(uint8_t *)SERIAL_PORT_MALLOC(1024);
pSerial_Device->Buffer_Size = 16;
pSerial_Device->pStart = UART_Start;
pSerial_Device->pInit = UART_Init;
pSerial_Device->pDeInit = UART_DeInit;
pSerial_Device->pSend_Data_Start = UART_Send_Data_Start;
pSerial_Device->pReceive_Data_Start = UART_Receive_Data_Start;
pSerial_Device->pGet_Serial_State = UART_Get_Idel_State;
pSerial_Device->pSet_BaudRate = UART_Set_BaudRate;
pSerial_Device->pSemaphoreId = NULL;
pSerial_Device->pLock_CallBack = NULL;
pSerial_Device->pUnLock_CallBack = NULL;
pSerial_Device->pReceive_Half_IT = UART_Receive_Half_IT;
pSerial_Device->pReceive_Complete_IT = UART_Receive_Complete_IT;
pSerial_Device->pSerial_IT = UART_Idel_IT;
pSerial_Device->Last_Rec_Data_Size_Cnt = 0;
/* 启动接收 */
pSerial_Device->pStart(pSerial_Device);
并将中断服务函数添加到相应的位置(Serial_Port_IT_CallBack
、Serial_Port_Half_IT_CallBack
、Serial_Port_Complete_IT_CallBack
),参考以下:
/**
* @brief 串口接收事件中断
*
* @param huart 串口句柄
* @param Size 接收数据大小
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
Serial_Port_IT_CallBack(huart, Size);
}
/**
* @brief 串口半接收完成中断
*
* @param huart 串口句柄
*/
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
Serial_Port_Half_IT_CallBack(huart);
}
/**
* @brief 串口接收完成中断
*
* @param huart 串口句柄
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
Serial_Port_Complete_IT_CallBack(huart);
}
需要注意的地方
以STM32为例:
- 使用
HAL_UARTEx_ReceiveToIdle_DMA
接口,半接收中断服务函数与接收完成服务函数为HAL_UARTEx_RxEventCallback
所以程序中做了如下处理:
/**
* @brief 串口中断
*
* @param Arg 串口句柄参数
* @param Size 数据大小字节
*/
void Serial_Port_IT_CallBack(void *Arg, uint32_t Size)
{
for(int i = 0; i < SERIAL_PORT_NUM_MAX; i++)
{
if(Arg == Serial_Device_List[i].pSerial_Handle)
{
/* 检测到半完成中断类型 */
if(Size == Serial_Device_List[i].Buffer_Size / 2)
{
if(NULL == Serial_Device_List[i].pReceive_Half_IT)
{
return;
}
Serial_Device_List[i].pReceive_Half_IT(&Serial_Device_List[i]);
return;
}
/* 检测到完成中断类型 */
if(Size == Serial_Device_List[i].Buffer_Size)
{
if(NULL == Serial_Device_List[i].pReceive_Complete_IT)
{
return;
}
Serial_Device_List[i].pReceive_Complete_IT(&Serial_Device_List[i]);
return;
}
/* 空闲 or 其他 */
if(NULL == Serial_Device_List[i].pSerial_IT)
{
return;
}
Serial_Device_List[i].pSerial_IT(&Serial_Device_List[i]);
return;
}
}
}
- H7有总线访问的问题,需要注意自己的外设能不能访问某一段内存,以及DMA与Cache的影响数据错误的问题
- “空闲中断中必须先停止接收才能拿到正确数据” 某博主的这句话不一定全部适用于STM32芯片,H7就没这事,不过也预留了接口去启用
#define USE_DMA_CIRCULAR_MODE 1 /**< 已使用硬件DMA循环接收模式,1代表打开,0代表未打开(未打开或者主动设置为0时,接收到数据将先停止接收再主动重启接收) */
代码仓库
https://github.com/aron566/Serial_Port