0x00 前言
做了很多项目,写过很多驱动文件,偶尔新开一个工程,就得去一大堆工程里面翻找自己需要的驱动文件,然后改引脚改宏定义加变量,比较繁琐,于是打算自己写一个嵌入式框架,用来提升开发效率。
准备以程序分层的思想去设计,这样一来即便是不同的单片机平台,也能快速开始使用
0x01 分层介绍
在我们的框架中,会对底层进行封装。在此框架完成之后,我们在应用层进行使用时,无论底层的是什么单片机,我们都只需要直接进行使用即可,极大的增强了代码的可移植性。
如下图所示,在硬件抽象层中,为所有的单片机提供统一的接口,根据所选择的单片机平台去调用它们官方的底层库。需要牢记的思想是不能跨层调用,下层为上层提供API,只能由上层去调用下层的函数,例如在交互界面中如果需要点亮LED灯,那么就应该去使用功能模块层的LED_ON函数去点亮,而非直接调用底层的GPIO_BitSet函数去驱动LED灯。
0X02 硬件抽象层函数示例
硬件抽象层提供了一个统一的接口,使得上层软件可以独立于具体的硬件实现。它隐藏了硬件的复杂性,简化了软件开发。
硬件接口:提供对硬件的统一访问接口,允许上层软件通过标准化的方式与硬件交互。
平台独立性:使得应用层和功能层可以在不同的硬件平台上运行,而无需修改代码。
资源管理:管理硬件资源的分配和使用,确保不同模块之间的协调。
在一个嵌入式系统中,HAL 可能提供对 GPIO、UART、I2C 等硬件接口的统一访问。
在一个多平台的嵌入式应用中,HAL 使得相同的应用代码可以在不同的微控制器上运行,例如:
// GPIO 初始化函数
void HAL_GPIO_Init(uint32_t GPIOx, GPIO_InitTypeDef *GPIO_Init);
// GPIO 写函数
void HAL_GPIO_WritePin(uint32_t GPIOx, uint32_t Pin, GPIO_PinState PinState);
// GPIO 读函数
uint8_t HAL_GPIO_ReadPin(uint32_t GPIOx, uint32_t Pin);
而 HAL_GPIO_Init 这个函数的底层应该如何去写呢?
写的时候我个人更倾向于将底层分为不同的文件去进行编写,然后根据所选择的芯片去进行不同的封装。
#ifdef STM32F103C8
#include "stm32f10x.h"
void HAL_GPIO_Init(uint32_t _GPIOx, GPIO_InitTypeDef *_GPIO_Init)
{
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = _GPIO_Init->Mode;
GPIO_InitStruct.GPIO_Pin = _GPIO_Init->Pin;
GPIO_InitStruct.GPIO_Speed = _GPIO_Init->Speed;
GPIO_Init(_GPIOx,&GPIO_InitStruct);
}
void HAL_GPIO_WRITE(uint32_t _GPIOx,uint32_t _Pin,uint32 _Value)
{
if(_Value)
GPIO_SetBits(_GPIOx,_Pin);
else
GPIO_ResetBits(_GPIOx,_Pin);
}
uint8_t HAL_GPIO_READ(uint32_t _GPIOx,uint32_t _Pin)
{
return GPIO_ReadInputDataBit(_GPIOx,_Pin);
}
#else ifdef GD32F103C8
#include "gd32f10x.h"
void HAL_GPIO_Init(uint32_t _GPIOx, GPIO_InitTypeDef *_GPIO_Init)
{
gpio_mode_set((uint32_t)_GPIOx, _GPIO_Init->Mode, _GPIO_Init->Pull, _GPIO_Init->Pin);
gpio_output_options_set((uint32_t)_GPIOx, _GPIO_Init->Speed, _GPIO_Init->Alternate, _GPIO_Init->Pin);
}
void HAL_GPIO_WRITE(uint32_t _GPIOx,uint32_t _Pin,uint32 _Value)
{
if(_Value)
gpio_bit_set(_GPIOx,_Pin);
else
gpio_bit_reset(_GPIOx, _Pin);
}
uint8_t HAL_GPIO_READ(uint32_t _GPIOx,uint32_t _Pin)
{
return gpio_input_bit_get(_GPIOx,_Pin);
}
#else
#error "Please Select Your Mcu!"
#endif
如此一来,倘若你的底层使用的是STM32F103C8,那么使用以下语句即可:
#define STM32F103C8
//#define GD32F103C8
0x03 硬件驱动层函数示例
硬件驱动层负责与具体的硬件设备进行交互,提供对硬件的控制和管理。它是功能层与硬件之间的桥梁。
设备控制:实现对硬件设备的初始化、配置和控制。
数据传输:负责从硬件设备读取数据或向其发送命令。
中断处理:处理来自硬件的中断请求,确保及时响应硬件事件。
在传感器系统中,硬件驱动层可能负责初始化传感器、读取传感器数据并处理相关的中断。
现在来到驱动层的函数编写,假设我们现在需要实现一个LED灯的初始化以及点亮和熄灭的函数。
在当前这一层,我们不应该出现区分GD32和STM32的条件编译语句了,这些是绝对要在底层就实现好的,我们接下来在驱动层使用的函数都是我们在硬件抽象层已经实现好了的函数。
void Drivers_LED_Init()
{
GPIOInitTypeDef LEDInitStruct;
LEDInitStruct.Mode = Output;
LEDInitStruct.Pin = Pin13;
HAL_GPIO_Init(GPIOC,LEDInitStruct);
}
实现到这里可能会有一点堵住,因为不同单片机固件库的名称并不同,所以在硬件驱动层中也要规划好用于初始化的结构体以及相应的宏定义:
// 根据不同的单片机定义引脚宏
#if defined(STM32)
#define Pin0 GPIO_Pin_0
#define Pin1 GPIO_Pin_1
#define Pin2 GPIO_Pin_2
#define Pin3 GPIO_Pin_3
#define Pin4 GPIO_Pin_4
#define Pin5 GPIO_Pin_5
#define Pin6 GPIO_Pin_6
#define Pin7 GPIO_Pin_7
#define Pin8 GPIO_Pin_8
#define Pin9 GPIO_Pin_9
#define Pin10 GPIO_Pin_10
#define Pin11 GPIO_Pin_11
#define Pin12 GPIO_Pin_12
#define Pin13 GPIO_Pin_13
#define Pin14 GPIO_Pin_14
#define Pin15 GPIO_Pin_15
// 定义输出模式
#define GPIO_MODE_INPUT GPIO_Mode_IN
#define GPIO_MODE_OUTPUT_PP GPIO_Mode_Out_PP
#define GPIO_MODE_OUTPUT_OD GPIO_Mode_Out_OD
#define GPIO_MODE_AF_PP GPIO_Mode_AF_PP
#define GPIO_MODE_AF_OD GPIO_Mode_AF_OD
#define GPIO_MODE_ANALOG GPIO_Mode_AIN
#elif defined(GD32)
#define Pin0 GPIO_PIN_0
#define Pin1 GPIO_PIN_1
#define Pin2 GPIO_PIN_2
#define Pin3 GPIO_PIN_3
#define Pin4 GPIO_PIN_4
#define Pin5 GPIO_PIN_5
#define Pin6 GPIO_PIN_6
#define Pin7 GPIO_PIN_7
#define Pin8 GPIO_PIN_8
#define Pin9 GPIO_PIN_9
#define Pin10 GPIO_PIN_10
#define Pin11 GPIO_PIN_11
#define Pin12 GPIO_PIN_12
#define Pin13 GPIO_PIN_13
#define Pin14 GPIO_PIN_14
#define Pin15 GPIO_PIN_15
// 定义输出模式
#define GPIO_MODE_INPUT GPIO_MODE_INPUT
#define GPIO_MODE_OUTPUT_PP GPIO_MODE_OUTPUT_PP
#define GPIO_MODE_OUTPUT_OD GPIO_MODE_OUTPUT_OD
#define GPIO_MODE_AF_PP GPIO_MODE_AF_PP
#define GPIO_MODE_AF_OD GPIO_MODE_AF_OD
#define GPIO_MODE_ANALOG GPIO_MODE_ANALOG
#else
#error "Unsupported microcontroller"
#endif
// GPIO 模式和上拉/下拉配置的定义
typedef enum {
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT_PP,
GPIO_MODE_OUTPUT_OD,
GPIO_MODE_AF_PP,
GPIO_MODE_AF_OD,
GPIO_MODE_ANALOG
} GPIO_ModeTypeDef;
typedef enum {
GPIO_NOPULL,
GPIO_PULLUP,
GPIO_PULLDOWN
} GPIO_PullTypeDef;
typedef struct {
uint16_t Pin; // 引脚编号
GPIO_ModeTypeDef Mode; // 模式
GPIO_PullTypeDef Pull; // 上拉/下拉配置
uint8_t Speed; // 输出速度
} GPIO_InitTypeDef;
现在重新看一下这个驱动层的初始化函数,不管是GD32平台还是STM32平台,对于GPIO引脚的模式似乎已经可以选定了,但是对于驱动层如果不填写GPIO参数的行为,为保证此框架的严谨及整个系统的稳定,硬件抽象层应该在未对参数进行选择的情况下,进行默认的配置
void Drivers_LED_Init()
{
GPIOInitTypeDef LEDInitStruct;
LEDInitStruct.Mode = GPIO_MODE_OUTPUT_PP;
LEDInitStruct.Pin = Pin13;
HAL_GPIO_Init(GPIOC,LEDInitStruct);
}
所以需要对 HAL_GPIO_Init 函数重新进行相应的功能完善
#ifdef STM32F103C8
#include "stm32f10x.h"
void HAL_GPIO_Init(uint32_t _GPIOx, GPIO_InitTypeDef *_GPIO_Init)
{
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = _GPIO_Init->Mode;
GPIO_InitStruct.GPIO_Pin = _GPIO_Init->Pin;
if(_GPIO_Init->Speed != 0XFF)
GPIO_InitStruct.GPIO_Speed = _GPIO_Init->Speed;
else
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(_GPIOx,&GPIO_InitStruct);
}
void HAL_GPIO_WRITE(uint32_t _GPIOx,uint32_t _Pin,uint32 _Value)
{
if(_Value)
GPIO_SetBits(_GPIOx,_Pin);
else
GPIO_ResetBits(_GPIOx,_Pin);
}
uint8_t HAL_GPIO_READ(uint32_t _GPIOx,uint32_t _Pin)
{
return GPIO_ReadInputDataBit(_GPIOx,_Pin);
}
#else ifdef GD32F103C8
#include "gd32f10x.h"
void HAL_GPIO_Init(uint32_t _GPIOx, GPIO_InitTypeDef *_GPIO_Init)
{
if(_GPIO_Init->Speed != 0XFF)
_GPIO_Init.GPIO_Speed = _GPIO_Init->Speed;
else
_GPIO_Init.GPIO_Speed = GPIO_Speed_50MHz;
if(_GPIO_Init->Pull!= 0XFF)
_GPIO_Init.Pull = _GPIO_Init->Pull;
else
_GPIO_Init.Pull = GPIO_NOPULL;
if(_GPIO_Init->Alternate != 0XFF)
_GPIO_Init.Alternate = _GPIO_Init->Pull;
else
_GPIO_Init.Alternate = gpio_af_0;
gpio_mode_set((uint32_t)_GPIOx, _GPIO_Init->Mode, _GPIO_Init->Pull, _GPIO_Init->Pin);
gpio_output_options_set((uint32_t)_GPIOx, _GPIO_Init->Speed, _GPIO_Init->Alternate, _GPIO_Init->Pin);
}
void HAL_GPIO_WRITE(uint32_t _GPIOx,uint32_t _Pin,uint32 _Value)
{
if(_Value)
gpio_bit_set(_GPIOx,_Pin);
else
gpio_bit_reset(_GPIOx, _Pin);
}
uint8_t HAL_GPIO_READ(uint32_t _GPIOx,uint32_t _Pin)
{
return gpio_input_bit_get(_GPIOx,_Pin);
}
#else
#error "Please Select Your Mcu!"
#endif
0x04 功能模块层函数说明
功能层位于应用层和硬件驱动层之间,负责提供具体的功能模块和服务。它将应用层的需求转化为对硬件的操作。
功能模块:实现特定的功能,如传感器数据处理、通信协议实现等。
接口管理:提供统一的接口供应用层调用,隐藏底层实现细节。
数据处理:对从硬件层获取的数据进行处理和转换,以便应用层使用。
在一个温度监测系统中,功能层可能负责从温度传感器读取数据、进行滤波处理,并将结果传递给应用层。
在一个通信系统中,功能层可能实现数据的编码、解码和协议处理。
这边就不写函数示例了,无非就是将自己之前的传感器模块的驱动更改为自己函数库实现的方式
0x05 应用层函数说明
应用层是嵌入式系统中用户直接交互的部分,负责实现具体的应用逻辑和功能。它通常包含用户界面、数据处理、控制逻辑等。
用户交互:处理用户输入(如按钮、触摸屏等)并提供反馈(如显示信息、声音提示等)。
业务逻辑:实现特定的功能需求,如数据采集、处理和存储,控制设备的行为等。
任务调度:在多任务环境中,管理不同任务的执行顺序和优先级。
如:在一个智能家居系统中,应用层可能负责控制灯光、温度和安全系统的逻辑。
由于我们在底层做了很好的封装,所以现在应用层的函数就非常信手拈来并且非常简洁了,在绘制好程序流程图之后,写代码就只需要将其翻译成代码即可了。
0x06 驱动安装脚本
有的时候某些设备驱动不一定会使用到,我们可以编写一个可视化界面去进行相关设备的驱动安装。可以先了解一下Keil和IAR工程的原理,它们都是使用的XML的格式去进行工程的配置,直接选中Keil或者IAR的工程文件,选择用文本编辑器来打开。
用文本编辑器打开后可以看到一个用<OutputName>和<OutputName>括起来的标签,这个是输出的文件名字,例如这个标签中填写的是HelloWorld,那么最终输出的就是HelloWorld.hex,除此之外还有为工程添加的.c文件以及工程所指明的.h文件路径、工程中所预先设置的全局宏定义等,都会在工程文件中有所体现。
摸索完原理后选择工程文件即可,会自动识别路径和工程类型,选择自己需要的驱动文件去安装到所选择的工程即可。
后续完善了功能会将框架工程和驱动安装软件的代码都开放出来。先看个如下的演示图。