为什么使用设计模式
在软件工程中,设计模式(design pattern)是解决软件设计中常见问题的通用可重复解决方案。 设计模式不是可以直接转换为代码的最终设计。 它是关于如何解决可以在许多不同情况下使用的问题的描述或模板。
设计模式可以通过提供经过测试的,经过验证的开发范例来加快开发过程。 有效的软件设计需要考虑直到实施的后期才变得可见的问题。 重用设计模式有助于防止可能引起重大问题的细微问题,并提高熟悉模式的开发人员和架构师的代码可读性。
通常,人们只了解如何将某些软件设计技术应用于某些特定的问题。 这些技术很难应用于更广泛的问题。 而设计模式提供了一种更通用的解决方案,并且易于移植和使用。
此外,使用设计模式允许开发人员使用众所周知的,易于理解的名称进行交流,减小了沟通成本。常见的设计模式也可以随着时间的流逝而得到改进,使其比开发人员的临时设计更为可靠。
工程实例
设计思路
设计目的是要实现一个通用的 HAL,适用于各种不同的单板(board)项目,以及不同的芯片(platform)。各种 boards 和 platforms 具有类似的属性和功能。因此这里参考了抽象工厂设计模式,设计如下的包含 HAL 的工程。整个工程的文件结构的组成:
├─ include
│ └─ hal
├─ main
├─ app
├─ hal
├─ board
│ └─ hal
└─ platform
├─ hal
├─ cpu
└─ driver
工程主要由四部分组成:
- app:整个工程的上层 application 应用软件;
- hal:上下层连接的硬件抽象层 HAL;
- board:针对不同的硬件单板的定制化配置;
- platform:针对不同的平台芯片的底层代码,对于所有的 board 都适用。例如,可以包含STM系列,TI的TMS,ADI的SHARC,NXP的S32等等不同的MCU、DSP芯片的驱动层代码。
其中,app 只需要调用 HAL 的 API 即可实现对硬件的使用。board 包含对单板的各种位置和初始化代码。platform 包含底层基础代码,操作硬件寄存器,适用于各种 board 单板。
HAL 相关的代码由于使用了设计模式,所以使用 C++实现,其他代码均使用 C 实现。
使用这种设计思路,使 HAL 这部分代码不需要被频繁修改,用户在使用的时候,只需要按照HAL所规定的API,添加对应 platform 类型(比如C2000)和 board 类型(比如 demodemo)的实现代码即可。这样,不同层之间的代码耦合度可以大大降低,可读性、可维护性也会有很大提高。
这种设计层次清晰,调用简单,但是不足之处就是对于内存有一些要求。因为使用了很多动态成员变量,和一些C++特性,对于RAM有一些要求,所以对于一些RAM空间有限的小型MCU/SOC并不适用。
对于内存有限的MCU,我设计了一套相似的 HAL lite 版本,全部由C语言实现,并且使用静态成员注册modules,避免程序半途因为RAM不够而崩溃。由于整体思路类似,就不再赘述。
HAL 的初始化与构建
我们假设一个简单的使用情景:
- 一块名字叫做 demodemo 的单板;
- 使用 TI 的 C2000 芯片;
- 需要使用这个芯片的 ADC 的功能。
main函数中通过调用 hal_platform_init(); 实现对所对应 platform(也就是demodemo + C2000)的初始化,文件位置 /board/hal/demodemo_c2000_platform.cpp。部分代码:
bool DemodemoPlatform::Init()
{
bool error = false;
error = bsp_init();
for (std::list<Driver *>::iterator it = mpDriverList.begin();
it != mpDriverList.end();
++it)
{
error = (*it)->Init();
}
return error;
}
uint32_t hal_platform_init(void)
{
ErrorCode_t status = NO_ERROR;
Platform::PlatformBuilder *pPlatformBuilder = nullptr;
/*construct the platform*/
pPlatformBuilder = new DemodemoMcuPlatform::DemodemoMcuPlatformBuilder();
if (nullptr != pPlatformBuilder)
{
gpPlatform = pPlatformBuilder->Construct();
/* Initialize the specific platform*/
gpPlatform->Init();
}
else
{
status = ERROR_NO_MEMORY;
}
return status;
}
这里使用了抽象工厂模式,可以针对不同的 platform 生成相应的实例。其中,首先通过:
Platform *DemodemoMcuPlatform::DemodemoMcuPlatformBuilder::Construct()
{
DemodemoMcuPlatform *pPlatform = nullptr;
pPlatform = new DemodemoMcuPlatform(this);
HardwareFactory *pHardwarefactory = nullptr;
pHardwarefactory = new DemodemoMcuHardwareFactory();
Soc *pSoc = pHardwarefactory->CreateSoc();
pPlatform->AddHardware(pSoc, pPlatform);
#if HAVE_ADC
Adc *pAdc = pHardwarefactory->CreateAdc();
pPlatform->AddHardware(pAdc, pPlatform);
#endif /* HAVE_ADC */
delete pHardwarefactory;
return pPlatform;
}
生成 demodemo 工厂实例,并通过CreateXxx(),AddHardware(),注册所需要使用的外设。
然后使用 gpPlatform->Init() 初始化 platform:
- 单板初始化:调用bsp_init() 做一些 demodemo单板一些必要的初始化,比如电源、时钟、中断、全局变量等的初始化。
- driver 初始化:通过
for (std::list<Driver *>::iterator it = mpDriverList.begin();
it != mpDriverList.end();
++it)
{
error = (*it)->Init();
}
实现对这个 platform 注册的所有外设的初始化,比如timer,UART,CAN,I2C等,这个例子只使用了一个ADC作参考。
这样,所有的与 HAL 有关的外设都创建了一个适用于 demodemo 单板的实例,当我们需要调相应的 HAL 的时候,可以在对应的 hal_xxx.cpp 中通过:
static inline Adc* getAdcInstance()
{
static Adc* spAdc = nullptr;
if (nullptr == spAdc)
{
Platform* pPlatform = static_cast<Platform*>(hal_platform_getInstance());
Driver* pDriver = const_cast<Driver*>(pPlatform->GetHardware("Adc"));
spAdc = static_cast<Adc*>(pDriver);
}
return spAdc;
}
得到所注册的外设(比如ADC)。然后再对外设模块具体操作即可:
uint32_t hal_adc_init(void)
{
Adc* pAdc = getAdcInstance();
if (nullptr != pAdc)
{
pAdc->Init();
return NO_ERROR;
}
else
{
return ERROR_NO_MEMORY;
}
}
Driver 如何被 App 调用
根据设计,app 在使用的过程中不需要知道底层驱动如何实现,而是可以调用 HAL API 实现对相应 driver 的使用。例如,可以这样使用 hal_adc_read() 读取 ADC 的数值:
/**
* app.c
*/
void app_run(void)
{
/* read ADC channel 1 value */
printf("APP: Task triggered \n");
hal_adc_read(1); // read ADC channel 1 value
}
这个 API 在文件 hal_adc.h 中 (位置 /include/hal),这个API是面向所有HAL的使用者,因此放在 /include/ 这个文件夹中。
对应文件 hal_adc.cpp 实现了具体操作,文件位置 /hal/src/hal_adc.cpp,并不允许 HAL API 使用者修改。
/**
* hal_adc.cpp
*/
#include "../../include/hal/hal_adc.h"
#include <stdio.h>
#include "../../include/hal/hal_platform.h"
#include "../../hal/include/platform_interface.h"
#if HAVE_ADC
static inline Adc* getAdcInstance()
{
static Adc* spAdc = nullptr;
if (nullptr == spAdc)
{
Platform* pPlatform = static_cast<Platform*>(hal_platform_getInstance());
Driver* pDriver = const_cast<Driver*>(pPlatform->GetHardware("Adc"));
spAdc = static_cast<Adc*>(pDriver);
}
return spAdc;
}
uint32_t hal_adc_init(void)
{
Adc* pAdc = getAdcInstance();
if (nullptr != pAdc)
{
pAdc->Init();
return NO_ERROR;
}
else
{
return ERROR_NO_MEMORY;
}
}
uint16_t hal_adc_read(uint8_t Channel)
{
Adc* pAdc = getAdcInstance();
if (NULL != pAdc)
{
printf("HAL: ADC read triggered \n");
return pAdc->Read(Channel);
}
return 0;
}
#endif /* HAVE_ADC */
HAL 通过调用 driver_interface 中定义的不同 CPU 的 ADC read 函数,实现对底层 driver 的调用,位置 /hal/include/driver_interface.h。
/**
* driver_interface.h
*/
#ifndef HAL_INCLUDE_DRIVER_INTERFACE_H_
#define HAL_INCLUDE_DRIVER_INTERFACE_H_
/* INCLUDE FILES */
#include <string>
#include <stdint.h>
#include "../../board/include/board.h"
#if HAVE_ADC
//#include "../../include/hal/hal_adc.h"
#endif
/* GLOBAL FUNCTIONS */
/**
* @brief Base class for all HAL drivers.
*
*/
class Driver
{
public:
virtual bool Init() = 0;
virtual ~Driver() { };
virtual const std::string &GetName() const
{
return mName;
};
protected:
Driver(): mName("Driver") {};
std::string mName;
};
/**
* @brief SoC chip type abstract class, derived from Driver.
*
*/
class Soc : public Driver
{
public:
virtual bool Init() = 0;
virtual ~Soc() { };
protected:
Soc() { };
};
#if HAVE_ADC
/**
* @brief ADC driver abstract class, derived from Driver.
*
*/
class Adc : public Driver
{
public:
virtual bool Init() = 0;
virtual uint16_t Read(uint8_t channel) = 0;
virtual ~Adc() { };
protected:
Adc() { };
};
#endif /* HAVE_ADC */
#endif /* HAL_INCLUDE_DRIVER_INTERFACE_H_ */
对上述HAL的driver 接口,可以用以下方式实现。根据MCU种类不同,用户可以定义自己的实现内容。例如,对于C2000 ADC 的 source 代码,可以这样实现,位置 /platform/hal/c2000_drivers.cpp:
/**
* c2000_drivers.cpp
*/
/* INCLUDE FILES */
#include "c2000_drivers.h"
#if HAVE_ADC
#include "../../platform/driver/c2000_driver_adc.h"
#endif
/* MODULE FUNCTIONS */
/**
* S32K SOC class implementation
*/
C2000Soc::C2000Soc()
{
mName.assign("Soc");
}
bool C2000Soc::Init()
{
bool error = false;
//error = s32k_driver_soc_init();
return error;
}
C2000Soc::~C2000Soc()
{
}
#if HAVE_ADC
/**
* S32K ADC class implementation
*/
C2000Adc::C2000Adc()
{
mName.assign("Adc");
}
bool C2000Adc::Init()
{
c2000_driver_adc_init();
return true;
}
uint16_t C2000Adc::Read(uint8_t channel)
{
return c2000_driver_adc_read(channel);
}
C2000Adc::~C2000Adc()
{
}
#endif /* HAVE_ADC */
这样,一个衔接上层的 app 与 底层的 ADC driver 的 HAL 就实现了。实例中只给出了 ADC 的实现,实际应用中可以根据需要添加 timer,UART,DAI等外设和服务。
执行 main.c 主函数,读取三次C2000 ADC 的数值:
int main(int argc, char **argv)
{
printf("HAL DEMO STARTS...\n");
printf("\n***Stage 1: INIT All***\n");
hal_platform_init();
app_init();
printf("\n***Stage 2: START All***\n");
hal_platform_start();
app_start();
printf("\n***Stage 3: RUN Application***\n");
while (Global.system_running)
{
app_run();
if (++Global.time > 2) break;
}
return 0;
}
我们可以得到结果:
总结
这个 HAL 的设计思路就是通过把整个工程的代码分为 app, board, hal, platform 这4层,实现 app->hal->driver 的调用,并且易于移植和调用。
优点:
- 更好的可读性:更多抽象,更清楚的逻辑;
- 更好的可维护性:每部分的代码可以单独维护,出现问题根据错误码方便查找追溯;
- 更好的可移植性:对于不同的单板,只需要修改 board 层代码有针对地修改即可,其他层代码可以完全共用;
- 更标准化:减少不同工程师的冲突。
缺点:
- 文件数量多,对内存的动态操作较多,会降低一定的稳定性;
- 代码量更大,需要的flash RAM空间会增多;
- 执行速度更慢:因为要调用的函数嵌套更多,入栈出栈的次数会更多;
- 对公共代码的修改要谨慎,保证其可靠性,避免影响其他 platform。
to be continued…
参考
https://en.wikipedia.org/wiki/Software_design_pattern
Design Patterns and Refactoring