如何在嵌入式平台中管理不同 Pin-Out 板的代码以实现更好的 HAL 管理?

How to I manage code for different Pin-Out boards in an embedded platform for better HAL management?

这里的问题是,如果您的项目很小,#if defined#define#elif defined 链会变得冗长、乏味且容易失败。在编译时必须有更好的方法来完成这个。

目前我 运行 我的代码通过 #if / #elif 链来定义 GPIO 的引脚和变量名称,没有问题这里有一个简化的上下文这看起来像什么,甚至添加一个头文件来排除 HAL。

所以主要看起来像:

#include “my_path/hal.h”

#define some_function ()
#if defined(BOARD_ID1)
      Do_something KEYS_GPIO_REG_UP
#elif defined(BOARD_ID1)
    Do_something_different KEYS_GPIO_REG_UP
#endif

main()
..

这是一个将GPIO分配给Variable的hal文件。

// Define hal.h 
    #if defined(BOARD_ID1)
    #define KEYS_GPIO_REG_UP            GPIOD->IDR
    #define BUTTON_GPIO_PIN_UP            GPIO_Pin_1  // PD.01
    #elif defined(BOARD_ID2)
    #define KEYS_GPIO_REG_UP            GPIOB->IDR
    #define BUTTON_GPIO_PIN_UP            GPIO_Pin_2  // PB.01

我想了解的是(请指出最佳实践文章和/或示例代码的方向)如何在编译时提供密钥 ID 来定义板: set BOARDID ${BOARDID} 我已经这样做了,而是做同样的事情来定义哪个 hal.h 为每种板类型创建一个 BOARID.h 文件以维护要使用的版本。我不确定这是否可行,或者一个选项是在代码中提供一个脚本变量,它自己和动态必须在 make 脚本中更改 ìnclude ${BOARDID}.h 作为一种可能性。

这不是 HAL,它只是一个 collection 编译器开关 - 这通常是最糟糕的选择之一,因为它们使代码变得混乱。

一个 HAL 是 board_x.c 中的一组完整函数,也是 board_y.c 中的另一组完整函数。每个 .c 文件中的函数具有相同的名称但执行不同的操作。两个 .c 文件都包含相同的 header API 和函数声明 - 这是实际的 HAL,也是调用应用程序知道和关心的唯一文件。

然后为单独的板创建单独的项目,或者通过外部版本控制来处理它。在一种情况下,您 link 在 board_x.c 中,而在另一种情况下,您在 board_y.c.

中 link

我真正想要的是 "manage" if-else-if 长链或为不同板设置属性的方法。我所做的是在我的原始头文件中嵌入几个 hea 文件:

所以现在我的文件看起来像这样:

// Define hal.h 
    #if defined(BOARD_ID1)
    #include "board_id01.h"
    #elif defined(BOARD_ID2)
    #include "board_id02.h"

在那里我可以拥有尽可能多的独立文件并更轻松地管理目标板,即。

// Header File board_id01.h 

#define KEYS_GPIO_REG_UP            GPIOD->IDR
#define BUTTON_GPIO_PIN_UP            GPIO_Pin_1  // PD.01
#define ....

这使我可以一次专注于 HAL 的一个板...然后只处理我的代码。

我希望这可以帮助任何人以一种简单的方式来管理您的多目标嵌入式开发和那些分享您的评论的人,更好地理解我现在看到的问题,但我没有很好地构建它。

您的解决方案虽然正确,但不是一个好的解决方案。我知道它可能看起来不错,因为您正在非常愉快地使用它,但这最终会导致大量宏定义、每个板具有不同名称/参数的函数,而且最有问题的一个,所有应用程序都会知道关于通过包含的实际硬件。甚至更糟。对任何板的任何更改都将强制重新编译所有不同板的所有项目(因为所有项目都包含所有板,尽管没有使用它)。

