Forums

Q: Hardware Abstraction Architecture

Started by Mark A. Odell December 16, 2003
I'm trying to come up with a simple yet extensible method of writing
firmware with hardware abstraction. For example, suppose one has a two
different boards with some overlap in software functionality and similar
CPUs (same arch., different mfgr). Furthermore, these two boards have
different revision levels which cause some of the functions to do things
slightly differently. I've been talking to my workmates and we've been
oozing in the direction of the following: 

i)  a board depends on its rev. and the CPU type
ii) an application depends on its board

So if I have board a and board be and an application foo then I'd have
foo.c, board_a.c, and board_b.c. Now foo.c would have foo application
generic functions while board_a.c and board_b.c would have functions with
identical names and signatures, prototyped in board.h, but each file would
have board-specific differences in implementation (see Fig. 1).
Furthermore, board_a,b.c file would have rev. difference files and CPU
primative files to isolate them from the containing the annoying rev.
based changes and minutia of CPU differences. 

I just can't seem to get my arms around this problem though. Any help
appreciated. 

Figure 1:          foo.c
                       #include "board.h"

                       init_board();
                       main();

                    board.h
                       #ifdef BOARD_TYPE == A
                       #    include "board_a.h"
                       #elif BOARD_TYPE == B
                       #    include "board_b.h"
                       #else
                       #    error "Bad board choice"
                       #endif

                       init_board();


                    board_a.h
                       #define BOARD_MEM_SIZE 1024
                       #define BOARD_CLOCK_HZ 100000000

                    board_a.c
                       #include "board.h"

                       init_board() { }


                    board_b.h
                       #define BOARD_MEM_SIZE 4096
                       #define BOARD_CLOCK_HZ 400000000

                    board_b.c
                       #include "board.h"

                       init_board() { }


Thanks,

-- 
- Mark ->
--
"Mark A. Odell" <nospam@embeddedfw.com> wrote in
news:Xns9453AD8DE154CopyrightMarkOdell@130.133.1.4: 

Oh, the linker is told, via the makefile, which .o file to use, e.g. if
the board is board A then link with board_a.o. 

-- 
- Mark ->
--
"Mark A. Odell" <nospam@embeddedfw.com> wrote in message
news:Xns9453AE071B7FDCopyrightMarkOdell@130.133.1.4...
> "Mark A. Odell" <nospam@embeddedfw.com> wrote in > news:Xns9453AD8DE154CopyrightMarkOdell@130.133.1.4: > > Oh, the linker is told, via the makefile, which .o file to use, e.g. if > the board is board A then link with board_a.o. > > -- > - Mark -> > --
Instead of architecting the interface of an entire board (which I assume has different devices), I suggest trying to abstract the interface of each of the devices on the board as a "class". This limits the scope of your interface to just a particular device; if, later on, other devices are added/removed, the effort to change your interfaces is minimized. If your interfaces are abstract enough, it is not necessary to supply different header files for different boards. You can use the same header file, but with different implementations that hide the differences in devices on the boards. Regards, Travis
Travis Breitkreutz wrote:

