浅谈HAL设计(2)- 设计模式与实例

为什么使用设计模式

在软件工程中,设计模式(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:

  1. 单板初始化:调用bsp_init() 做一些 demodemo单板一些必要的初始化,比如电源、时钟、中断、全局变量等的初始化。
  2. 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;
}

我们可以得到结果:

image.png

总结

这个 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