Porting firmware code to a new hardware platform often requires making changes that break support for old platforms. Code maintenance, defect fixes, and new features become cumbersome and error prone when such changes must be made across multiple versions of the same code.
In last month’s issue, I talked about putting a marker in comments (such as TUNE TIMING
) to easily find platform-specific code that needs tuning for each platform. However, that approach requires engineers to remember the name of each marker, search for it, and update the associated code appropriately. This can quickly get cumbersome with multiple different markers to remember (e.g. ADJUST BUFFER SIZE
and SET BASE ADDRESS
). Even with a single, standard marker (e.g. PLATFORM SPECIFIC CODE
), this method still has the problem that the resultant code is specific to only one platform. A different version of the code has to be maintained for each supported hardware platform.
Multiple versions of the same code can also arise when a project team takes a snapshot of firmware code from an existing platform for independent development on a different hardware platform. In this situation, each team typically makes changes to their own version of the code in relative isolation, adding features and fixing defects without thought for other platforms that use the same code base. If there are no motivations to share and coordinate development efforts, there will be duplication of work, reduction in quality, and a divergence of design, functionality, and support.
A better approach is to use the same firmware code base across multiple hardware platforms. Differences between platforms can be managed with techniques such as compile- and run-time switches. This allows the bulk of the code to be common to all platforms, thus minimizing defects and costs associated with managing different versions of the same firmware code across multiple hardware platforms.
Several techniques exist for supporting multiple platform versions. Some approaches, such as compile-time and run-time switches, determine the appropriate code to run based on explicit knowledge of the target platform version. Other approaches use exactly the same code (without platform-specific variables or switches) for all supported platforms to achieve their target functionality. These approaches avoid platform-specific variables or switches. Here are two such examples.
Last month I described a problem with a short busy loop used to induce a small delay. Based on tuning experiments, we used a loop count that was 20 or 30% more than necessary to give us some margin. That was margin turned out to be insufficient and caused problems two years later when porting to a new hardware platform with a faster CPU. We subsequently fixed the problem by increasing the loop count by a factor of 10 for all platforms instead of using a different loop count depending on the platform. Although this change introduced an additional delay for platforms that did not need it, the solution fixed the problem, still met performance targets, and retained the platform-independence of the code. The trade-off was worth it.
In a more recent case, one of our clients had a CAN (Controller Area Network) block on an old chip and a new chip. When trying to port the firmware to the new chip, we discovered that we could not access the registers of the CAN block. Investigations revealed that the chip designers had (for technical reasons) left that block in reset mode at power up, which required that firmware take it out of reset before accessing registers. The fix was easy:
WriteReg (CAN_RESET_REG, 0x00000000); // Take out of reset
This line of code worked for both the old and the new chip. In the old chip, taking the block out of reset when it was already out of reset had no effect and was therefore a safe operation. It was a no-op. Since the same code could be used for both the old and the new chip, there was no need for platform-specific variables or switches.
In the process of fixing this problem, we realized that we had a potential problem in the CAN block in the old chip. We had not explicitly reset it before we started to use it; we had assumed that the block was in the power-on default state. While we had not experienced problems with that assumption, we decided that a more robust solution was to also reset the block on the old chip. Thus, the block on the old and the new chip would be guaranteed to be in the same state before continuing.
Again, we were able to use the same operations for both the old and new chips; first, put the block into reset and then take it out of reset. The code looked like this:
// Put the CAN block in reset for all chips, even though on some // chips, the CAN block is already in reset. WriteReg (CAN_RESET_REG, 0x00000001); // Put into reset WriteReg (CAN_RESET_REG, 0x00000000); // Take out of reset // CAN block is now in default state, no matter which chip is used.
The line to put the block into reset became a no-op for the new chip because the block was already in reset. After the reset code, both the old and new are in reset. The second line took them out of reset and the blocks on both chips were then in the same known state. Since the reasons for putting the new block into reset when it was already in reset is not clear, I added comments to explain the purpose of this less-than-intuitive code.
These lines satisfied the different requirements for the CAN block in both the old and new chips without requiring platform-specific variables or switches to check for the version of the hardware platform. This kept the code cleaner and easier to maintain, and reduced defect risk.
Until the next version…
2 Comments
I have used compile time switches to build code for various debugging purposes such as emulating hardware that is not yet ready for integration with the software. This lets one continue developing and testing the software in the absence of the platform hardware.
Regarding delay loops, the best practice of all is to eliminate them entirely and use event-driven code instead. When delay loops are unavoidable, best practice dictates use of a calibrated timer function to produce delays of reliable, repeatable duration. Watchdog timers can be sampled for this purpose, or in a pinch one can sample a bus clock and divide the frequency to a usable range.