> Instead of architecting the interface of an entire board (which I assume has > different devices), I suggest trying to abstract the interface of each of > the devices on the board as a "class". This limits the scope of your > interface to just a particular device; if, later on, other devices are > added/removed, the effort to change your interfaces is minimized. > > If your interfaces are abstract enough, it is not necessary to supply > different header files for different boards. You can use the same header > file, but with different implementations that hide the differences in > devices on the board.
Those are good ideas. One issue to look at is whether to have separate source files for the different versions or to use conditional inclusion to differentiate. I lean towards the latter when they are fairly similar in order to use as much "single source" definition as possible, so that a change which would effect both versions is only changed in one place. That advantage needs to weighed against the cost of making the code harder to read with lots of conditional code. When I do use conditional code, I try to avoid a lot of small conditional sections crammed together. Thad
"Thad Smith" <ThadSmith@acm.org> wrote in message
news:3FE00BEF.20A8E66@acm.org...
> Travis Breitkreutz wrote: > > > Instead of architecting the interface of an entire board (which I assume
has
> > different devices), I suggest trying to abstract the interface of each
of
> > the devices on the board as a "class". This limits the scope of your > > interface to just a particular device; if, later on, other devices are > > added/removed, the effort to change your interfaces is minimized. > > > > If your interfaces are abstract enough, it is not necessary to supply > > different header files for different boards. You can use the same
header
> > file, but with different implementations that hide the differences in > > devices on the board. > > Those are good ideas. > > One issue to look at is whether to have separate source files for the > different versions or to use conditional inclusion to differentiate. I > lean towards the latter when they are fairly similar in order to use as > much "single source" definition as possible, so that a change which > would effect both versions is only changed in one place. That advantage > needs to weighed against the cost of making the code harder to read with > lots of conditional code. When I do use conditional code, I try to > avoid a lot of small conditional sections crammed together.
That's a good point, too. At my workplace, there used to be a bias against conditional coding because some people had some very bad experiences with poorly designed conditional code. However, using separate source files for devices that were very similar caused an enormous amount of confusion and inconsistency during maintenance. (Some files would get new features, others would not; bugs would be fixed in some files, but not others; etc.) We are currently migrating towards a more sensible combination of conditional code (when possible) and separate source (when necessary). Regards, Travis
Travis Breitkreutz <breitkreutz_travis_o@insightbb.com> wrote:

> That's a good point, too. At my workplace, there used to be a bias > against conditional coding because some people had some very bad > experiences with poorly designed conditional code. However, using > separate source files for devices that were very similar caused an > enormous amount of confusion and inconsistency during maintenance.
You appear to have duplicated more than was good for maintainability, then. As the saying goes, don't do that, then!
> fixed in some files, but not others; etc.) We are currently > migrating towards a more sensible combination of conditional code > (when possible) and separate source (when necessary).
Yep. Maybe the most sensible possibility would be: never use #ifdef for anything else but to decide which platform-specific file to #include, from a generic #include. And never put code in a platform-specific source file that doesn't absolutely have to be in there. It may be worthwile investigating OO-like techniques, most prominently inheritance and polymorphism. I.e. have a "default platform abstraction" as a (kind of) class, and overwrite only those parts of it that you must, for the individual variations. Using macros to map generic names to specific ones, you won't even need function pointers for that, so no loss of efficiency. It could look something like this: ------- generic.h: #ifdef USING_VARIANT1 # include "variant1.h" #endif #ifndef INCLUDED_FROM_GENERIC_C #ifndef actual_foo extern void generic_foo(void); #define actual_foo generic_foo #endif #ifndef actual_bar extern void generic_bar(void); #define actual_bar generic_bar #endif #endif ------- variant1.h: /* to keep generic foo(): do nothing */ /* to overwrite generic bar() with our specialized version: */ extern void variant1_bar(void); #define actual_bar variant1_bar ------- ends Same tricks can be applied to global variables. Macros are #ifdef tested on their own names, so definitions from variant1.h survive through generic.h. -- Hans-Bernhard Broeker (broeker@physik.rwth-aachen.de) Even if all the snow were burnt, ashes would remain.
Hans-Bernhard Broeker <broeker@physik.rwth-aachen.de> wrote in
news:brprhm$dkv$1@nets3.rz.RWTH-Aachen.DE: 

