Editor's note: This article, last in a series of discussions of x macro usage, examines how to use x macros to automate the task of configuring jump tables and lookup tables used by communication handlers.

Part 2 in this series showed how to develop the following x macro table with the caveat that the command codes needed to be contiguous:

/* —— NAME ——- FUNCTION — CODE — */

#define COMMAND_TABLE

ENTRY(COMMAND0, command0, 0x00)

ENTRY(COMMAND1, command1, 0x01)

ENTRY(COMMAND2, command2, 0x02)

…

ENTRY(COMMANDX, commandX, 0x0X)



One nice thing about having contiguous codes is that the test for index validity is simple:

ASSERT(command < N_COMMANDS);

command_jump_table[command]();

When command codes are not contiguous, you need to ensure that you don’t jump to a non-existing function. The simplest way to do this is to make the jump table large enough to support every possible index. However, this is at the expense of memory. You will need 512 bytes (assuming an 8-bit command code size) if the architecture uses 16-bit pointers or 1024 bytes if it uses 32-bit pointers.

With this implementation, instead of the simple ASSERT statement, define an error handling function that can be embedded into the jump table for all invalid commands.

Here is how to do that with x macros, together with some other preprocessor trickery:

#define INIT_X1 process_reserved,

#define INIT_X2 INIT_X1 INIT_X1

#define INIT_X4 INIT_X2 INIT_X2

#define INIT_X8 INIT_X4 INIT_X4

#define INIT_X16 INIT_X8 INIT_X8

#define INIT_X32 INIT_X16 INIT_X16

#define INIT_X64 INIT_X32 INIT_X32

#define INIT_X128 INIT_X64 INIT_X64

#define INIT_X256 INIT_X128 INIT_X128



#define EXPAND_JUMP_TABLE(a,b,c) [c] = process_##b,



static const p_func_t command_jump_table[256] = {

/* initialize all pointer to the reserved function */

INIT_X256,

/* overwrite pointers to valid functions */

COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)

};

This requires the use of a C99 compiler for the designated initializer syntax, furthermore the compiler may warn you that you are overriding a previously initialized value. If a C99 compiler is not available, then the valid function pointers can be updated at run time, at the expense of not being able to place the jump table in ROM. Here's how to this:

#define EXPAND_JUMP_TABLE(a,b,c)

command_jump_table[c] = process_##b;

/* during run-time initialization */

COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)

Optimizing jump tables

For many applications where memory is limited, this implementation is not practical. What can be done instead is to add an extra level of indirection as follows:

command_jump_table[command_offset_table[command]]();

In this case a second table is utilized that has one entry for every possible command code. Each entry contains an offset into the actual jump table. The memory requirements for this implementation are significantly less than the initial implementation. We need 256 bytes for our offset table, but only 2 or 4 bytes for each function pointer in the jump table. Therefore implementing a communications handler with a dozen commands would need 280/304 bytes instead of 512/1024.

One side-effect of this implementation is that we need to reserve one of the command codes since invalid commands need to resolve to a valid offset so that the correct error handler can be called. The most logical offset value to use is zero (simplifies initialization) and thus the new command table looks like this:

/* —— NAME ——- FUNCTION — CODE — */

#define COMMAND_TABLE

ENTRY(RESERVED, reserved, 0x00)

ENTRY(COMMANDA, commandA, 0x02)

ENTRY(COMMANDB, commandB, 0x09)

…

ENTRY(COMMANDZ, commandZ, 0xef)

A nice side-effect of this implementation is that it is actually easier to automate the creation of these two separate tables than it is to automate the creation of the single jump table.

The creation of the jump table has not changed from our original example with contiguous command codes. The caveat being that we can no longer use the table directly via the command code.

#define EXPAND_AS_JUMP_TABLE(a,b,c) process_##b,

static const p_func_t command_jump_table[N_COMMANDS] = {

COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)

};

The creation of the offset table is as follows (recall the following struct from last month):

#define EXPAND_AS_STRUCT(a,b,c) uint8_t b,

