software engineering for embedded systems || hardware’s interface to embedded software
TRANSCRIPT
CHAPTER 6
Hardware’s Interface toEmbedded Software
Gary Stringham
Chapter Outline
Introduction 156
Collaborate with the hardware team 157Proactive collaboration 157
Ambassadors 158
Register design tools 158
Co-development activities 160
System integration 160
Useful hardware design aspects 161Notification of hardware events 161
Launching tasks in hardware 162
Bit field alignment 163
Fixed bit positions 164
Block version number 165
Debug hooks 165
Supporting multiple versions of hardware 167Compile-time switches 167
Build-time switches 171
Run-time switches 174
Self-adapting switches 174
Difficult hardware interactions 176Atomic register access 176
Mixed bit types in the same register 178
Edge vs. level interrupts 180
Testing and troubleshooting 180
Temporary hooks 180Permanent hooks 181
Conclusion 182
Best practices 182
155Software Engineering for Embedded Systems.
DOI: http://dx.doi.org/10.1016/B978-0-12-415917-4.00006-2
© 2013 Elsevier Inc. All rights reserved.
Introduction
Most of the other parts of this book discuss embedded software with very little need to refer
to the hardware it is running on. Discussions on co-development and microprocessors talk
to some degree about hardware. If a compiler is used, the processor’s details are mostly
hidden from the embedded software engineer. But at some point, embedded software has to
be written that will directly interface with hardware. This chapter focuses on that interface
between hardware and embedded software.
In the ideal world, hardware can be changed and modified up to the last minute just like
software can. But that is obviously not reality. Co-development tools and techniques allow
embedded software to run on simulated hardware (either simulated in software on a
computer or simulated on FPGAs) before it is locked in; eventually the embedded software
must run on actual hardware. When problems occur on real hardware, the challenge then
becomes how to determine whether the problem is in hardware or software, and then how
to resolve the problem. At that point, the pressure is on the embedded software engineers to
generate a fix or workaround in embedded software. As Jack Ganssle humorously stated,
“Quality is firmware’s fault � because it is too late to fix it in hardware”. (“Firmware” and
“embedded software” are generally the same thing and can be used interchangeably.)
This chapter will discuss ways to eliminate defects and to mitigate the errors that do creep
in. It will call out potential problems that the embedded software engineer needs to be
aware of when accessing hardware.
Occasionally the design of the hardware is such that it is cumbersome for the embedded
software to interface with. Most of the time, the embedded software engineer is stuck
with that design because the hardware is an off-the-shelf part or is already cast in silicon.
But if there is an opportunity for the software team to make design recommendations to
the hardware team, do it. This chapter will discuss some of those recommended hardware
design practices in the form of Hardware Best Practices. These are part of a collection
of 300 best practices published in Hardware/Firmware Interface Design: Best Practices for
Improving Embedded Systems Development, written by Gary Stringham and published by
Elsevier. Those 300 best practices are available as a spreadsheet to purchasers of this
software engineering book. In this chapter, references to “Hardware Best Practice x.y.z”
refers to one of those 300 best practices where x.y.z is the number of that best practice in
the spreadsheet and in the Hardware/Firmware Interface Design book.
I will follow the same pattern for “Embedded Software Best Practices” though I won’t
number them.
This chapter will cover the following topics:
• Collaborating with the hardware team
156 Chapter 6
• Useful hardware design aspects
• Supporting multiple versions of hardware
• Difficult hardware interactions
• Testing and troubleshooting.
Collaborate with the hardware team
A successful embedded systems product requires the successful collaboration of different
teams, including the hardware team and the embedded software team. However,
collaboration between those two teams does not come naturally.
The two teams have different tool sets, life-cycles, cultures, and vocabularies. They may
be in different buildings, different geographical locations, or even different companies. But
I have also heard from engineers that even in a small company, where the few hardware
and software engineers are in the same room, even they don’t collaborate very well.
Because of the lead times required to build hardware, collaboration is further complicated
by the different timing. The hardware team often has their design frozen before the software
team starts up. In order for the software team to have an influence on the hardware design,
they must start early, even before they really have much to do yet.
Proactive collaboration
Early in my time as an embedded software engineer in Hewlett-Packard’s LaserJet design
lab, we, the embedded software team, would be developing device drivers for one ASIC
while the hardware team was designing the next-generation ASIC. We were on the same
floor, just a few hundred feet from each other. But we didn’t talk to each other very much
back then. Those of us on the embedded software team would occasionally complain to the
hardware team for the lousy hardware design we were forced to work with. But then the
hardware team would complain that we were too busy to talk to them back when they were
designing that ASIC. We were too busy writing the device drivers for the previous-
generation ASIC.
I quickly learned to make regular visits to my counterparts in the hardware team. I found
out where they were in their design cycle and asked them for copies of their register
documentation when it became available. I took the time to read it, mark it up, and then go
back with questions, comments, and recommendations for changes in the hardware design.
But this was not in my job description � I was busy enough with my work writing device
drivers for the current ASIC that I was not supposed to take time to work with the hardware
team on the next ASIC. But I did it anyway. And it paid off. A year later, when that new
Hardware’s Interface to Embedded Software 157
ASIC landed on my desk, I knew it and I had some of its problems corrected. I was then
able to produce my device drivers in less time.
Embedded Software Best Practice: Initiate contact with the hardware engineer early
in the design of the block to discuss the block, its device driver, and their interactions.
I championed this approach and, as a result, changes were made to the development process
and embedded software became a required signoff item in the various checkpoints of the
hardware design life-cycle. For example, a milestone required that the embedded software
team sign off on the register documentations for all the blocks. This formality then required
those embedded software engineers who write device drivers to read their respective
documentation � it became part of their job description.
Embedded Software Best Practice: Review hardware design documents.
Hardware Best Practice 3.2.5: Make sure that the firmware team is represented in
reviews and signoffs of hardware checkpoints throughout the life-cycle.
The success of adding that formality to the hardware development life-cycle was evident
when a hardware team called a meeting with the embedded software engineers to review
the high-level design of a new ASIC. It resulted in a very productive discussion because the
embedded software engineers knew of necessary ASIC changes that were required that the
hardware team didn’t know of. Since this was very early in the development of the ASIC,
the changes were able to be accommodated.
Ambassadors
In addition to the checkpoints, the LaserJet lab management assigned ambassadors to each
team (though that title was not used). Someone from the software team was assigned to be
the ambassador to the hardware team, to sit in on their meetings, to note any schedule
updates, and to answer any questions they might have. And the same with someone from
the hardware team as ambassador to the software team. This gave each team a point of
contact to the other side and significantly helped the collaborative efforts.
Embedded Software Best Practice: Designate a member of the embedded software
team as ambassador to the hardware team.
Hardware Best Practice 3.1.2: Designate a member of the hardware team as
ambassador to the embedded software team.
Register design tools
One of the biggest challenges in getting hardware and software to work together is making
sure that both sides are working off the same specifications.
158 Chapter 6
Typically, a hardware engineer writes the documentation that specifies what registers are
at what addresses and contain what bits in what location. The hardware engineer then enters
that same information again in the hardware design files. The software engineer reads the
documentation and enters the register and bit information into software files.
That is three times that the register and bit information is entered into something.
That gives three chances for human errors to be entered. Plus there is the chance that the
hardware design changes but the documentation or the software do not. Then when software
is loaded on hardware, things don’t work and time must be spent to figure out why.
Many design teams tried to solve the problem by using automated scripts to keep hardware
and software files in sync. But as is common with in-house tools, it lacked sufficient
support to maintain it, keep it current, and add necessary features.
A few years ago, such tools became available commercially and open source. I call this niche
Register Design Tools. Engineers enter register and bit information into an input file which is
then processed to generate hardware include files, software include files, and documentation
files. If a change is needed, the input file is modified and reprocessed, then the new output files
are deployed. This keeps everybody in sync. Figure 6.1 illustrates this process.
As stated earlier, this is a new market niche that is still evolving and it is still relatively
unknown. So to promote this niche, I have provided a list of commercial and open-source
products. However, products come and go, or are purchased by other companies. So the
following is the list of products that is correct that time of going to press:
• CSRCompilert by Semifore, Inc, commercial, http://www.semifore.com.
• csrGen by Chuck Benz ASIC and FPGA Design, open source, http://asics.chuckbenz.
com/#csrGen_-_generate_verilog_RTL_code_for.
Synthesis,Verification, etc.
Compilers,Debuggers, etc.
Hardware EmbeddedSoftware
Documentation
RegisterDesign Tool
Figure 6.1:Register design tools generate hardware, embedded software, and documentation files.
Hardware’s Interface to Embedded Software 159
• IDesignSpect by Agnisys Inc, commercial, http://agnisys.com/products/ids.
• MRV � Magillem Register View by Magillem, commercial, http://www.magillem.com/
eda/mrv-magillem-register-view.
• Socrates Bitwiset by Duolog Technologies, commercial, http://www.duolog.com/
products/bitwise/.
• SpectaRegt by PDTi, commercial, http://www.productive-eda.com/register-
management/.
• Vregs by Veripool, open source, http://www.veripool.org/wiki/vregs.
If you are working with your hardware teams on the design of ASICs, SoCs, FPGAs, etc.,
encourage them to use one of these tools if they are not currently doing so. It will improve
collaboration efforts.
Hardware Best Practice 5.5.2: Use automated register design tools to generate register
and bit documentation from block design files.
Co-development activities
In contrast to register design tools, co-development tools are very well known and have
several product offerings from several companies. Co-development tools come in a variety
of platforms and features. But the main purpose is to allow embedded software to execute
on simulated hardware. The hardware may be simulated in software, FPGAs, or some other
method. It may be simulated slowly in great detail or faster at a high level. This has the
advantage of allowing software to run before final hardware is made, and even to be able to
find and fix hardware problems before it is too late.
While some of these tools are lacking support and features and are still maturing, using
them judiciously can boost co-development activities. Don’t make major changes all at
once � start with one piece with good potential then test it, deploy it, and add from there.
Hardware Best Practice 3.2.6: Use co-development activities, such as virtual
prototypes, FPGAs, co-simulation, and old hardware to get firmware engineers involved
in developing code and finding and resolving problems before the physical chips arrive.
Chapter 2, Embedded Systems Hardware/Software Co-development. discusses this topic in
more detail.
System integration
When hardware and embedded software are being integrated together as a complete system,
there will be problems. Problems will occur immediately in bringing up the system.
160 Chapter 6
And problems will occur during final test when something goes wrong after a 20-hour
test under specific conditions.
Hardware engineers need to make themselves available as needed to help the software
engineers with system-level integration and testing. Finding the root cause is only
half of the effort. Coming up with a fix or workaround is the other half. If the root cause
is in hardware, then hardware engineers may need to assist in determining a software
workaround to avoid respinning the chip at a cost of a million dollars and a three-month
delay.
Hardware Best Practice 3.3.6: Involve both hardware and firmware engineers
to determine the root cause of complicated defects and to then design a firmware
workaround.
Useful hardware design aspects
In this section I will discuss a few hardware design aspects that make programming easier
for the embedded software engineers. When you read the hardware documentation,
look for these aspects and ask for them if they are not there.
Notification of hardware events
Events occur in hardware that the embedded software needs to be made aware of. Events
can be grouped into two general categories:
1. Software-initiated events: events that are the result of having completed hardware tasks
launched by the embedded software, such as completing the transmission of an
outgoing I/O packet.
2. External events: events that are the result of external triggers, such as an asynchronous
incoming I/O packet.
In either case, the hardware needs to notify the software so that software can take
appropriate action. The following are different ways that hardware can notify embedded
software of an event:
• No notification: this is the worst kind. Software has to guess when it can take the next
step.
• Timed delay: with software-initiated events, software can set a timer to wait a specific
amount of time before taking the next step. If the delay is long (seconds or more) and
response does not need to be precise, software can use the OS timeout support. But if
the timed delay is short, it is very difficult for software to know how much time has
elapsed without hardware support.
Hardware’s Interface to Embedded Software 161
• Status bit: hardware sets a status bit when an event occurs. Software has to check the
bit, polling if necessary, until the event has occurred. A status bit is good if it is a
software-initiated event that is going to occur soon. If not, then software must poll,
tying up bandwidth, until the event occurs.
• Interrupt bit: this is the best way for hardware to notify software of events. This allows
software to tend to other tasks until an event occurs. This works well for external
events and for software-initiated events that will take some time to complete.
In one ASIC block I had to wait a short amount of time after hitting a reset bit before
I could do the next step. There was no status or interrupt bit to let me know when it was
done. I figured out that three times through a busy loop generated enough of a delay.
About three years later on a new-generation product, there was a problem and the engineer
assigned to that printer was trying to figure out why it was not behaving well. He spent a
few months but could not figure out what was causing it. Management finally had to bring
me back to work on it. After two weeks of investigation, I noticed the delay loop and
remembered why I had that. The new generation product had a different CPU resulting
in a faster delay loop � three passes was no longer enough.
Even if I had documented that section of code well (which I hadn’t) it still would have taken
a long time to determine that the symptom was due to the insufficient delay loop. Had there
been a status bit in the hardware, months of engineering effort could have been averted.
Hardware Best Practice 7.1.1: Always provide an indicator to firmware of any event
or condition that firmware needs to know about.
If you have to deal with non-indicative hardware events, do your best with timing delays
but be sure to clearly comment in the code what the issue is to alert future maintainers
of that code.
Launching tasks in hardware
Preferably when software needs to launch a task in hardware, software writes a 1 to a queue
bit which hardware clears when done. (The technical description of a queue bit is R/W1S,
Read/Write 1 Set, in which software must write a 1 to set the bit but cannot clear the bit.) This
is how it is done most of the time. But in some designs, the software sets the bit and then later
software has to clear the bit (an R/W, Read/Write bit). This is dangerous for two reasons.
If the software can clear the bit before hardware has a chance to see it, hardware will not
know to run the task. But hardware is faster than software, right? So it should be able to see
it no matter how fast software is, right? Wrong. I had a case where a state machine in
hardware would occasionally look to see whether the bit was set when it went through that
state. Occasionally that state machine has to service an external event. We discovered
162 Chapter 6
that if that state machine was busy handling that external event, software could set then
clear that bit before the state machine came back. I had to put in a short delay in my code
to ensure that the bit stayed set long enough for hardware to see it under all conditions.
The other danger is software leaves the bit set too long. When the hardware is done with the
task and it sees the bit is set, is it still set from the past time and so it shouldn’t run the task
again? Or has it been cleared and then set again so it should run the task again? Software could
get delayed because of higher-priority tasks causing it to not clear that bit in a timely fashion.
Because of these potential problems, a queue bit should be used to launch a hardware task,
not a read/write bit. A queue bit provides a good handshake between software and hardware.
• Software reads the queue bit.
• If it is zero, software knows it can set the bit to tell the hardware to do the task.
• Once set, software can poll the hardware bit until it clears. Once cleared, software
knows that hardware saw the bit and is executing the task.
• Hardware checks the bit occasionally.
• Once the hardware sees the bit is set, hardware can start executing the task.
• When hardware starts the task, it clears the queue bit.
Hardware Best Practice 8.5.3: Provide a queue bit that firmware must set � and only
hardware can clear � to initiate a task in the block.
If you have a situation where hardware tasks are launched with R/W bits, examine the
situation very carefully for any too short or too long problems that may occur and
document what the software is doing to address them.
Bit field alignment
From the perspective of software and hardware, the position and locations of bit fields
(groups of two or more consecutive bits) in a register usually don’t matter. But since human
beings are involved, it does. It is because we human beings need help in reading and
interpreting bits. By convention, we read and write the contents of registers as a series of
hexadecimal numbers with each character representing four bits.
The following is a 32-bit register with five bit fields, A, B, C, D, and E. Each bit field has
three bits. The contents of this register are also shown.
Bits 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
R/W � � � � � � � � � � � � � � � � � E E E D D D C C C B B B A A AContents 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 1 1 1 0 0 1 0 1
Hex 0 0 0 0 1 4 E 5
Hardware’s Interface to Embedded Software 163
For the purposes of readability by humans, this will be written as an 8-character
hexadecimal number, 0x000014E5. But it is hard to determine from that the contents of bit
field C. However, by adding unused spacing to fill in partial nibble fields, the five fields
can look like this.
The hex number for this register is now 0x00012345, making it much easier to see that
field C, located in the third nibble from the right, contains a 3. In this example, the 3-bit
fields are nibble-aligned. If the bit field is 5 or more bits, it should be byte aligned.
Hardware Best Practice 8.2.5: Place bit fields of 3 to 4 bits nibble-aligned, of 5 to
8 bits byte-aligned, of 9 to 16 bits 16-bit aligned, and so on.
If you are stuck with an alignment like in the first example, maybe you can modify
the print routine to break out the fields for you, such as like this: “E5 1, D5 2, C5 3,
B5 4, A5 5”.
Fixed bit positions
To help software access different versions of hardware with changes in bits in registers,
the hardware design team should follow these best practices:
Hardware Best Practice 8.2.9: Avoid changing bit assignments from one version of
the block to the next.
Hardware Best Practice 8.2.10: Avoid reusing bit positions of deleted bits in an
existing register.
To illustrate, supposed block version A defines bits T, A, and H in bits 0, 1, and 2,
respectively. But for block version B, the H bit is dropped and the C bit is added.
Following the above best practice 8.2.10, the C bit is not put in the same place where H
was. The C bit is put in a previously unused position, bit 3. The software can be set up
to support all four bits, T, A, H, and C, as illustrated in this diagram.
Bits . . . 5 4 3 2 1 0
Block version A . . . � � � H A TBlock version B . . . � � C � A TSoftware Supports . . . � � C H A T
Bits 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
R/W � � � � � � � � � � � � � E E E � D D D � C C C � B B B � A A AContents 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 0 1 0 1
Hex 0 0 0 1 2 3 4 5
164 Chapter 6
In block version A, the software will never read a 1 in position 3 so it won’t ever invoke
the C action. If the software tries to write a 1 in that position in block version A, it is
ignored.
If C had been placed in position 2, software would first have to determine which block
version it is, then switch on whether to handle position 2 as an H or as a C.
Block version number
Chips that have multiple blocks, such as ASICs, SoCs, and FPGAs, typically have a register
that contains the chip version number. This helps software identify which version of chip
is installed so that it can handle any differences between the versions. Differences may
include new features, deleted features, or defects fixed.
These chips often consist of several blocks, such as a USB host block or an MP3 decoder.
When a new version of the chip is released, the chip-level version number changes,
even though some of the blocks did not change. This forces all device drivers to update
their database, even if their respective block didn’t change.
A better solution is to give each block its own version register. Then only those device
drivers for blocks that changed need to be updated.
For example, suppose the USB block changed but the MP3 block did not. A new chip with
the new USB version would have a new chip version number because something on the
chip is different. The USB block version number would change but the MP3 block would
not change. No change is necessary for the MP3 driver because it already recognizes the
MP3 version number.
This is especially beneficial in FPGA environments when the contents of the FPGA can
change frequently. The overall FPGA version number will keep changing for each new mix
but those blocks that have not changed don’t need their device drivers updated.
Hardware Best Practice 8.4.6: Provide block-level ID and version registers for each
block on a chip.
Debug hooks
As is well known, it is difficult to make perfect designs. When trying to integrate
embedded software with hardware, it can be challenging to know if the root cause of a
problem is located in hardware or software. When trying to locate the root cause, software
has the advantage. Debuggers can be attached to monitor internal variables and execution
paths. Debug statements can be added to the code, which is then re-compiled and re-run
on the hardware to get more information.
Hardware’s Interface to Embedded Software 165
Hardware does not have that luxury. Once cast in silicon, it cannot be changed nor can
internal signals be probed, making it a black box that forces engineers to intuit what might
be going on. It is especially difficult for software engineers since they know very little of
how things are supposed to work inside the hardware. (This is where collaboration with the
hardware engineers is helpful.)
Debug hooks built into the hardware and left there can become very useful to help diagnose
problems. Hooks consist of extra bits and registers solely for the purpose of providing
additional support to software. But some will raise the issue that these hooks take up silicon
space. That is true, though generally very, very little space. Plus, adding hooks is like
buying insurance for your car. You don’t plan on getting into an accident just so you can
collect on the insurance. But if you do get into one, you will be glad you have insurance.
Same with debug hooks. If there are problems, you will be glad you have them.
Hardware Best Practice 11.1.1: Allocate silicon space for test and debug hooks.
The following are just a few possible hooks:
• Internal registers: provide read access to many key registers (or any bank of flip-flops).
• State machine state: provides read access to the current state of state machines. Reading
the state machine register multiple times can reveal if the state machine is stuck or
moving along fine.
• Signals: provide read access to key internal signals. Several signals could be grouped
into one register.
• I/O signals: provide read access to I/O signals. This helps diagnose discreet signals
and communication protocols.
• DMA controller registers: provide read access to the address and byte-count registers
of DMA controllers. Multiple successive reads can reveal if data is flowing or if data is
stuck somehow.
Think about past challenges you have had in trying to integrate software with hardware.
Ask yourself what information from within the hardware would have been helpful during
the integration. Then talk to the hardware engineers about adding those as debug hooks.
Another very useful debugging hook is to have one or more GPIO pins available for
debugging purposes. They come in handy to look for timing problems, activity levels,
occurrences of rare events, and other uses.
Hardware Best Practice 11.5.2: Provide extra unassigned GPIO pins to permit
debugging and last-minute fixes.
Again, these hooks are not only for finding problems in hardware, but also for finding
problems in software. Reading the DMA registers will help ensure that the software wrote
the correct values to them.
166 Chapter 6
Supporting multiple versions of hardware
I worked on the LaserJet printer product line for many years and my software code had to
support many different versions of hardware. I had to support large, medium, and small LaserJet
printers, color and monochrome printers, and single-function and multi-function printers. In
addition, I had to support old and new printers. It would have been prohibitive to support a
different version of the embedded software code for each and every product that we sold.
I strived to have one version of the software code support multiple versions of printers. This has
the advantage in that if a defect is found and fixed in the code, it is fixed for all products using
that code. If a new feature is added for one product, it is then available for all products that need
that new feature. And it was easy to see where differences exist between hardware versions.
Eventually, I was able to get to the point where an easy port to a new LaserJet printer only
took one hour.
Embedded Software Best Practice: Maintain a common firmware code base that
supports multiple versions of hardware platforms.
Code that supports multiple hardware versions uses switches to handle differences between
versions. I will be discussing four types of switches:
• Compile-time switches
• Build-time switches
• Run-time switches
• Self-adapting switches.
Compile-time switches
Compile time switches uses the C Preprocessor (CPP) directives, #define, #undef, #if,
#endif, etc. This can be used if the code does not need to switch at run time. And it should
only be used if the differences are small.
Note: there are those who strongly favor avoiding CPP directives in favor of alternative
methods, such as using const var instead of #define VAR to define constants. Both methods
have pros and cons which I will not get into here.
Use #define to specify constants values that apply to one particular model. The #define directive
is often used when a constant is needed in more than one location in the code. But it can also
be used if it is only needed once. Even though it is used only once, using #define allows
consolidation of all version-specific constants in one place, simplifying the porting effort.
In this discussion, I will use as an example a hypothetical automobile dashboard controller
module. The software in this module needs to work with a few different SoCs, a few
Hardware’s Interface to Embedded Software 167
different types of display panels, and be used in a few different models of automobiles that
the company produces. Ideally the same software will work in all cases and be able to
handle the differences as necessary.
Suppose the display panels use stepper motors to position the needle for the speedometer. The
software positions the needle by giving the stepper motor a value and the stepper motor moves to
that location. But it is not as simple as telling the stepper motor to position the needle at 55 mph.
Maybe it needs to tell the stepper motor to move to position number 220. In other words, the unit
of the stepper motor is not necessarily 1 unit per mph unit. And one type of display might differ
from another in the stepper motor units. Figure 6.2 shows a speedometer and calls out two
important numbers, Units At Zero (what value to give to the stepper motor to put the needle at 0)
and Units Per Ten (how many stepper motor units it takes to move the needle for every 10 mph).
Table 6.1 shows two hypothetical speedometers with their respective values, along with the
calculated values for 55 and 88.
The formula for calculating the stepper motor value is as follows:
Stepper5Speed�UPT
101UAZ
130120
110
100
90807060
50
40
3020
100
140130
Units AtZero
Units PerTen
Figure 6.2:A hypothetical speedometer with constants to position the needle.
Table 6.1: Values for two speedometer models.
Details
Speedometer
ABC VRM
Units at zero (UAZ) 65 450Units per ten (UPT) 100 2 75
Stepper for 55 (to show mph) 615 38Stepper for 88 (to show kph) 945 2 210
168 Chapter 6
If the vehicle is traveling at 55 mph, then the controller module would want to move the
needle to 55. It uses the right UAZ and UPT values for whichever model is being used
and comes up with either 615 or 38 and instructs the stepper motor to move the needle to
that position. If, however, the driver had put the dashboard into metric units, then the speed
would be 88 kph; 88 would be plugged into the formula and either 945 or 2210 would be
the value to give to the stepper motor.
Listing 6.1 shows how a speedometer switch can be used to have one version of the
software code support these variations.
Notice the #else clause that will cause a compiler warning if neither speedometer model is
specified. This technique is very useful when porting code to a new product to ensure that a
speedometer is specified. It may be that the engineer forgot to specify whether it was the
ABC or the VRM speedometer, or it may be that a new speedometer is now being used and
constants need to be defined for it.
Embedded Software Best Practice: When using #if switches in the C Preprocessor,
use #elif to test for all known cases then include a #else case with a #error to catch
unexpected or incomplete switch branches.
Now suppose there are three cars, with the codenames Potato, Corn, and Carrot, that use
these two speedometer models. Listing 6.2 shows how a car switch is used to keep straight
which car uses which speedometer.
Again notice the #else clause to catch if a car is not specified.
The speedometer stepper motor is not the only aspect that would be different. Suppose that
the VRM speedometer also supports a tachometer. But the ABC speedometer does not.
Any tachometer-specific code would then need to be compiled in for VRM. But rather than
using #ifdef SPEEDOMETER_VRM around tachometer code, it is better to use something
#if defined(SPEEDOMETER_ABC)
# define UNITS_AT_ZERO 65
# define UNITS_PER_TEN 100
#elif defined(SPEEDOMETER_VRM)
# define UNITS_AT_ZERO 450
# define UNITS_PER_TEN -75
#else
# error Unknown speedometer model
#endif
Listing 6.1:Speedometer switch: set-up constants based on speedometer model used.
Hardware’s Interface to Embedded Software 169
like #ifdef TACHOMETER. The #define TACHOMETER would then be placed in the
#if SPEEDOMETER_VRM switch of the speedometer switch section. This allows a new
speedometer type that also supports a tachometer to simply turn that on.
Suppose also that a fuel computer is a feature on some models but not all. Marketing
would determine whether a car should have the fuel computer feature. Somewhere on the
dashboard is support for the fuel computer if enabled. Another #define in the car switch
would be used to specify if the fuel computer feature should be turned on. Listing 6.3 now
shows the tachometer and fuel computer aspects added in.
/*** List of cars ***/
#if defined(CAR_POTATO)
# define SPEEDOMETER_ABC
#elif defined(CAR_CORN)
# define SPEEDOMETER_VRM
#elif defined(CAR_CARROT)
# define SPEEDOMETER_VRM
# define FUEL_COMPUTER // Only the Carrot gets the fuel computer
#else
# error Unknown car
#endif
/*** List of speedometers ***/
#if defined(SPEEDOMETER_ABC)
# define UNITS_AT_ZERO 65
# define UNITS_PER_TEN 100
#elif defined(SPEEDOMETER_VRM)
# define UNITS_AT_ZERO 450
# define UNITS_PER_TEN -75
# define TACHOMETER // Only VRM can support tachometer feature
#else
# error Unknown speedometer model
#endif
Listing 6.3:Car and speedometer switch with tachometer and fuel computer added.
#if defined(CAR_POTATO)
# define SPEEDOMETER_ABC
#elif defined(CAR_CORN)
# define SPEEDOMETER_VRM
#elif defined(CAR_CARROT)
# define SPEEDOMETER_VRM
#else
# error Unknown car
#endif
Listing 6.2:Car switch: specify speedometer model used by each car.
170 Chapter 6
Any references to a car or speedometer are only mentioned here in this section
of code. This section makes necessary #defines that are used elsewhere in the code
as needed. This is important to keep the code clean. For example, it gets cumbersome
to have #ifdef CAR_, car_model. around the tachometer code. This is what will
happen:
#if defined(CAR_CORN) || defined(CAR_CARROT) || <...list of all cars...>
tachometer code...
#endif
The problem is that as new cars are added, the list gets long and difficult to
keep straight.
Now, let’s add a new car, Peas. Peas comes with a new speedometer model, the XLS,
but no new features beyond that. In other words, all necessary code support is in place.
The code section containing the car and speedometer switches is the only place that needs
to be changed to support a Peas, as shown in Listing 6.4.
This is all the changes needed to now add support for Peas. It uses the XLS speedometer
which supports the tachometer, and the fuel computer is enabled. No other changes are
necessary elsewhere in the code.
Tables 6.2 and 6.3 give a clearer picture of the details for the above car and speedometer
switches.
One piece remains, and that is to specify which car to build the code for. One option is on
the command line for the compiler.
cc . . . �DCAR_PEAS . . .
Another option is pointing to a #include file located in the Peas directory, and the file
contains #define CAR_PEAS.
Cc . . . �I/products/peas/inc . . .
Again, this technique should not be used to control large chunks of source code � build-
time switches would handle that better.
Build-time switches
Suppose that the differences between the above-mentioned speedometers are not as simple
as a few constants and features, and that the software code required to access them is quite
different. What is needed are separate subroutines to handle the differences. Subroutines
Hardware’s Interface to Embedded Software 171
Table 6.2: Car details.
Details
Car
Potato Corn Carrot Peas
Speedometer ABC VRM VRM XLSFuel computer No No Yes Yes
/*** List of cars ***/
#if defined(CAR_POTATO)
# define SPEEDOMETER_ABC
#elif defined(CAR_CORN)
# define SPEEDOMETER_VRM
#elif defined(CAR_CARROT)
# define SPEEDOMETER_VRM
# define FUEL_COMPUTER
#elif defined(CAR_PEAS) // New car
# define SPEEDOMETER_VRM // Uses new speedometer
# define FUEL_COMPUTER // And gets the fuel computer
#else
# error Unknown car
#endif
/*** List of speedometers ***/
#if defined(SPEEDOMETER_ABC)
# define UNITS_AT_ZERO 65
# define UNITS_PER_TEN 100
#elif defined(SPEEDOMETER_VRM)
# define UNITS_AT_ZERO 450
# define UNITS_PER_TEN -75
# define TACHOMETER
#elif defined(SPEEDOMETER_XLS) // New speedometer
# define UNITS_AT_ZERO 32 // With its constants
# define UNITS_PER_TEN 80
# define TACHOMETER // It supports the tachometer
#else
# error Unknown speedometer model
#endif
Listing 6.4:Adding support for the Peas and its new XLS speedometer.
172 Chapter 6
can be large and it is not a good idea to use #ifdef/#endif to switch them in and out.
Instead, let’s create three files, one for each speedometer:
• speedometer_abc.c
• speedometer_vrm.c
• speedometer_xls.c
Each file will have at least these two functions:
• DisplaySpeed (int speed)
• DisplayTachometer (int rpm).
The main code, when the car is traveling at 55 mph, will call DisplaySpeed (55). Or if it
is in metric mode, and thus going 88 kph, it will call DisplaySpeed (88). Then whichever
function is built into the code, ABC, VRM, or XLS, it will do whatever is necessary to
move the needle to the desired position.
To display 2700 rpm (revolutions per minute) on the tachometer, the main code will call
DisplayTachometer (2700). For VRM and XLS, they will respond appropriately and
display it. For ABC, which does not support a tachometer, it simply returns to the main
code saying “Done” even though it really didn’t do anything.
The main code does not need to worry about which speedometer is attached. It simply
makes the call and whichever code is linked in will be the one to respond.
To handle the fuel computer feature if needed, a separate source code file, fuel_computer.c,
would be linked into the code.
Listing 6.5 shows how the code for the four cars could be built.
Note that the appropriate speedometer is listed and that fuel computer support is included
only when needed.
Of course there are other ways to accomplish this build-time switching, depending on
your build environment. But the point is that code that is only needed some of the time
is contained in separate files that are included in the build only when needed.
Table 6.3: Speedometer details.
Details
Speedometer
ABC VRM XLS
Units at zero (UAZ) 65 450 32Units per ten (UPT) 100 275 80Tachometer No Yes Yes
Hardware’s Interface to Embedded Software 173
Run-time switches
Now let’s suppose that the dashboard controller module, including its embedded software,
needs to be identical for all cars. In other words, run-time switching is necessary to
accommodate the different speedometers and other optional components. This requires that
the necessary information about all supported devices must be included in the code.
Listing 6.6 shows how this is to be done. Section 1 contains two enum specifications, one
for cars and one for speedometers. Section 2 contains the car and speedometer tables with
the pertinent details. Section 3 makes a function call to get the car model and, using the
structs, gets the necessary constants. Section 4 makes a function call to get the current
speed and, using the constants, calculates the necessary stepper motor value.
Using arrays as above is not necessarily the best way to write the code. Using pointers is a
more common way of doing it. The following shows section 3 rewritten to use pointers.
The necessary struct and table changes are not shown.
/* Section 3: Get specific details for this car model */carStruct *pCar5getCarStruct(); /* Get pointer for this car */int uaz5pCar-.pSpeedometer-.units_at_zero;int upt5pCar-.pSpeedometer-.units_per_ten;
If separate functions are needed, then appropriate functions would be selected, as illustrated
here.
pCar-.pSpeedometer-.DisplaySpeed(55);
Self-adapting switches
In the previous examples, human beings have to write the necessary constants into the code.
That is prone to errors. Another approach is to build the constants into the speedometer and
potato:
cc main.c speedometer_abc.c –o potato.exe
corn:
cc main.c speedometer_vrm.c –o corn.exe
carrot:
cc main.c speedometer_vrm.c fuel_computer.c –o carrot.exe
peas:
cc main.c speedometer_xls.c fuel_computer.c –o peas.exe
Listing 6.5:Portions of the makefile for car code.
174 Chapter 6
have the code query the speedometer to get the values. This way, no matter which
(old or new) speedometer is plugged in, the code can adapt.
While it might not be needed in this speedometer example, it might be useful in
situations where there might be slight variations from unit to unit. Instead of it being 65
for units at zero for the ABC speedometer, a calibration might put one unit at 64 and
/* Section 1: Set up enums */
enum cars {potato,
corn,
carrot,
peas};
enum speedometers {abc,
vrm,
xls};
/* Section 2: Set up tables */
struct speedometerStruct {int units_at_zero;
int units_per_ten;
boolean support_tach;
} speedometerStructs [] =
{{ 65, 100, false}, /* ABC */
{450, -75, true}, /* VRM */
{ 32, 80, true}}; /* XLS */
struct carStruct {enum speedometers speedometer;
boolean fuelComputer;
} carStructs [] =
{{abc, false}, /* Potato */
{vrm, false}, /* Corn */
{vrm, true}, /* Carrot */
{xls, true}}; /* Peas */
/* Section 3: Get specific details for this car model */
enum cars car = getCarModel(); /* What car is this controller installed on? */
int uaz = speedometerStructs[carStructs[car].speedometer].units_at_zero;
int upt = speedometerStructs[carStructs[car].speedometer].units_per_ten;
/* Section 4: Get the current speed, calculate the stepper motor value,
and set the stepper motor */
int speed = getCurrentSpeed();
stepperValue = speed * upt / 10 + uaz;
setSpeedometerStepper (stepperValue);
Listing 6.6:Run-time support for dashboard.
Hardware’s Interface to Embedded Software 175
another one at 66. A more precise display of the speed is then available by having
hardware provide these constants.
int uaz5GetSpeedometerUnitsAtZero();int upt5GetSpeedometerUnitsPerTen();stepperValue5speed * upt / 101uaz;setSpeedometerStepper (stepperValue);
As can be seen, there are several different ways that one code base can support multiple
products and components. They each have their pros and cons. Use the approach that makes
the code more maintainable.
Embedded Software Best Practice: use switches so that one embedded software code
base can support multiple types and versions of hardware.
The hard part may be to determine what should or should not be a switch. This is
something that requires looking back at previous products and/or waiting to see what future
products look like. But as support for more products is added, it will be fairly easy to make
that determination.
Difficult hardware interactions
This next section will go over some common interactions with hardware that have the
potential to be dangerous if not carefully handled. Problems in these areas could result in
some very tedious debugging sessions.
Atomic register access
Most registers in a chip are accessed by only one device driver or one thread of execution.
A few registers, such as GPIO and global interrupt enable registers, are likely to be
modified by more than one thread. Registers, especially those shared by multiple threads,
are modified by first reading the current contents of the register, modifying the desired bit,
then writing the modified contents back out to the register.
Problems occur when one thread modifying the contents of the register is interrupted by
another that also wants to modify the register. Figure 6.3 illustrates this. Thread A reads the
register which has 0xBED. Thread A ORs it with 03 400, which yields 0xFED. But, before
Thread A can write it back out to the register, Thread B interrupts. It reads the register and
gets 0xBED. It then ANDs it with B03 040 to get 0xBAD, which it then writes out to the
register and then exits. Thread A is allowed to resume; however, its copy of the register
is now out of date but it doesn’t know that. Its next step is to write its modified, out-of-date
copy to the register, which results in overwriting the changes that Thread B made.
176 Chapter 6
This nasty condition is rare. The timing has to be just right for it to happen. But it will
happen � eventually. And when it does, the side effect of overwriting the interrupting
thread’s changes could be widely varied, making it difficult to identify this problem.
There is no safe way to resolve this problem in software. The best way is to temporarily
disable interrupts around the read-modify-write portion of the code as seen in Listing 6.7.
Sometimes registers, such as memory-mapped registers, are accessed using pointers as if
they were pointing to a regular memory location. The three-line code can then be written as
one line, as seen in Listing 6.8.
Thread A
Read;
Read;
Write;
OR 0×400 => 0×FED;
AND ~0×400 => 0×BAD;
0×BED
0×BED
0×BAD
0×FEDWrite;
Thread BRegister
Figure 6.3:Driver B’s changes will be overwritten if it interrupts Thread A mid-task.
disableInterrupts ();
*pReg |= 0x400; // Set the desired bit
enableInterrupts ();
Listing 6.8:Even this one-liner needs to have interrupts disabled.
disableInterrupts ();
value = readReg (reg); // Get the current register settings
value |= 0x400; // Set the desired bit
writeReg (regA, value); // Write it back out
enableInterrupts ();
Listing 6.7:The best (though not perfect) way software can avoid register overwrites.
Hardware’s Interface to Embedded Software 177
It is only one line in C but it translates into several assembly language steps, leaving it
exposed to being interrupted mid-task.
The problem with this approach is that engineers need to ensure that each and every
read-modify-write code for that register has the disable interrupts around it. If one section
does not, the system is still exposed, since any other higher-priority section can still
interrupt it and make a change that will be overwritten when the first thread resumes.
Semaphores would not work if interrupt service routines are involved because they cannot
risk being blocked by a semaphore get() call while servicing the interrupt.
The only foolproof system is to have hardware implement atomic registers. The following
is an example of one. Actually it is two register addresses; one (shown as being at address
0x6000) is used to set desired bits and the other (at address 0x6004) is used to clear desired
bits. Bits can be set and cleared as desired without having to first read the existing contents
or without having to coordinate with any other thread. Either address can be used to read, if
desired, to determine what the current contents are.
Hardware Best Practice 8.5.9: Provide atomic access to registers that more than one
device driver will access.
Mixed bit types in the same register
There are five types of bits that are commonly used in hardware registers:
• Read/Write (R/W): these are common. Software sets and clears the bits by writing
1 s and 0 s to configure the hardware as desired. Software can read these bits to
determine their current setting.
• Read-only (RO): these are also common. Hardware reports conditions and status.
Software can only read these bits, not change them. Writes to them are ignored.
• Write-only (WO): these are not common but hardware engineers should avoid
implementing them if possible. It is hard for software to verify what it wrote out if it
can’t read it back. Preferably, hardware engineers should make them R/W bits instead.
• Interrupt (R/W1C): hardware sets the bit and software clears it by writing a
1 (W1C5Write 1 Clear). This is commonly used for hardware to report interrupt
conditions. Software reads the register to determine which interrupts are pending, then
MSB GPIO Output Register � R/W1S 0x6000, R/W1C 0x6004 LSB
Bits 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
R/W1S � � � � � � � � � � � � � � � � � � � � � � � � H G F E D C B AR/W1C � � � � � � � � � � � � � � � � � � � � � � � � H G F E D C B AReset 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
178 Chapter 6
writes a 1 in one or more of the pending positions to acknowledge (ack) the interrupt.
Writing a 0 does nothing. Software can only clear this bit, not set it.
• Queue (R/W1S): software sets this bit by writing a 1 (W1S5Write 1 Set). Software
uses queue bits to invoke a task in hardware. Hardware will clear the bit at some time
before its task is done. Software can read the bit to see if it is still set. Writing a 0 does
nothing. Software can only set this bit, not clear it.
A register should only have bits of one type. Mixing types in the same register could create
problems for the software engineer if not careful. For example, for R/W bits, software often
reads it, modifies the desired bits leaving the other bits unchanged, then writes it back out.
For interrupt bits, software often reads it then writes one 1 to ack one interrupt but leaves
any other pending bits pending by writing 0 s in all other bit positions.
If R/W bits and interrupt bits are in the same register, then the following could occur. The R/W
operation of read-modify-write is dangerous because any pending interrupts (which returned
a 1 when read) will be erroneously acked (when writing that 1 back out), thereby losing
interrupts. Responding to interrupts will have the software write a 1 in one pending bit position
but then writing 0 everywhere else, which would reset any bits in the R/W locations.
Listing 6.9 shows the extra steps necessary to avoid problems with both R/W and Interrupt
bits in the same register.
Similar steps are required for other combinations of writeable (not read-only) bits.
#define READ_WRITE_BITS 0x0000007F // Which bits are R/W bits
#define INTERRUPT_BITS 0x001F0000 // Which bits are interrupt bits
// Turn on bit 3 in regA
value = readReg (regA); // Get the current register settings
value &= ~REG_A_INTERRUPT_BITS; // Ignore any pending interrupts
value |= 0x8; // Set bit 3
writeReg (regA, value); // Write it back out
// Look for any interrupts
value = readReg (regA); // Get the current register settings
value &= ~REG_A_READ_WRITE_BITS; // Ignore read/write bits
// Look at value and discover that interrupt 18 is pending
// Ack interrupt 18 but no other interrupt that may be pending while
// leaving the read/write bits unchanged
value = readReg (regA); // Get the current register settings
value &= ~REG_A_INTERRUPT_BITS; // Ignore any pending interrupts
value |= 0x40000; // Put in a 1 to ack interrupt 18
writeReg (regA, value); // Ack intr 18, leaving r/w bits the same
Listing 6.9:Extra steps required to handle a register with both R/W bits and interrupt bits.
Hardware’s Interface to Embedded Software 179
Hardware Best Practice 8.2.13: Do not mix different writeable bit types in any
combination in the same register.
Edge vs. level interrupts
Interrupt modules trigger interrupts in one of two ways, edge or level.
Edge-triggered interrupts are triggered when the interrupt module sees an edge on the
incoming signal line, a change from deasserted to the asserted level. Once pending,
software can ack the interrupt, making the interrupt no longer pending, even if the incoming
signal line is still asserted.
Level-triggered interrupts are those where the interrupt module will trigger an interrupt
whenever the incoming signal is asserted. As long as the signal is still asserted, the interrupt
cannot be acked. Attempts to do so will simply re-trigger the interrupt. The software must
first get the incoming signal to clear before it can ack the interrupt. Some incoming signals
last very briefly so that no additional action is required by the software before it acks the
interrupt. But others might require software to take some additional action, such as clear
a buffer or an error condition, before the interrupt can be acked.
Level-triggered interrupts are more difficult to deal with because of the requirement to clear
the incoming signal first. So, ideally, hardware does not implement level-triggered
interrupts, only edge-triggered interrupts.
Hardware Best Practice 9.1.9: Make the interrupt module edge triggered.
Testing and troubleshooting
Most of the time during development is spent on the assumption that things will work. Just
some of the time is spent in handling error conditions. Testing that is done early will only be on
simulated or incomplete platforms. But when near-final embedded software is put on near-final
hardware, the tough stuff starts because it is hard to ensure that every aspect gets tested.
An important part of embedded software is to have the ability to conduct some tests and do
some troubleshooting in a system with very little debugging hardware attached. The most
we had attached during this stage on our LaserJet printers was an RS-232 port. But it gave
us a view into the inner activities of the printer. We used this extensively.
Temporary hooks
Ideally the embedded software should be tested for proper handling of any of hardware’s
possible behavior. Testing normal behavior is easy. But getting the hardware to pretend that
some anomaly has occurred can be tricky if not impossible.
180 Chapter 6
In order to test for proper response by the embedded software, add a temporary hook to
simulate unusual hardware conditions. For example, in the routine that reads hardware’s
current status, do so normally but then on the tenth time through the loop, modify the value
returned from hardware by turning on some error bits, or, in other words, add a temporary
hook which will replace hardware’s real response with a pretend response that indicates the
rare condition. Then you can observe whether the software is responding properly.
The following is a list of potential things to test for with temporary hooks:
• Overflow and underflow conditions by incrementing or decrementing counts by one.
• Put the block in an illegal configuration and test for proper response.
• Replace incoming data packets with bad ones to simulate various error conditions.
• Artificially insert delays to add stress to the system.
Embedded Software Best Practice: Put in temporary test hooks to unit test the device
driver for difficult test cases, such as rare error conditions reported by the block.
It is okay that temporary hooks mess up the system and maybe even crash it. The object
is to test something for proper behavior. Once the section being tested has passed, then it
does not matter what happens to the system. For example, testing for stack underflow
conditions may result in hanging the system because it was shorted something and it is
still waiting.
Be careful, though, to ensure that all temporary hooks added that cause bad behavior to
occur get removed before shipping the final product. Using techniques such as #if
TEMPORARY_CODE or /* TEMPORARY CODE Please Remove */ will help make the temporary code
easy to find.
Embedded Software Best Practice: Mark all temporary hooks in code so that it can be
easily found for removal.
Permanent hooks
Temporary hooks that perturb the system should come out. But there are some testing
and debugging hooks that should stay in permanently. Probably the most powerful tool
I had for troubleshooting LaserJet problems was the permanent hooks in the code.
Permanent hooks should be very light on resources and unobtrusive while running in
normal operation. But if invoked, the permanent hooks could put a load on the system,
potentially causing it to fail. The following are some ideas for permanent hooks:
• Log the last few data packets in a ring buffer that can be dumped to the debugger.
• Log the last few interrupts in a ring buffer.
• Log the order of events from the block, the application, and any other sources.
Hardware’s Interface to Embedded Software 181
• Break into and dump any ring buffers, the software’s variables and structures, and the
current state of the registers in that respective block.
• Ability to poke values in hardware registers.
My permanent hooks were used extensively even months after my device driver was
stable because they kept catching other system-level problems from other modules in the
system.
Embedded Software Best Practice: Design debugging hooks into the device driver,
such as an interrupt and event log, an ability to query software variables, and to peek
and poke hardware registers.
Conclusion
The most important concept from this chapter is collaboration with the hardware team.
If you remember nothing else from this chapter work closely with the hardware team and
many of these issues will be addressed.
I outlined a few hardware design concepts that help the embedded software engineers in
their coding and encouraged the software engineers to visit with their respective hardware
engineers about adding these features. I gave examples on how one version of embedded
software could support multiple versions of hardware and components, and how that makes
the code easier to keep current with new features and bug fixes. I also discussed difficult
hardware interactions to watch out for and some tips on testing and troubleshooting
problems.
Best practices
Again, the Hardware Best Practices listed in this chapter are some of 300 best practices that
come from the book Hardware/Firmware Interface Design: Best Practices for Improving
Embedded Systems Development. Many concepts in this chapter are discussed at length in
that book. A spreadsheet of the 300 best practices from the Hardware/Firmware Interface
book is available to purchasers of this software engineering book.
182 Chapter 6