software engineering for embedded systems || hardware’s interface to embedded software

28
CHAPTER 6 Hardware’s Interface to Embedded Software Gary Stringham Chapter Outline Introduction 156 Collaborate with the hardware team 157 Proactive collaboration 157 Ambassadors 158 Register design tools 158 Co-development activities 160 System integration 160 Useful hardware design aspects 161 Notification 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 167 Compile-time switches 167 Build-time switches 171 Run-time switches 174 Self-adapting switches 174 Difficult hardware interactions 176 Atomic register access 176 Mixed bit types in the same register 178 Edge vs. level interrupts 180 Testing and troubleshooting 180 Temporary hooks 180 Permanent hooks 181 Conclusion 182 Best practices 182 155 Software Engineering for Embedded Systems. DOI: http://dx.doi.org/10.1016/B978-0-12-415917-4.00006-2 © 2013 Elsevier Inc. All rights reserved.

Upload: gary

Post on 09-Dec-2016

227 views

Category:

Documents


11 download

TRANSCRIPT

Page 1: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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.

Page 2: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 3: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

• 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

Page 4: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 5: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 6: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

• 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

Page 7: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 8: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

• 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

Page 9: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 10: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 11: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 12: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 13: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 14: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 15: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 16: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 17: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 18: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 19: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 20: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 21: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 22: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 23: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 24: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 25: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 26: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 27: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

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

Page 28: Software Engineering for Embedded Systems || Hardware’s Interface to Embedded Software

• 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