#本文章来源于韦东山老师B站视频课程《FreeRTOS下UART的封装》#
- 这里仅为个人对原作者视频内容的记录和自己的一些体会
为什么使用架构
在嵌入式程序中,有几种类型的程序层级,最初级的是完全耦合,应用程序直接操作寄存器,第二类是对驱动层进行抽象,抽象出类似于HAL、LL库的代码,app直接操作驱动层,第三类是在驱动之上再进行封装,支持用户库和应用。更高层级的抽象可以为用户层提供稳定的接口,避免了硬件调整造成的额外工作。
本文所重点讲述的就是如何在驱动层进行进一步抽象
路线选择:面向过程or面向对象
从左侧依赖hal、依赖硬件了解(IT硬件中断、huart1串口号)知识的软件进行抽象有两种思路,分别是函数式、对象式。下面分别是两种实现的例子。我用自己的话总结了以下他们的本质区别:函数式或者过程式,需要将差异紧耦合在过程内部,导致串口号遍历、接口函数遍历等各种扩展性问题;对象式编程将差异封装在对象内部,可以独立设计、使用。
外设对象抽象(基于rtos&hal库进行Device封装)
定义设备对象
定义串口设备对象UART_Device,其中void *指针用于指向一个私有数据类型UART_Data(不对外暴露,每一类串口都要实现的一个数据对象,因操作系统、驱动函数而变化,包括串口设备句柄、信号量、队列、接受数据缓存字符)
struct UART_Device {
char *name;
int (*Init)(struct UART_Device *pDev, int baud, int datas, char parity, int stop);
int (*Send)(struct UART_Device *pDev, uint8_t *datas, int len, int timeout_ms);
int (*Recv)(struct UART_Device *pDev, uint8_t *data, int timeout_ms);
void *priv_data;
};
// 以下为私有类型,针对驱动库,操作系统可以进行调整
struct UART_Data {
UART_HandleTypeDef *handle;
SemaphoreHandle_t xTxSem;
QueueHandle_t xRxQueue;
uint8_t rxdata;
};
// 创建静态数据
#define UART_RX_QUEUE_LEN 100 // dev 数据队列长度
struct UART_Device g_stm32_uart1; // dev 对象
static struct UART_Data g_stm32_uart1_data = { // dev 对象私有数据
&huart1,
};
定义设备操作
本实例中以freertos操作系统为例,基于hal库进行外设操作,串口收发使用中断,除封装收发函数外还需要再对应中断处理函数中进行数据处理,同步使用信号量和队列,均由操作系统提供。
extern UART_HandleTypeDef huart1; // dev 对应串口号
static int stm32_uart_init(struct UART_Device *pDev, int baud, int datas, char parity, int stop)
{
struct UART_Data *data = pDev->priv_data;
data->xTxSem = xSemaphoreCreateBinary();
data->xRxQueue = xQueueCreate(UART_RX_QUEUE_LEN, 1);
/* 启动第1次数据的接收 */
HAL_UART_Receive_IT(data->handle, &data->rxdata, 1);
return 0;
}
static int stm32_uart_send(struct UART_Device *pDev, uint8_t *datas, int len, int timeout_ms)
{
struct UART_Data *data = pDev->priv_data;
/* 仅仅是触发中断而已 */
HAL_UART_Transmit_IT(data->handle, datas, len);
/* 等待发送完毕:等待信号量 */
if (pdTRUE == xSemaphoreTake(data->xTxSem, timeout_ms))
return 0;
else
return -1;
}
static int stm32_uart_recv(struct UART_Device *pDev, uint8_t *data, int timeout_ms)
{
struct UART_Data *uart_data = pDev->priv_data;
/* 读取队列得到数据, 问题:谁写队列?中断:写队列 */
if (pdPASS == xQueueReceive(uart_data->xRxQueue, data,timeout_ms))
return 0;
else
return -1;
}
// 中断处理程序
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
struct UART_Data *data;
if (huart == &huart1)
{
data = g_stm32_uart1.priv_data;
/* 释放信号量 */
xSemaphoreGiveFromISR(data->xTxSem, NULL);
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
struct UART_Data *data;
if (huart == &huart1)
{
data = g_stm32_uart1.priv_data;
/* 写队列 */
xQueueSendFromISR(data->xRxQueue, &data->rxdata, NULL);
/* 再次启动数据的接收 */
HAL_UART_Receive_IT(data->handle, &data->rxdata, 1);
}
}
接口暴露
struct UART_Device *g_uart_devs[] = {&g_stm32_uart1};
struct UART_Device *GetUARTDevice(char *name)
{
int i = 0;
for (i = 0; i < sizeof(g_uart_devs)/sizeof(g_uart_devs[0]); i++)
{
if (0 == strcmp(name, g_uart_devs[i]->name))
return g_uart_devs[i];
}
return NULL;
}
// 整个文件暴露了一个UART_Device类型和GetUARTDevice函数
// 调用:
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN 5 */
char c;
struct UART_Device *pUARTDev = GetUARTDevice("stm32_uart1");
pUARTDev->Init(pUARTDev, 115200, 8, 'N', 1);
/* Infinite loop */
for(;;)
{
//HAL_UART_Transmit_IT(&huart1, "100ask\r\n", 8);
//HAL_Delay(100);
pUARTDev->Send(pUARTDev, "100ask\r\n", 8, 100);
//while (HAL_OK != HAL_UART_Receive(&huart1, &c, 1, 100));
while (0 != pUARTDev->Recv(pUARTDev, &c, 100));
c++;
//HAL_UART_Transmit_IT(&huart1, &c, 1);
//HAL_Delay(1);
pUARTDev->Send(pUARTDev, &c, 1, 1);
}
/* USER CODE END 5 */
}
优化
有没有可能进一步优化程序呢?个人觉得现阶段的程序有如下问题:1.使用字符串定位串口对象,占用资源且存在不确定性 2.存在静态内存开销,也就是没有用驱动功能的时候就存在内存开销了 3.对象封装上见仁见智,此种封装方法简洁高效,可以借鉴。