Configuration Table Pattern

« Back to Glossary Index

Description


Store configuration and initialization information inside of a table, and pass the table to an initialization routine that iterates over the table entries.

Problem

When attempting to create a portable and reusable software design, you need to decouple your application code from the underlying platform – the OS, the hardware, and any other non-portable constructs. Abstraction layers (OSAL, HAL) are typically used to create a decoupling point in the application. You can write drivers and modules that interact only with the provided abstractions, and changes below the abstractions will not require corresponding changes in the application layer.

One challenge in this scheme is handling initialization and configuration. Initialization requirements and available settings vary widely across devices, processors, and OSes. Attempting to create a general abstraction for these settings is a fool’s errand, as you will end up with too many potential options; only a limited set will apply to any given device, rendering the abstraction useless. Hiding this information within the driver or HAL implementation is also risky: each application will need different settings, meaning that you need to change the HAL for each application. How can you properly handle configuration and initialization while still benefiting from abstraction?

A separate problem from the one stated above: configuration and initialization information, such as thread settings and thread creation calls, are often scattered throughout an application. How can you group these settings together in a single, easy-to-find location?

Solution

One way to address this problem is by defining and maintaining configuration tables for each of the various peripheral devices, OS types, and other non-portable constructs. All relevant information required for configuration and initialization is stored in these tables. They are specified at the application level, allowing each application (or the various configurations) to initialize the underlying hardware system according to its needs. Drivers and abstraction layers can remain reusable and do not need to be modified to support different configuration settings.

Specifying configurable settings in this way allows you to maintain a generic interface. There is no need to craft an initialization interface that supports all possible configuration settings. You can create a generic initialization function, such as OS_thread_init(const OS_thread_config_t* config). While the exact definition of the configuration structure may change from one RTOS to another, the initialization interface will remain the same, and the definitions for each RTOS will be consistent across different applications. This achieves a suitable middle ground for designing for change while still supporting implementation-specific configuration options.

Implementation

To implement this pattern, proceed through the following steps:

  1. Identify the Configuration Parameters
  2. Create the Configuration Structure and Supporting Types
  3. Populate the Table
  4. Create the Initialization Routine

Identify the Configuration Parameters

First, you need to identify the common configuration parameters that apply for your processor (family). This information can be found by reviewing the datasheet or the chip vendor drivers. Find what registers exist and what the various fields in the registers mean. Looking at different code examples can also be helpful for figuring out what typical configuration patterns are.

As you review this information, make a list of the various configurable parameters as well as possible values for that parameter..

Note

For the purposes of abstraction, especially if supporting multiple processors in a given family, you should focus primarily on common settings that are available. However, configuration is something that typically varies from one processor to another, so do not be surprised if you need to use different configuration table definitions for different processor families.

For example, here is a common set of configurable parameters for a timer:

  • Timer mode
  • Enabled/disabled status
  • Clock source
  • Clock pre-scaler
  • Count direction (up/down)
  • Target count / interval / period
  • Periodic / one-shot

Here is a common set of configurable parameters for GPIO:

  • Direction (input/output/tri-state)
  • Pull-up/down setting
  • Drive options (normal, hi-drive)

You will also want to determine how the different parameters will be “mapped”. Some drivers will be mapped to a specific instance, such as TIMER0 and TIMER1. Others, like DMA, may have a combination of a “device” (DMA0, DMA1) and “channel” (1..8). GPIO will commonly be mapped to a “port” (GPIOA, GPIOB) and “pin number”

Create the Configuration Structure and Supporting Types

Once you have identified the various parameters and options, you are ready to create a structure that contains all the configurable settings.

Beningo offers the following timer configuration structure example in Reusable Firmware Development:

typedef struct {
	uint32_t TimerChannel; /**< Name of timer */
	uint32_t TimerEnable; /**< Timer Enable State */
	uint32_t TimerMode; /**< Counter Mode Seettings */
	uint32_t ClockSource; /**< Defines the clock source */
	uint32_t ClockMode; /**< Clock Mode */
	uint32_t ISREnable; /**< ISR Enable State */
	uint32_t Interval; /**< Interval in microseconds */
} TimerConfig_t;

While Beningo shows plain uint32_t values above, we prefer to use enumerations or vendor-supplied parameters for configuration table values, making them more readable and easier to reference. For Beningo’s GPIO configuration structure example, he uses this approach:

