It is well-understood that reusing the same code across multiple products is ideal. However, how can it be done when each product has different features, constants, and chips? Here are a few techniques I used to have one device driver support several HP LaserJet printers with different features, capabilities, and chips.
Years ago when I started with the device driver, there was just one product it had to support, so the driver was written specifically for that product. When I ported the driver to the next product, I used #define and #ifdef statements to manage the product-specific code, like this:
#ifdef PRODUCT_SKYWALKER # define CHIP_BASE_ADDRESS 0x80004000 #else // for product Vader # define CHIP_BASE_ADDRESS 0x6002C000 #endif ... #ifdef PRODUCT_SKYWALKER *regConfig |= HQ_MODE_1; #else *regConfig |= HQ_MODE_2; #endif *regConfig |= MEMORY_LOCAL;
But, when I added support for a third product, the code quickly became messy:
#ifdef PRODUCT_SKYWALKER # define CHIP_BASE_ADDRESS 0x80004000 #else // for product Vader or Obiwan # define CHIP_BASE_ADDRESS 0x6002C000 #endif ... #ifdef PRODUCT_SKYWALKER *regMode |= HQ_MODE_1; #else // for Vader or Obiwan *regMode |= HQ_MODE_2; #endif #ifndef PRODUCT_OBIWAN // Skywalker and Vader only *regConfig |= MEMORY_LOCAL; #endif
I analyzed the mess and determined that I was switching on product-specific features and chip-specific features. Product-specific features were unique to that product. Chip-specific features were unique to that chip, no matter which product used it. So, I created two types of feature switches, product-features and chip features. Based on the product, I turn on appropriate product-specific switches, including which chip that product uses. Then based on the chip, I turn on other appropriate switches. So, I modified the code this way.
// Use product switches to specify chip and enable features #if defined PRODUCT_SKYWALKER # define FEATURE_MEMORY_LOCAL # define CHIP_CHOCOLATE #elif defined PRODUCT_VADER # define FEATURE_MEMORY_LOCAL # define CHIP_POTATO #elif defined PRODUCT_OBIWAN # define CHIP_POTATO #else # error Unrecognized product #endif // Use chip switches to enable chip features #if defined CHIP_CHOCOLATE # define BASE_ADDRESS 0x80004000 # define HQ_MODE HQ_MODE_1 #elif defined CHIP_POTATO # define BASE_ADDRESS 0x6002C000 # define HQ_MODE HQ_MODE_2 #else # error Unrecognized chip #endif *regMode |= HQ_MODE; #ifdef FEATURE_MEMORY_LOCAL *regConfig |= MEMORY_LOCAL; #endif
By breaking down product differences and chip differences into features, this code uses the product name switches only once at the beginning of the code to specify feature switches. Then feature switches are used throughout the code as necessary. When porting to a new product, a new product switch can be added to the list of products at the beginning to turn on its appropriate features and the code is ready to go.
This is a very simple example but, as the number of supported products and differences increases, the number of switches also increases. This style helps keep the device driver code manageable and usable across several chips and products.
Until the next switch…
5 Comments
I too use a lot of #ifdefs. It’s nice to use an editor (like N++) that will highlight and gray out (or allow collapsing) the sections that are turned on/off based upon the current define states.
I disagree. The best way is to avoid compiler switches. [Use files] and make the selection via the makefile.
I fear that your suggestions are soon to become obsolete. With hardware/software co-design you do not have the luxury of waiting for the chip. Today’s ASIC teams create the chip-specific files needed by the firmware team.
I’ve found keeping the different product features in one build can become quite cumbersome. Instead I’ve started factoring my code into sections that can be removed or added to projects.
I like the talk about conditional compiler controls. I am supporting 5 variants of a product, 4 of which require product-specific code.