> Yep. Maybe the most sensible possibility would be: never use #ifdef > for anything else but to decide which platform-specific file to > #include, from a generic #include. And never put code in a > platform-specific source file that doesn't absolutely have to be in > there.
I wholly agree with this.
> It may be worthwile investigating OO-like techniques, most prominently > inheritance and polymorphism. I.e. have a "default platform > abstraction" as a (kind of) class, and overwrite only those parts of > it that you must, for the individual variations. Using macros to map > generic names to specific ones, you won't even need function pointers > for that, so no loss of efficiency. It could look something like > this: > > ------- generic.h: > #ifdef USING_VARIANT1 > # include "variant1.h" > #endif > > #ifndef INCLUDED_FROM_GENERIC_C > #ifndef actual_foo > extern void generic_foo(void); > #define actual_foo generic_foo > #endif > #ifndef actual_bar > extern void generic_bar(void); > #define actual_bar generic_bar > #endif > #endif
This trick seems to be unnecessary as the linker will be in control of which variant .o file is used. E.g. if I have cpu_a_cache.c file and a cpu_b_cache.c file, each would have functions with identical names and signatures, such as void disable_icache(void); Since the linker will pull one and only one of these .o files, there is no name conflict. The common prototype goes in cpu_cache.h and consumers of the cache functionality refer to the function disable_icache() without need of #define renaming. -- - Mark -> --
Mark A. Odell <nospam@embeddedfw.com> wrote:
[...]
> This trick seems to be unnecessary as the linker will be in control of > which variant .o file is used. E.g. if I have cpu_a_cache.c file and a > cpu_b_cache.c file, each would have functions with identical names and > signatures, such as void disable_icache(void); Since the linker will pull > one and only one of these .o files, there is no name conflict.
But that scheme starts to break when CPU board c comes along, which needs to have *another* function specialized, which a and b shared. One idea behind my plan, which I failed to mention, is that having more than one source file specific to a given variant creates another maintenance nightmare. I for one wouldn't like to have all of cpu_b_cache.c cpu_b_mymemcpy.c cpu_b_whatever.c cpu_b_foo.c and so on. You'ld end up with O(F*V) source files for F features to be done on V board variants. Rather collect as many hardware dependencies as possible in a single file, cpu_b.c.
> The common prototype goes in cpu_cache.h and consumers of the cache > functionality refer to the function disable_icache() without need of > #define renaming.
To put it closer to your terms, I would suggest not only having a cpu_cache.h, but also a cpu_cache.c, where cpu_cache.o would always be built and linked in. Or one could use a library of generic functions and per-variant source files to override only what's needed. One could link to both cpu_a.o and generic.lib, in that order, such that functions not defined by cpu_a.o would automatically be supplied by generic.lib. And if only some tiny detail changed, it may even work to call the generic function from the specific version. Even a somewhat automatic "updated all variants with this patch?" check could be done: each time any of the generic library functions is modified, you increment a #define'd version number in the header for this function. Every override implementation checks against that number and refuses to compile until edited. Doesn't help against active stupidity, but avoids passive oversight. -- Hans-Bernhard Broeker (broeker@physik.rwth-aachen.de) Even if all the snow were burnt, ashes would remain.
Hans-Bernhard Broeker <broeker@physik.rwth-aachen.de> wrote in
news:brpvl2$ifn$1@nets3.rz.RWTH-Aachen.DE: 