正确的方法是让一个 .h 文件共享 API。这是一组定义行为的函数,而不是硬件。类似 void setUartSpeed(uart_t *uart, size_t speed) 的函数,其中 uart_t 是一个不透明的指针,并且为每个板定义不同。是的,对 API 的任何更改都会导致所有板的编译错误,但这很好,因为您会注意到它并且您只需更改板的 .c 文件,而不是所有项目文件。因此,更正确的方法是@Lundin 回答的方法。


根据您的意见,如何避免这样的实施? https://github.com/opentx/opentx/blob/2.3/radio/src/targets/taranis/hal.h

只考虑行为,而不是硬件:

即谈论宏KEYS_GPIO_REG_MENU:你应该完全删除它,因为它是一个硬件实现细节,对于应用程序的其余部分来说不是必需的。取而代之的是,您应该添加 GPIOStruct* getPinout() setHigh(GPIOStruct *)setLow(GPIOStruct *) 等函数,其中 GPIOStruct 是在 .c 文件中定义的不透明指针。 所以,在每个板上你都会有一个 .c 文件定义:

  • getPinout 函数,它将 return 一个包含所有可用引脚的数组。
  • setHigh 和 setLow 函数,这会将引出线设置为逻辑电平。也许,对于相同的 uC 架构,这个函数可以被放置为通用的,因为它取决于 uC 而不是在板上。

因此,特别是在您的示例中,您将拥有:

//hal.h
typedef struct GPIOStruct GPIOStruct;
GPIOStruct* getPinout();
void setHigh(GPIOStruct* pin);
void setLow(GPIOStruct* pin);

//board PCBX9E.c
struct GPIOStruct {
    void *idr; //Set to the correct type, probably volatile uint8_t
    int idx;
};
static const GPIOStruct gpio_reg_menu={.idr=&GPIOD->IDR, .idx=7};
static const GPIOStruct gpio_reg_exit={.idr=&GPIOD->IDR, .idx=2};
//And so on...

GPIOStruct* getPinout()
{
    GPIOStruct gpios[]={
        &gpio_reg_menu,
        &gpio_reg_exit,
        //And so on...
    }
}


//board PCBXLITE.c
//Note: this structs are the same. It could be moved to a file called taranis_gpio.h, since the architecture of the uC is the same for both boards, 
//among with the functions to setHigh/setLow a pin.
struct GPIOStruct {
    void *idr; //Set to the correct type, probably volatile uint8_t
    int idx;
};

//But the pinout is different, so this should be kept in each board file.
static const GPIOStruct gpio_reg_menu={.idr=&GPIOE->IDR, .idx=8};
static const GPIOStruct gpio_reg_exit={.idr=&GPIOE->IDR, .idx=7};
//And so on...

GPIOStruct* getPinout()
{
    GPIOStruct gpios[]={
        &gpio_reg_menu,
        &gpio_reg_exit,
        //And so on...
    }
}

并且在您的项目中,您只需添加一个或另一个 .c 文件,具体取决于您要编译到的板。


希望不要从这里淹没你...

可以在以下论文中找到处理硬件时功能的深入分离: https://atomicobject.com/uploads/archive/files/EIT2006EmbeddedTDD.pdf

总结一下,他们为每个硬件模块创建了三个文件:

  • Hardware:这个文件包含了硬件访问,寄存器等等。这主要是我们要说的。

  • 模型:该文件将包含硬件应该做什么的高级表示。 IE。它将包含如下函数:openMenuisMenuOpened、等等...

  • 导体:此文件包含胶水。它监控和更新硬件,并监控和更新另一端的模型。

通过这种分离,将硬件从一种实现更改为另一种实现非常容易,只需更改 .c


在示例中,所有这些文件都是三元组的 hardware 元素。

你可以像我展示的那样保留它,直接处理 GPIO,或者你可以遵循这个方案并通过提供 openMenucloseMenu 等函数对其进行更多抽象.这些函数将是三元组的 model 元素。最后,遍历 GPIO 的主要代码是 conductor 元素。