One of the good thing in
C that I didn`t use in my early days of embedded programming was Function
pointers ( although it was there in the project thanks to the vendor supplied
drivers ). But in many of my projects that helped me to create efficient software.
So I decided to write about that in my first technical blog.
There are three major
possible use cases for function pointers – Arrays of function pointers (Lookup
table), vector table ( type of lookup table ) and callback functions. We will
see about each of that, one by one.
Let`s look at lookup
table use cases first, to be honest I had been deliberately avoiding the use of
the same in my early days as I was not confident due to its cryptic syntax. But
as I worked with the projects that had multi-level menu, function pointer was
one of the key feature in C that got my attention. I have developed lot of
vending products with the multi-level menu over the years, in those projects
function pointers not only provided efficiency, but as well as the readability,
as using switch case for a menu could go on for more than one page.
The basic declaration of
arrays of function pointers in shown below.
void (*pf[])(void) =
{
fna,
fnb,
fnc,
...
fnz
};
There are few things
which could be added to make the above declaration better as shown in below
code snippet, which is inspired by nigel jones “Arrays of Pointers to
Functions” article (Link is attached in the end of the article).
Static void (* const pf[])(void) =
{
fna,
fnb,
fnc,
...
fnz
};
Below shows the use case of arrays of function
pointers in one of my project. There will be request from another node to my
target, for that request target has to invoke the appropriate callback
routines. This can be done with the help of switch case, but as the number of
request commands increases, the concerning switch case will become too large
and will hover across more than one page, results in poor readability and also
poor efficiency if the case which positioned bottom of the switch case is
executed frequently. Both of this can be addressed ( In cache enabled systems
efficiency may differ ) with the help of lookup table (arrays of function
pointers) as you can see below.
Static void ( * const RequestHandler[]) ( void ) =
{
vDoTask1,
vDoTask2,
vDoTask3,
…
vDoTaskN,
};
void AcceptRequest ( uint8_t u8RequestCommand )
{
if (
u8RequestCommand % 2 )
{
u8RequestCommand = u8RequestCommand / 2;
if ( u8RequestCommand < sizeof(
RequestHandler ) / sizeof( *RequestHandler ) )
{
RequestHandler [ u8RequestCommand ]( ) ;
}
}
}
Here all the request
commands are odd value and all the reply commands from target are even value,
so u8RequestCommand is being divided by 2, after that we ensure the
u8RequestCommand is within the limit of look-up table and that value is being used to lookup the target
function in lookup table.
There are few things to
be noted in this declaration:
1) Prefixing the
declaration with static ensures that RequestHandler can`t be accessed outside
of the module ( file ). You can put the RequestHandler declaration inside the
AcceptRequest function definition as that will even prevent the access outside
of the AcceptRequest function.
2) By testing the u8RequestCommand will ensure only
valid command requests will be accepted and we can be rest assured out of bound
access will not happen.
3) By declaring the RequestHandler with the const
will make it more likely to be placed in ROM. This will prevent the lookup
table being changed at runtime results in more reliable code.
4) Other things like making the datatype
u8RequestCommand unsigned and choosing the smallest datatype possible provide
some protection as mentioned in nigel jones article.
In case if the request
functions need arguments with different data type, we can change the prototype
as shown below,
Static uint8_t ( * const RequestHandler[]) (
void * ) =
{
…
}
This provides more
flexibility to the user as in this case, we just need to typecast the pointer
to appropriate types in the member functions as shown below,
Void vDoTask1 ( void * pParamInput )
{
Uint32
u32Input = 0;
u32Input
= * ( ( uint32_t * ) pParamInput );
….
}
Void vDoTask2 ( void * pParamInput )
{
float
fInput = 0;
fInput
= * ( ( float * ) pParamInput );
….
}
We can even pass the
variable number of arguments with the help of structures, but use cases for
these are rare.
As far as use of
function pointers as a vector table is concerned most of the time we don`t need
to change the startup file provided by the vendor so does the vector table, but if we want to port the project from one compiler to another (
e.g: gcc to IAR ) we may need to tweak that. But I prefer having the respective
macro definition for each of the handlers I`m using in platform layer file, and
by changing that macro we can switch between different compilers by just
changing the startup file for the respective compiler. Below shows the vector
table for SAM3S controller in startup file given by IAR.
const
IntVector __vector_table[] =
{
{ .__ptr = __sfe( "CSTACK" ) },
__iar_program_start,
NMI_Handler,
…
UART0_Handler, /*
8 UART0 */
…
IrqHandlerNotUsed /* 35
not used */
};
Below shows the vector
table for the same SAM3s in in startup file given by Atmel (GCC Based
compiler),
IntFunc exception_table[] = {
/* Configure Initial Stack Pointer, using
linker-generated symbols */
(IntFunc) (&_estack),
Reset_Handler,
NMI_Handler,
…
USART0_Handler, /*
14 USART 0 */
…
Dummy_Handler /* 35 not used */
};
As there are different
handler names used in different compilers for the same IRQ, we need to map
this with our handler. One example is shown below,
#define
DEBUG_USART_HANDLER USART0_Handler
Void
DEBUG_USART_HANDLER ( void )
{
…
}
Another use of function
pointer is through the use of callback functions which is there in almost every
embedded project.
You can refer the below
article for more information which provides ample information about the use of
function pointers.
Really good work. Keep going. It makes lot of information
ReplyDeleteThanks 😊
Delete