/**
* Defines the digital input/output configuration table’s elements that are used
* by Dio_Init to configure the Dio peripheral.
*/
typedef struct
{
/* TODO: Add additional members for the MCU peripheral */
	DioChannel_t Channel;          /**< The I/O pin        */
	DioResistor_t Resistor;         /**< ENABLED or DISABLED     */
	DioDirection_t Direction;    /**< OUTPUT or INPUT                */
	DioPinState_t Data;               /**<HIGH or LOW          */
	DioMode_t Function;            /**< Mux Function  - Dio_Peri_Select*/
} DioConfig_t;

Example definitions for the enumerations referenced above are shown below.

/**
* Defines the possible states for a digital output pin.
*/
typedef enum
{
	DIO_LOW,                                  /** Defines digital state ground */
	DIO_HIGH,                                 /** Defines digital state power */
	DIO_PIN_STATE_MAX                        /** Defines the maximum digital state */
} DioPinState_t;

/**
* Defines an enumerated list of all the channels (pins) on the MCU
* device. The last element is used to specify the maximum number of
* enumerated labels.
*/
typedef enum
{
	/* TODO: Populate this list based on available MCU pins */
	FCPU_HB,                   /**< PORT1_0 */
	PORT1_1,                   /**< PORT1_1 */
	PORT1_2,                   /**< PORT1_2 */
	PORT1_3,                   /**< PORT1_3 */
	UHF_SEL,                   /**< PORT1_4 */
	PORT1_5,                   /**< PORT1_5 */
	PORT1_6,                   /**< PORT1_6 */
	PORT1_7,                   /**< PORT1_7 */
	DIO_MAX_PIN_NUMBER    /**< MAX CHANNELS */    
} DioChannel_t;
Note

If the chip vendor’s supplied definitions will work for table values, you can certainly use those. You can also use your custom enumeration values as the index in a look-up table kept in the driver implementation (or other file).

These enumeration and structure definitions are best placed within a separate header file instead of in the primary driver abstraction header. For example, if you have a timer.h header which provides generic timer functions, the configuration-related definition should go into timer_config.h. There are a few different reasons for this:

  1. The base abstractions (e.g., gpio_set_output, gpio_read) should be usable on any processor and are unrelated to the device-specific configuration parameters (and, potentially, vendor-supplied definitions). Excluding timer configuration information from this header ensures that there is no accidental coupling to platform-specific details in code that would otherwise be portable.
  2. Tightly coupled application code that sets up the configuration information for the system’s needs can intentionally include timer_config.h.
  3. The driver implementation can include timer_config.h in order to access the full definition of the structure type.
  4. Depending on how the driver is structured and how the various configurable parameter values are supplied, you may be able to maintain a common driver definition while supporting multiple processors by changing the configuration definitions. You can maintain different timer_config.h definitions for different processors, selecting the right one for use by changing the include directories used to build the project.

Populate the Table

Once your definitions are in place, you need to create a configuration table and populate it with configuration entries for the various devices in the system.

Beningo provides an example timer configuration table:

static const TmrConfig_t TmrConfig[] = 
{
	// Timer	Timer	Timer	Clock	Clock Mode	Clock		Interrupt	Interrupt	Timer
	// Name		Enable	Mode	Source	Selection	Prescaler	Enable		Priority	Interval (us)
	{TMR0,	ENABLED, UP_COUNT, FLL_PLL, MODULE_CLK, TMR_DIV_1, DISABLED, 3, 100},
	{TMR1,	DISABLED,	UP_COUNT,	NOT_APPLICABLE, STOP, TMR_DIV_1, DISABLED, 0, 0},
	{TMR2,	ENABLED,	UP_COUNT,	FLL_PLL,	MODULE_CLK,	TMR_DIV_1, DISABLED, 3, 100},
};

The configuration table should be placed either in its own file (e.g., timer_config.c) or in a file in the application that is designated for tight coupling and handling initialization/configuration.

Some notes on the declaration:

  1. As long as you do not want the table to be changeable during operation, it should be declared const.
  2. You can declare it static to limit visibility to the current file.
  3. If you do declare it static but need to access the pointer to the table in another module in order to pass it to an initialization function, you can define an access function.
    const TmrConfig_t* Timer_GetConfig()
    {
    	return TmrConfig;
    }
    

Create the Initialization Routine

Finally, you need an initialization routine that takes the configuration table pointer as a const input parameter. The routine will iterate through the entries in the table and configure the devices appropriately by writing to registers.

Here Beningo’s timer initialization example. The registers are stored in pointer arrays.

