jemo
jemo

Reputation: 1

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

The issue here is, if you project is small, #if defined, #define, #elif defined chains get long, tedious, and prone to failure. There’s got to be a better way to accomplish this at compile.

Currently I’m running my code through the #if / #elif chains to define pins and variable names to GPIOs, there is not issue there here is a simplified context of what this looks like, even adding a header file to exclude the HAL.

So the main would look like:

#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()
..

Here is a hal file to assign GPIO to Variable.

// 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

What I would like to understand is (pls just point me in the direction of a best practice article and or sample code) how can at compile, provide a key id to define the board with a: set BOARDID ${BOARDID} Which I already do, but rather do the same to define which hal.h create a BOARID.h file for every board type to maintain the version to use. I’m not sure if this is possible, or an option is to provide a scripting variable within the code it self and dynamical having to change the ìnclude ${BOARDID}.h as a possibility in the make script.

Upvotes: 0

Views: 728

Answers (3)

LoPiTaL
LoPiTaL

Reputation: 2595

Your solution, although correct, is not a good solution. I know it may seem good, since you are working with it completely happy, but this will end up with a lot of macro definitions, functions with different names / parameters for each board, and, the most problematic one, all the application will know about the actual hardware via the includes. Even worse. any change to any board will force a recompilation for all the projects of all the different boards (since all the projects have the include of all the boards, despite not using it).

The correct aproach is to have a single .hfile with a shared API. This is, a single set of functions which define behaviour, not hardware. Functions like void setUartSpeed(uart_t *uart, size_t speed), where uart_t is an opaque pointer, and defined differently for each board. Yes, any change to the API will cause a compilation error for all boards, but this is fine, since you will notice it and you have to change only the boards .c files, not all the project files. So, the more correct approach would be the one @Lundin answered.


Following your comments, how to avoid such an implementation? https://github.com/opentx/opentx/blob/2.3/radio/src/targets/taranis/hal.h

Just think about behaviours, not about hardware:

I.e. talking about the macro KEYS_GPIO_REG_MENU: you should completely remove it, since it is a hardware implementation detail, not necessary for the rest of the application. In its place, you should add functions like GPIOStruct* getPinout() setHigh(GPIOStruct *), setLow(GPIOStruct *) and so on, where GPIOStruct is an opaque pointer defined in the .c file. So, on each board you will have a .c file defining:

  • The getPinout function, which will return an array with all the available pins.
  • setHigh and setLow functions, which will set the pinout to a logical level. Maybe, this functions can be placed as common for same uC architecture, since it depends on uC and not on the board.

So particularly in your example you would have:

//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...
    }
}

And in your project, you just add one or the other .c file, depending on the board you are compiling to.


Hope not to overwhelm you from here...

An in-depth separation of functionality when dealing with hardware can be found in the following paper: https://atomicobject.com/uploads/archive/files/EIT2006EmbeddedTDD.pdf

Sumarising, they create three files per hardware module:

  • Hardware: this file contains the hardware access, registers and so. This is mainly what we are talking about.

  • Model: this file would contain a high-level representation of what the hardware is suposed to do. I.e. it would contain functions like: openMenu, isMenuOpened, etc...

  • Conductor: this file contains the glue. It monitors and updates the hardware, and monitors and updates the model with the changes on the other side.

With this separation, it is really easy to change the hardware from one implementation to another, just changing the .c


In the example, all those files would be the hardware element of the triad.

You can leave it as I show you, dealing directly with GPIOs, or you can just follow this scheme and abstract it a little bit more by providing functions like openMenu, closeMenu and so. These functions would be the model element of the triad. Finally, your main code looping over the GPIOs would be the conductor element.

Upvotes: 1

jemo
jemo

Reputation: 1

What I was looking for really was a way to "manage" the if-else-if long chain or setting attributes for different boards. What I did was embed several hea files on my original header file:

So now my files looks like this:

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

Where I can then have as many isolated files and manage the target board easier, ie.

// Header File board_id01.h 

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

This allows me to focus on a single board of the HAL at a time... then just work on my code.

I hope this helps anyone out with a simple way to manage your multi-target embedded development and those that shared your comments, better understand my problem as I look at it now, and I did not frame it well.

Upvotes: 0

Lundin
Lundin

Reputation: 214300

This isn't a HAL, it's just a collection of compiler switches - which is often one of the worst options since they clutter down the code a lot.

A HAL would be a complete set of functions in board_x.c, and another complete set of functions in board_y.c. The functions in each .c file having the same names but doing different things. Both .c files include the same header API with the function declarations - which is the actual HAL and the only file the calling application knows and cares about.

Then create separate projects for separate boards, or handle it through external version control. In one case you link in board_x.c and in another case you link in board_y.c.

Upvotes: 2

Related Questions