C, "extern" type of variables in hardware abstraction layer

Viewed 134

I am working on a Hardware Abstraction Layer. The aim of this HAL is to easily switch between the drivers for linux and the drivers for an MCU.

I am working on the SPI interface. Below is the signature of the function of the HAL that "open" the interface of SPI.

hal/spi.h

spi_handle_t spi_open(spi_port_t channel, spi_config_t config);

spi_port_t :

  • On Linux, it is an elementary type : uint32_t
  • On the MCU, it is a struct.

spi_config_t :

  • On both Linux and the MCU it is a struct but with different fields.

So in mcu/spi.c I have at some point this :

typedef spiBASE_t spi_channel_t;
typedef spiDAT1_t spi_config_t;

spi_handle_t spi_open(spi_channel_t channel, spi_config_t config) {
.
.
.
}

And for linux/spi.c :

typedef uint32_t spi_channel_t;
typedef ChannelConfig_t spi_config_t;

spi_handle_t spi_open(spi_channel_t channel, spi_config_t config) {
.
.
.
}

Now the problem is in hal/spi.h, I need to define what is spi_channel_t and spi_config_t.

Is there a way to make something like (I know it is impossible with extern, but for explanation matters...) :

extern spi_channel_t;
extern spi_config_t;

This would say to the compiler : "Ok these two types are not defined in the header file, still you can find their storage size on one of the files I passed to the toolchain".

2 Answers

What you seem to be looking for is the trick called opaque type. It's a way to use forward declaration of structs in order to achieve private encapsulation and polymorphism in C. It is commonly used for professionally-written embedded systems drivers in particular and can be implemented like this:

hal/spi.h

// forward declaration of a struct, with typedef and struct tag:
typedef struct spi_handle_t spi_handle_t; 

// Require the caller to declare a spi_handle_t* pointer, not an object:
spi_handle_t* spi_init (...); 

mcu/spi.c

struct spi_handle_t
{
  // whatever you need here - these are now 100% private members
};

spi_handle_t* spi_init (...)
{
  spi_handle* result = address_of_some_static_memory_pool;

  /* init struct members here */

  return result;  
}

linux/spi.c

struct spi_handle_t
{
  uint32_t something;
  // whatever you need here - these are now 100% private members
};

spi_handle_t* spi_init (...)
{
  spi_handle* result = malloc(sizeof *result); // malloc is fine to use in Linux

  /* init struct members here */

  return result;  
}

Now the caller will have to pass along the spi_handle* to other functions in the driver. This isn't just handy for OO design, but it also makes it possible to run the same code with multiple instances. For example if you have 2 different SPI hardware peripherals on the MCU and want to use them differently, but with the same driver code.

The typical way of doing this would be to use the preprocessor in hal/spi.h:

#if defined(HAL_LINUX)
typedef uint32_t spi_port_t; /* int in linux */
#elif defined(HAL_MCU)
typedef struct {
    /* your struct in bare metal app */
} spi_port_t;
#else
#error "Platform not defined!"
#endif

You could also put the different types into different headers like hal/spi_linux.h and hal/spi_mcu.h and conditionally include one of them in hal/spi.h.

Alternatively, you could just take pointers and cast them to their actual type. This would be less safe because you would have to check the platform at runtime and decide the type behind the pointer, but this decision depends on many other things.

Related