void Tmr_Init(const TmrConfig_t *Config)
{
	for(int i = 0; i < NUM_TIMERS; i++)
	{
		// Loop through the configuration table and set each register
		if(Config{i].TimerEnable == ENABLED)
		{
			//Enable the clock gate
			*tmrgate[i] |= tmrpins[i];
			
			// Reset the timer register
			*tmrreg[i] = 0;
			
			// Clear the timer counter registe
			*tmrcnt[i] = 0;
			
			// Calculate and set period register
			// period = (System clock freq in Hz / Timer Divider)
			// (1,000,000 / Desired Timer Interval in Microseconds)) - 1
			*modreg[i] = ((GetSystemClock() / Config[i].ClkPrescaler) / (TMR_PERIOD_DIV / Config[i]Interval)) - 1;
			
			// If the timer interrupt is set to ENABLED in the timer 
			// configuration table, set the interrupt enable bit, enable IRQ,
			// and set interrupt priority. Else, clear the enable bit.
			if(Config[i].IntEnabled == ENABLED)
			{
				*tmrreg[i] |= REGBIT6,
				Enable_IRQ(TmrIrqValue[i]);
				Set_Irq_Priority(TmrIrqValue[i], Config[i].IntPriority);
			}
		}
	}
}

Consequences

  • Configuration tables externalize application-specific configuration information, resulting in the following beneficial properties:
    • Decoupling configuration from driver implementations, keeping drivers generic and reusable. Drivers can be reused from one application to the next, and only minor modifications are required to support new microcontrollers.
    • Keeping configuration information in a single, easily found location (rather than scattered throughout the application), making it easier to review and modify.
  • Configuration table entries can be easily scaled up or down as needed.
  • Configuration tables result in increased binary size due to storing tables in flash. The degree to which this impacts a system depends on the amount of flash memory storage and the number and size of tables. For the tiniest microcontrollers, tables can quickly reduce available flash storage. For most modern systems, the impact is negligible.

Known Uses

  • This pattern is described by Jacob Beningo in *Reusable Firmware Development*. Beningo shows examples of using pointer arrays in combination with a configuration table when implementing the initialization routine for a Timer driver and GPIO driver in his HAL, and his examples are shown above. Configuration tables are used to store initial configuration information for all instances used in an application for any given peripheral type.
  • Configuration tables can be used to support different board revisions within a single binary. Each revision can have separate tables that map to the specific hardware configuration. On boot, the software can identify the hardware revision and load the proper table.
    • More advanced implementations might use a base table, tracking subsequent revisions based on the “deltas”, which are then copied into the primary table before implementation occurs.
    • Alternatively, an update system can download a separate binary file that contains the tables for the target hardware configuration, loading only those tables into flash. This approach trades increased update complexity for reduced on-device storage requirements, since you no longer need to store tables that will not be used by a given configuration.
  • Configuration tables are also useful for specifying the initialization parameters for types in an OSAL. For example, a table can be used to manage the threads, mutexes, or message queue configurations for a given application. Beningo gives the following FreeRTOS task configuration example in his article on the subject.
    /**
     * Task configuration structure used to create a task configuration table.
     * Note: this is for dynamic memory allocation. We create all the tasks up front
     * dynamically and then never allocate memory again after initialization.
     * todo: This could be updated to allocate tasks statically. 
     */
    typedef struct
    {
    	TaskFunction_t const TaskCodePtr;           /*< Pointer to the task function */
    	const char * const TaskName;                /*< String task name             */
    	const configSTACK_DEPTH_TYPE StackDepth;    /*< Stack depth                  */
    	void * const ParametersPtr;                 /*< Parameter Pointer            */
    	UBaseType_t TaskPriority;                   /*< Task Priority                */
    	TaskHandle_t * const TaskHandle;            /*< Pointer to task handle       */
    }TaskInitParams_t;
    
     /**
     * Task configuration table that contains all the parameters necessary to initialize
     * the system tasks. 
     */
    TaskInitParams_t const TaskInitParameters[] = 
    {
    	// Pointer to the Task function, Task String Name  ,  The task stack depth       ,   Parameter Pointer, Task priority  , Task Handle 
    	{(TaskFunction_t)Task_Telemetry,   "Task_Telemetry",    TASK_TELEMETRY_STACK_DEPTH,   &Telemetry, TASK_TELEMETRY_PRIORITY,   NULL       }, 
    	{(TaskFunction_t)Task_TxMessaging, "Task_TxMessaging",  TASK_TXMESSAGING_STACK_DEPTH, NULL      , TASK_TXMESSAGING_PRIORITY, NULL       }, 
    	{(TaskFunction_t)Task_RxMessaging, "Task_RxMessaging",  TASK_RXMESSAGING_STACK_DEPTH, &Telemetry, TASK_RXMESSAGING_PRIORITY, NULL       }, 
    	{(TaskFunction_t)Task_SensorData,  "Task_SensorData",   TASK_SENSOR_STACK_DEPTH,      &Telemetry, TASK_SENSOR_PRIORITY,      NULL       }, 
    	{(TaskFunction_t)Task_Diagnostic,  "Task_Diagnostic",   TASK_DIAGNOSTIC_STACK_DEPTH,  &Telemetry, TASK_DIAGNOSTIC_PRIORITY,  NULL       }, 
    	{(TaskFunction_t)Task_Application, "Task_Application",  TASK_APPLICATION_STACK_DEPTH, &Telemetry, TASK_APPLICATION_PRIORITY, NULL       }, 
    };
    

    The corresponding initialization routine would just loop over the structure and create tasks:

    // Loop through the task table and create each task. 
    for(uint8_t TaskCount = 0; TaskCount < TasksToCreate; TaskCount++)
    {
    	// Elided for brevity, but: check return code and assert if not pdPASS
    	xTaskCreate(TaskInitParameters[TaskCount].TaskCodePtr,
    				  TaskInitParameters[TaskCount].TaskName,
    				  TaskInitParameters[TaskCount].StackDepth,
    				  TaskInitParameters[TaskCount].ParametersPtr,
    				  TaskInitParameters[TaskCount].TaskPriority, 
    				  TaskInitParameters[TaskCount].TaskHandle);
    }
    