> But that scheme starts to break when CPU board c comes along, which > needs to have *another* function specialized, which a and b shared.
I'd definitely have a board_n.c file that would initialize a given hardware design properly based upon the CPU and its peripherals as well as off-chip periperals.
> One idea behind my plan, which I failed to mention, is that having > more than one source file specific to a given variant creates another > maintenance nightmare. I for one wouldn't like to have all of > > cpu_b_cache.c > cpu_b_mymemcpy.c > cpu_b_whatever.c > cpu_b_foo.c > > and so on. You'ld end up with O(F*V) source files for F features to > be done on V board variants. Rather collect as many hardware > dependencies as possible in a single file, cpu_b.c.
I see your point but I oft times the core doesn't change but the periperals do (slightly sometimes). That is, the IBM405GPr and the 440GX share some peripherals exactly but are different on others. It'd be nice to have a family file for the 4xx series, say for the UART, and then use it for all families of the 4xx CPU.
>> The common prototype goes in cpu_cache.h and consumers of the cache >> functionality refer to the function disable_icache() without need of >> #define renaming. > > To put it closer to your terms, I would suggest not only having a > cpu_cache.h, but also a cpu_cache.c, where cpu_cache.o would always be > built and linked in. Or one could use a library of generic functions > and per-variant source files to override only what's needed. One > could link to both cpu_a.o and generic.lib, in that order, such that > functions not defined by cpu_a.o would automatically be supplied by > generic.lib. And if only some tiny detail changed, it may even work > to call the generic function from the specific version.
I see taht cpu_cache.c file as an extra layer of wrapping if all the cpu_X_cache.c files all have same-named functions. Unless there is some common code shared by every cache on earth, I can't see putting anything at the level of cpu_cache.c. I'd rather have the linker choose which .o file to link against than have the C preprocessor re-defining function names to a common name I guess. Thanks for replies. -- - Mark -> --
"Mark A. Odell" <nospam@embeddedfw.com> wrote in message
news:Xns945475734630FCopyrightMarkOdell@130.133.1.4...
> Hans-Bernhard Broeker <broeker@physik.rwth-aachen.de> wrote in > news:brpvl2$ifn$1@nets3.rz.RWTH-Aachen.DE: > > > But that scheme starts to break when CPU board c comes along, which > > needs to have *another* function specialized, which a and b shared. > > I'd definitely have a board_n.c file that would initialize a given > hardware design properly based upon the CPU and its peripherals as well as > off-chip periperals. > > > One idea behind my plan, which I failed to mention, is that having > > more than one source file specific to a given variant creates another > > maintenance nightmare. I for one wouldn't like to have all of > > > > cpu_b_cache.c > > cpu_b_mymemcpy.c > > cpu_b_whatever.c > > cpu_b_foo.c > > > > and so on. You'ld end up with O(F*V) source files for F features to > > be done on V board variants. Rather collect as many hardware > > dependencies as possible in a single file, cpu_b.c. > > I see your point but I oft times the core doesn't change but the > periperals do (slightly sometimes). That is, the IBM405GPr and the 440GX > share some peripherals exactly but are different on others. It'd be nice > to have a family file for the 4xx series, say for the UART, and then use > it for all families of the 4xx CPU.
Another issue with collecting everything into only one source file is that the application pays a memory penalty for peripherals that it links in but doesn't use. (Some -- but not all -- linkers can extract from a .o file only those functions that are actually used.) Maintenance may be more of a hassle with several files, but the cost is usually outweighed by the benefit of fitting a viable application into a smaller footprint. Of course, if the given platform has sufficient memory resources, then this is not an issue -- but I'm not lucky enough to work on such platforms.
> >> The common prototype goes in cpu_cache.h and consumers of the cache > >> functionality refer to the function disable_icache() without need of > >> #define renaming. > > > > To put it closer to your terms, I would suggest not only having a > > cpu_cache.h, but also a cpu_cache.c, where cpu_cache.o would always be > > built and linked in. Or one could use a library of generic functions > > and per-variant source files to override only what's needed. One > > could link to both cpu_a.o and generic.lib, in that order, such that > > functions not defined by cpu_a.o would automatically be supplied by > > generic.lib. And if only some tiny detail changed, it may even work > > to call the generic function from the specific version. > > I see taht cpu_cache.c file as an extra layer of wrapping if all the > cpu_X_cache.c files all have same-named functions. Unless there is some > common code shared by every cache on earth, I can't see putting anything > at the level of cpu_cache.c. I'd rather have the linker choose which .o > file to link against than have the C preprocessor re-defining function > names to a common name I guess.
Whether cpu_device.c is necessary depends upon the nature of the given device and its family. I can think of a few cases where using such a scheme would be beneficial. In general, though, one can't always tell whether using cpu_device.c provides real benefit until after one has developed support for, or has anticipated the use of, several similar devices. At least initially, I would keep things simple. Regardless of how device support is implemented, I think we all agree that it is far more important to design a single set of clear interfaces for a given device. The underlying implementation (and its organization) can then change whenever necessary without affecting an application that uses the interfaces. Regards, Travis