typedef struct{

COMMAND_TABLE(EXPAND_AS_STRUCT)

} size_struct_t;

#define N_COMMANDS sizeof(size_struct_t)

This struct can be used for multiple purposes. In addition to taking the size of the struct, we can calculate the offsets of each element in the struct to use when initializing the offset table:

#define EXPAND_AS_OFFSET_TABLE_INITIALIZER(a,b,c)

[c] = (uint8_t) offsetof(size_struct_t, b);

…where offsetof() is a standard library macro defined in “stddef.h”. If the reader is unfamiliar with the offsetof() macro I highly recommend reading the article by Nigel Jones, Learn a new trick with the offsetof() macro. Use this new x macro do the following:

/* statically declare our offset table, valid commands initialized to the correct offsets and invalid command initialized to 0 */

uint8_t command_offset_table[256] = {

COMMAND_TABLE(EXPAND_AS_OFFSET_TABLE_INITIALIZER)

};

As before, this macro expansion relies on a C99 compiler. If a C99 is not available, do the following:

uint8_t command_offset_table[256] = {0};



#define EXPAND_AS_OFFSET_TABLE_INITIALIZER(a,b,c)

command_offset_table[c] = (uint8_t) offsetof(size_struct_t, b);

/* during run-time initialization */

COMMAND_TABLE(EXPAND_AS_OFFSET_TABLE_INITIALIZER)

A nice side-effect of the offset table implementation is that if we need to implement a function which identifies if a given code is a valid command; the resulting function is a one-liner:

bool command_is_valid(uint8_t command){

return command_offset_table[command];

}

Timeout Tables

Another common table that can be similarlyinitialized is a timeout table. In the case of a master processorinstead of a slave processor, you need to allow a set period of time forthe slave to respond before determining that a communications fault hasoccurred. Usually different commands will have differing requirementsfor this time, so implementing a look-up table for each command isnecessary. Modifying the x macro table, we can add this functionalityeasily:

/* —— NAME ——- FUNCTION — CODE — TIMEOUT(ms) —*/

#define COMMAND_TABLE

ENTRY(RESERVED, reserved, 0x00, 0)

ENTRY(COMMANDA, commandA, 0x02, 100)

ENTRY(COMMANDB, commandB, 0x09, 500)

…

ENTRY(COMMANDZ, commandZ, 0xef, 20)



#define EXPAND_AS_TIMEOUT_TABLE_INITIALIZER(a,b,c,d) d,

uint16_t timeout_table[N_COMMANDS] = {

COMMAND_TABLE(EXPAND_AS_TIMEOUT_TABLE_ININTIALIZER)

};

Here is how I would use this table:

int get_command_timeout(uint8_t command) {

return timeout_table[command_offset_table[command]];

};

GPIO Configuration

Onefinal area is the use of x macros to allow for code reuse by making itsimple to change what gpio pins are used by a module. As an exampleconsider the following table:

#define SWITCHES_GPIO_TABLE(ENTRY)

ENTRY(SWITCHA, GPIOE, GPIO_Pin_0)

ENTRY(SWITCHB, GPIOA, GPIO_Pin_0)

ENTRY(SWITCHC, GPIOC, GPIO_Pin_5)

ENTRY(SWITCHD, GPIOE, GPIO_Pin_1)



Iwill leave it to the reader as an exercise, but from this table, aslong as the supporting code follows a few conventions, all you have todo is change the port and pin name in the table and all of theinitialization code adjusts accordingly, making it easy to port over to anew design.

Final Thoughts

I hope you have enjoyedthis overview of x macros and that you have learned something new in theprocess. Though an advanced technique, once in place the use of xmacros can reduce errors and make the embedded programmer's lifeeasier. I would love to read in the comments how readers are using xmacros in their designs.

Read Part 1

Read Part 2

Andrew Lucas leads a team of firmware developers at NCR Canada, where he isresponsible for the firmware architecture of intelligent deposit modulesfor NCR's line of ATMs.

Share this: Twitter

Facebook

LinkedIn

More

Reddit

Tumblr



Pinterest

WhatsApp



Skype

Pocket



Telegram