References

  • Reusable Firmware Development, by Jacob Beningo, discusses configuration tables in Chapter 4.

    A good practice is to place the structure definition within a header file, such as timer_config.h. An example timer configuration structure can be found in Figure 4-19. Keep in mind that once this structure is created the first time, it will only require minor modification to be used with another microcontroller.

    The initialization function can be written to take the configuration parameters for the clock and automatically calculate the register values necessary for the timer to behave properly so that the developer is saved the painful effort of calculating the register values.

    The initialization can be written to simplify the application developers’ software as much as possible. For example, a timer module could have the desired baud rate passed into the initialization, and the driver could calculate the necessary register values based on the input configuration clock settings. The configuration table then becomes a very high-level register abstraction that allows a developer not familiar with the hardware to easily make changes to the timer without having to pull out the datasheet.

    In my own development efforts, I typically design a new HAL as the need arises. Once designed though, I can reuse the HAL from one project to the next with little to no effort. Application code becomes easily reusable because the interface doesn’t change! I use configuration tables to initialize the peripherals, and once the common features are identified, the initialization structure doesn’t change. A typical peripheral driver using the HAL interface takes less than a day to implement in most circumstances.

    The best place to start is at the configuration table. The configuration table lists the primary features of the driver that need to be configured at startup. Manipulating and automating this table and its configuration is the best bet for testing the initialization code.

    Create configuration tables so that drivers and application modules are easily configurable rather than hard coded. Add enough flexibility so that at a later time the software can be improved without bringing down a house of cards.

    The initialization function should take a pointer to a configuration table that will tell the initialization function how to initialize all the Gpio registers. The configuration table in systems that are small could contain nearly no information at all, whereas sophisticated systems could contain hundreds of entries. Just keep in mind, the larger the table is, the larger the amount of flash space is that will be used for that configuration. The benefit is that using a configuration table will ease firmware maintenance and improve readability and reusability. On very resource-constrained systems where a configuration table would use too much flash space, the initialization can be hard coded behind the interface, and the interface can be left the same.

  • Using Callbacks with Interrupts by Jacob Beningo

    A configuration table could be used to assign the function that is executed. The advantages here are multifold such as:

    • The function is assigned at compile time
    • The assignment is made through a const table
    • The function pointer assignment can be made so that it resides in ROM versus RAM which will make it unchangeable at runtime
  • A Simple, Scalable RTOS Initialization Design Pattern by Jacob Beningo

    I often find that developers initialize task code in seemingly random places throughout their application. This can make it difficult to make changes to the tasks, but more importantly just difficult to understand what all is happening in the application. It also makes it so that the application is not very scalable or easy to adapt and sometimes results in developers not knowing that a task even exists!

    The design pattern, which I often follow for as much of my code as possible, is to create a generic initialization function that can loop through a configuration table and initialize all the tasks.

    There are certainly several different ways that this can be done, but the idea is to make it so that the driver code is constant, unchanging and could even be provided as a precompiled library. The application code can then still easily change the interrupt behavior without having to see the implementation details.

Categories: Field Atlas


« Back to Glossary Index

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.