Making Embedded Systems

Elecia White

Editor

Mike Hendrickson

Editor

Andy Oram

Copyright © 2011 Elecia White

O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://my.safaribooksonline.com). For more information, contact our corporate/institutional sales department: (800) 998-9938 or corporate@oreilly.com.

Nutshell Handbook, the Nutshell Handbook logo, and the O’Reilly logo are registered trademarks of O’Reilly Media, Inc. Making Embedded Systems, the image of a great-eared goatsucker, and related trade dress are trademarks of O’Reilly Media, Inc.

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and O’Reilly Media, Inc., was aware of a trademark claim, the designations have been printed in caps or initial caps.

While every precaution has been taken in the preparation of this book, the publisher and authors assume no responsibility for errors or omissions, or for damages resulting from the use of the information contained herein.

O'Reilly Media

Preface

I love embedded systems. The first time a motor turned because I told it to, I was hooked. I quickly moved away from pure software and into a field where I can touch the world. Just as I was leaving software, the seminal work was done on design patterns.[1] My team went through the book, discussing the patterns and where we’d consider using them. As I got more into embedded systems, I found compilers that couldn’t handle C++ inheritance, processors with absurdly small amounts of memory in which to implement the patterns, and a whole new set of problems where design patterns didn’t seem applicable. But I never forgot the idea that there are patterns to the way we do engineering. By learning to recognize the patterns, we can use the robust solutions over and over. So much of this book looks at standard patterns and offers some new ones for embedded system development. I’ve also filled in a number of chapters with other useful information not found in most books.

About This Book

After seeing embedded systems in medical devices, race cars, airplanes, children’s toys, and gunshot location systems, I’ve found a lot of commonalities. There are a lot of things I wish I knew then on how to go about designing and implementing software for an embedded system. This book contains some of what I’ve learned. It is a book about good software design in resource-constrained environments.

It is also a book about understanding what interviewers look for when you apply for an embedded systems job. Each section ends with an interview question. These are generally not language specific; instead, they attempt to divine how you think. Good interview questions don’t have a single correct answer. Instead of trying to document all the paths, the notes after each question provide hints about what an interviewer might look for in your response. You’ll have to get the job (and the answers) on your own merits.

One note, though: my embedded systems don’t have operating systems. The software runs on the bare metal. When the software says “turn that light on,” it says it to the processor without an intermediary. This isn’t a book about an embedded operating system (OS). But the concepts translate to processors running OSs, so if you stick around, you may learn about the undersides of OSs too. Working without one helps you really appreciate what an OS does.

This book describes the archetypes and principles that are commonly used in creating embedded system software. I don’t cover any particular platform, processor, compiler, or language, because if you get a good foundation from this book, specifics can come later.

About the Author

In the field of embedded systems, I have worked on DNA scanners, inertial measurement units for airplanes and race cars, toys for preschoolers, a gunshot location system for catching criminals, and assorted medical and consumer devices.

I have specialized in signal processing, hardware integration, complex system design, and performance. Having been through FAA and FDA certification processes, I understand the importance of producing high-quality designs and how they lead to high-quality implementations.

I’ve spent several years in management roles, but I enjoy hands-on engineering and the thrill of delivering excellent products. I’m happy to say that leaving management has not decreased my opportunities to provide leadership and mentoring.

Acknowledgments

This book didn’t happen in a vacuum. It started with a colleague who said, “Hey, do you know a book I can give to one of my junior engineers?” From that planted seed came months of writing; I learned to really appreciate understanding (and encouraging) friends. Then there were the engineers who gave their time to look at the technical material (any remaining issues are my fault, not theirs, of course). Finally, O’Reilly provided tremendous support through the whole process.

Thanking each person as they properly deserve would take pages and pages, so I will just go through them all in one breath, in no particular order: Phillip King, Ken Brown, Jerry Ryle, Matthew Hughes, Eric Angell, Scott Fitzgerald, John Catsoulis, Robert P. J. Day, Rebecca Demarest, and Jen Costillo. These folks made a difference in the flavor of this book. There are additional thank-yous spread throughout the book, where I got help from a particular person on a particular area, so you may see these names again (or a few different ones).

Two people are left out of that list. Andy Oram is an excellent editor at O’Reilly, and I was lucky to get him. He’s made a big difference to this book and to my writing. Finally, authors always give gushing thanks to their spouses; it is a cliché. However, having written a book, I see why. Christopher White, my favorite drummer, physicist, and embedded systems engineer, thank you most of all. For everything.

[1] Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides, 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley.

Organization of This Book

I read nonfiction for amusement. I read a lot more fiction than nonfiction, but still, I like any good book. I wrote this book to be read almost as a story, from cover to cover. The information is technical (extremely so in spots), but the presentation is casual. You don’t need to program along with it to get the material (though trying out the examples and applying the recommendations to your code will give you a deeper understanding).

This isn’t intended to be a technical manual where you can skip into the middle and read only what you want. I mean, you can do that, but you’ll miss a lot of information with the search-and-destroy method. You’ll also miss the jokes, which is what I really would feel bad about. I hope that you go through the book in order. Then, when you are hip-deep in alligators and need to implement a function fast, pick up the book, flip to the right chapter, and, like a wizard, whip up a command table or fixed point implementation of variance.

Or you can skip around, reading about solutions to your crisis of the week. I understand. Sometimes you just have to solve the problem. If that is the case, I hope you find the chapter interesting enough to come back when you are done fighting that fire.

The order of chapters is:

Chapter 1, Introduction

What is an embedded system? How is development different from traditional software?

Chapter 2, Creating a System Architecture

How to create (and document) a system architecture.

Chapter 3, Getting Your Hands on the Hardware

Hardware/software integration during board bring-up can be scary, but there are some ways to make it smoother.

Chapter 4, Outputs, Inputs, and Timers

The embedded systems version of “Hello World” is making an LED blink. It can be more complex than you might expect.

Chapter 5, Managing the Flow of Activity

This chapter describes how to set up your system, where to use interrupts (and how not to), and how to make a state machine.

Chapter 6, Communicating with Peripherals

Different serial communication forms rule embedded systems (UART, SSP, SPI, I²C, USB, etc.). Networking, bit-bang, and parallel buses are not to be discounted.

Chapter 7, Updating Code

When you need to replace the program running in your processor, you have a few options ranging from internal updaters to building your own solution.

Chapter 8, Doing More with Less

This covers methods for reducing consumption of RAM, code space, and processor cycles.

Chapter 9, Math

Most embedded systems need to do some form of analysis. Understanding how mathematical operations and floating points work (and don’t work) will make your system faster.

Chapter 10, Reducing Power Consumption

From reducing processor cycles to system architecture suggestions, this chapter will help you if your system runs on batteries.

The information is presented in the order that I want my engineers to start thinking about these things. It may seem odd that architecture is first, considering that most people don’t get to it until later in their careers. However, I want the people I work with to be thinking about how their code fits in the system long before I want them to worry about optimization.

Conventions Used in This Book

Typography

The following typographical conventions are used in this book:

Italic

Indicates new terms, URLs, filenames, and file extensions.

Constant width

Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, data types, and keywords.

Tip

This icon signifies a tip, suggestion, or general note.

Warning

This icon indicates a warning or caution.

Terminology

A microcontroller is a processor with onboard goodies like RAM, code space (usually flash), and various peripheral interfaces (e.g., I/O lines). Your code runs on a processor, or central processing unit (CPU). A microprocessor is a small processor, but the definition of “small” changes.

A DSP (digital signal processor) is a specialized form of microcontroller that focuses on signal processing, usually sampling analog signals and doing something interesting with the result. Usually a DSP is also a microcontroller, but it has special tweaks to make it perform math operations faster (in particular, multiply and add).

As I wrote this book, I wanted to use the correct terminology so you’d get used to it. However, with so many names for the piece of the system that is running your code, I didn’t want to add confusion by changing the name. So, I stick with the term processor to represent whatever it is you’re using to implement your system. Most of the material is applicable to whatever you actually have.

Using Code Examples

This book is here to help you get your job done. In general, you may use the code in this book in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing a CD-ROM of examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.

We appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Making Embedded Systems by Elecia White (O’Reilly). Copyright 2012 Elecia White, 978-1-449-30214-6.”

If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.

Safari® Books Online

Note

Safari Books Online is an on-demand digital library that lets you easily search over 7,500 technology and creative reference books and videos to find the answers you need quickly.

With a subscription, you can read any page and watch any video from our library online. Read books on your cell phone and mobile devices. Access new titles before they are available for print, and get exclusive access to manuscripts in development and post feedback for the authors. Copy and paste code samples, organize your favorites, download chapters, bookmark key sections, create notes, print out pages, and benefit from tons of other time-saving features.

O’Reilly Media has uploaded this book to the Safari Books Online service. To have full digital access to this book and others on similar topics from O’Reilly and other publishers, sign up for free at http://my.safaribooksonline.com.

How to Contact Us

Please address comments and questions concerning this book to the publisher:

O’Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (in the United States or Canada)

707-829-0515 (international or local)

707-829-0104 (fax)

We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at:

http://shop.oreilly.com/product/0636920017776.do

To comment or ask technical questions about this book, send email to:

bookquestions@oreilly.com

For more information about our books, conferences, Resource Centers, and the O’Reilly Network, see our website at:

http://www.oreilly.com

Chapter 1. Introduction

Embedded systems are different things to different people. To someone who has been working on servers, an application developed for a phone is an embedded system. To someone who has written code for tiny 8-bit microprocessors, anything with an operating system doesn’t seem very embedded. I tend to tell nontechnical people that embedded systems are things like microwaves and automobiles that run software but aren’t computers. (Most people recognize a computer as a general-purpose device.) Perhaps an easy way to define the term without haggling over technology is:

An embedded system is a computerized system that is purpose-built for its application.

Because its mission is narrower than a general-purpose computer, an embedded system has less support for things that are unrelated to accomplishing the job at hand. The hardware often has constraints. For instance, consider a CPU that runs more slowly to save battery power, a system that uses less memory so it can be manufactured more cheaply, and processors that come only in certain speeds or support a subset of peripherals.

The hardware isn’t the only part of the system with constraints. In some systems, the software must act deterministically (exactly the same each time) or in real time (always reacting to an event fast enough). Some systems require that the software be fault-tolerant with graceful degradation in the face of errors. For example, consider a system in which servicing faulty software or broken hardware may be infeasible (e.g., a satellite or a tracking tag on a whale). Other systems require that the software cease operation at the first sign of trouble, often providing clear error messages (for example, a heart monitor should not fail quietly).

Compilers, Languages, and Object-Oriented Programming

Another way to identify embedded systems is that they use cross-compilers. Although a cross-compiler runs on your desktop or laptop computer, it creates code that does not. The cross-compiled image runs on your target embedded system. Because the code needs to run on your processor, the vendor for the target system usually sells a cross-compiler or provides a list of available cross-compilers to choose from. Many larger processors use the cross-compilers from the GNU family of tools.

Embedded software compilers often support only C, or C and C++. In addition, many embedded C++ compilers implement only a subset of the language (multiple inheritance, exceptions, and templates are commonly missing). There is a growing popularity for Java, but the memory management inherent to the language works only on a larger system.

Regardless of the language you need to use in your software, you can practice object-oriented design. The design principles of encapsulation, modularity, and data abstraction can be applied to any application in nearly any language. The goal is to make the design robust, maintainable, and flexible. We should use all the help we can get from the object-oriented camp.

Taken as a whole, an embedded system can be considered equivalent to an object, particularly one that works in a larger system (e.g., a remote control talking to a set-top box, a distributed control system in a factory, an airbag deployment sensor in a car). In the higher level, everything is inherently object-oriented, and it is logical to extend this down into embedded software.

On the other hand, I don’t recommend a strict adherence to all object-oriented design principles. Embedded systems get pulled in too many directions to be able to lay down such a commandment. Once you recognize the trade-offs, you can balance the software design goals and the system design goals.

Most of the examples in this book are in C or C++. I expect that the language is less important than the concepts, so even if you aren’t familiar with the syntax, look at the code. This book won’t teach you any programming language (except for some assembly language), but as I’ve said, good design principles transcend language.

Embedded System Development

Embedded systems are special, offering special challenges to developers. Most embedded software engineers develop a toolkit for dealing with the constraints. Before we can start building yours, let’s look at the difficulties associated with developing an embedded system. Once you become familiar with how your embedded system might be limited, we’ll start on some principles to guide us to better solutions.

Debugging

If you were to debug software running on a computer, you could compile and debug on that computer. The system would have enough resources to run the program and support debugging it at the same time. In fact, the hardware wouldn’t know you were debugging an application, as it is all done in software.

Embedded systems aren’t like that. In addition to a cross-compiler, you’ll need a cross-debugger. The debugger sits on your computer and communicates with the target processor through the special processor interface (see Figure 1-1). The interface is dedicated to letting someone else eavesdrop on the processor as it works. This interface is often called JTAG (pronounced “jay-tag”), regardless of whether it actually implements that widespread standard.

Figure 1-1. Computer and target processor

The processor must expend some of its resources to support the debug interface, allowing the debugger to halt it as it runs and providing the normal sorts of debug information. Supporting debugging operations adds cost to the processor. To keep costs down, some processors support a limited subset of features. For example, adding a breakpoint causes the processor to modify the memory-loaded code to say “stop here.” However, if your code is executing out of flash (or any other sort of read-only memory), instead of modifying the code, the processor has to set an internal register (hardware breakpoint) and compare it at each execution cycle to the address being run, stopping when they match. This can change the timing of the code, leading to annoying bugs that occur only when you are (or maybe aren’t) debugging. Internal registers take up resources, too, so often there are only a limited number of hardware breakpoints available (frequently there are only two).

To sum up, processors support debugging, but not always as much debugging as you are accustomed to if you’re coming from the software world.

The device that communicates between your PC and the processor is generally called an emulator, an in-circuit emulator (ICE), or a JTAG adapter. These may refer (somewhat incorrectly) to the same thing, or they may be three different devices. The emulator is specific to the processor (or processor family), so you can’t take the emulator you got for one project and assume it will work on another. The emulator costs add up, particularly if you collect enough of them or if you have a large team working on your system.

To avoid buying an emulator or dealing with the processor limitations, many embedded systems are designed to have their debugging done primarily via printf or some sort of lighter-weight logging to an otherwise unused communication port. Although incredibly useful, this can also change the timing of the system, possibly leaving some bugs to be revealed only after debugging output is turned off.

Writing software for an embedded system can be tricky, as you have to balance the needs of the system and the constraints of the hardware. Now you’ll need to add another item to your to-do list: making the software debuggable in a somewhat hostile environment.

More Challenges

An embedded system is designed to perform a specific task, cutting out the resources it doesn’t need to accomplish its mission. The resources under consideration include:

Memory (RAM)

Code space (ROM or flash)

Processor cycles or speed

Power consumption (which translates into battery life)

Processor peripherals

To some extent, these are exchangeable. For example, you can trade code space for processor cycles, writing parts of your code to take up more space but run more quickly. Or you might reduce the processor speed in order to decrease power consumption. If you don’t have a particular peripheral interface, you might be able to create it in software with I/O lines and processor cycles. However, even with trading off, you have only a limited supply of each resource. The challenge of resource constraints is one of the most pressing for embedded systems.

Another set of challenges comes from working with the hardware. The added burden of cross-debugging can be frustrating. During board bring-up, the uncertainty of whether a bug is in the hardware or software can make issues difficult to solve. Unlike your computer, the software you write may be able to do actual damage to the hardware. Most of all, you have to know about the hardware and what it is capable of. That knowledge might not be applicable to the next system you work on. You will be challenged to learn quickly.

Once development and testing are finished, the system is manufactured, which is something most pure software engineers never need to consider. However, creating a system that can be manufactured for a reasonable cost is a goal that both embedded software engineers and hardware engineers have to keep in mind. Supporting manufacturing is one way you can make sure that the system that you created gets reproduced with high fidelity.

After manufacture, the units go into the field. With consumer products, that means they go into millions of homes where any bugs you created are enjoyed by many. With medical, aviation, or other critical products, your bugs may be catastrophic (which is why you get to do so much paperwork). With scientific or monitoring equipment, the field could be a place where the unit cannot ever be retrieved (or retrieved only at great risk and expense; consider the devices in volcano calderas), so it had better work. The life your system is going to lead after it leaves you is a challenge you must consider as you design the software.

After you’ve figured out all of these issues and determined how to deal with them for your system, there is still the largest challenge, one common to all branches of engineering: change. Not only do the product goals change, but the needs of the project also change through its lifespan. In the beginning, maybe you want to hack something together just to try it out. As you get more serious and better understand (and define) the goals of the product and the hardware you are using, you start to build more infrastructure to make the software debuggable, robust, and flexible. In the resource-constrained environment, you’ll need to determine how much infrastructure you can afford in terms of development time, RAM, code space, and processor cycles. What you started building initially is not what you will end up with when development is complete. And development is rarely ever complete.

Creating a system that is purpose-built for an application has an unfortunate side effect: the system might not support change as the application morphs. Engineering embedded systems is not just about strict constraints and the eventual life of the system. The challenge is figuring out which of those constraints will be a problem later in product development. You will need to predict the likely course of changes and try to design software flexible enough to accommodate whichever path the application takes. Get out your crystal ball.

Principles to Confront Those Challenges

Embedded systems can seem like a jigsaw puzzle, with pieces that interlock (and only go together one way). Sometimes you can force pieces together, but the resulting picture might not be what is on the box. However, we should jettison the idea of the final result as a single version of code shipped at the end of the project.

Instead, imagine the puzzle has a time dimension that varies over its whole life: conception, prototyping, board bring-up, debugging, testing, release, maintenance, and repeat. Flexibility is not just about what the code can do right now, but also about how the code can handle its lifespan. Our goal is to be flexible enough to meet the product goals while dealing with the resource constraints and other challenges inherent to embedded systems.

There are some excellent principles we can take from software design to make the system more flexible. Using modularity, we separate the functionality into subsystems and hide the data each subsystem uses. With encapsulation, we create interfaces between the subsystems so they don’t know much about each other. Once we have loosely coupled subsystems (or objects, if you prefer), we can change one area of software with confidence that it won’t impact another area. This lets us take apart our system and put it back together a little differently when we need to.

Recognizing where to break up a system into parts takes practice. A good rule of thumb is to consider which parts can change independently. In embedded systems, this is helped by the presence of physical objects that you can consider. If a sensor X talks over a communication channel Y, those are separate things and good candidates for being separate subsystems (and code modules).

If we break things into objects, we can do some testing on them. I’ve had the good fortune of having excellent QA teams for some projects. In others, I’ve had no one standing between my code and the people who were going to use the system. I’ve found that bugs caught before software releases are like gifts. The earlier in the process errors are caught, the cheaper they are to fix and the better it is for everyone.

You don’t have to wait for someone else to give you presents. Testing and quality go hand in hand. Writing test code for your system will make it better, provide some documentation for your code, and make other people think you write great software.

Documenting your code is another way to reduce bugs. It can be difficult to know the level of detail when commenting your code.

i++; // increment the index

No, not like that. Lines like that rarely need comments at all. The goal is to write the comment for someone just like you, looking at the code a year from when you wrote it. By that time, future-you will probably be working on something different and have forgotten exactly what creative solution old-you came up with. Future-you probably doesn’t even remember writing this code, so help yourself out with a bit of orientation (file and function headers). In general, though, assume the reader will have your brains and your general background, so document what the code does, not how it does it.

Finally, with resource-constrained systems, there is the temptation to optimize your code early and often. Fight the urge. Implement the features, make them work, test them out, and then make them smaller or faster as needed.

You have only a limited amount of time: focus on where you can get better results by looking for the bigger resource consumers after you have a working subsystem. It doesn’t do you any good to optimize a function for speed if it runs rarely and is dwarfed by the time spent in another function that runs frequently. To be sure, dealing with the constraints of the system will require some optimization. Just make sure you understand where your resources are being used before you start tuning.

“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.”

--Donald Knuth

Further Reading

There are many excellent references about design patterns. These two are my favorites:

Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley.

There are many excellent references about design patterns, but this was the one that sparked the revolution. Due to its four collaborators, it is often known as the “Gang of Four” book (or a standard design pattern may be noted as a GoF pattern):

Freeman, Eric T., Elisabeth Robson, Bert Bates, and Kathy Sierra. 2004. Head First Design Patterns. Cambridge, MA: O’Reilly Media.

Interview Question: Hello World

Here is a computer with a compiler and an editor. Please implement “hello world.” Once you have the basic version working, add in the functionality to get a name from the command line. Finally, tell me what happens before your code executes—in other words, before the main() function. (Thanks to Phillip King for this question.)

In many embedded systems, you have to develop a system from scratch. With the first part of this task, I look for a candidate who can take a blank slate and fill in the basic functionality, even in an unfamiliar environment. I want him to have enough facility with programming that this question is straightforward.

This is a solid programming question, so you’d better know the languages on your resume. Any of them are fair game for this question. When I ask for a “hello world” implementation, I look for the specifics of a language (that means knowing which header file to include and using command arguments in C and C++). I want the interviewee to have the ability to find and fix syntax errors based on compiler errors (though I am unduly impressed when he can type the whole program without any mistakes, even typos).

Tip

I am a decent typist on my own, but if someone watches over my shoulder, I end up with every other letter wrong. That’s OK, lots of people are like that. So don’t let it throw you off. Just focus on the keyboard and the code, not on your typing skills.

The second half of the question is where we start moving into embedded systems. A pure computer scientist tends to consider the computer as an imaginary ideal box for executing his beautiful algorithms. When asked what happens before the main function, he will tend to answer along the lines of “you know, the program runs,” but with no understanding of what that implies.

However, if he mentions “start” or “cstart,” he is well on his way in the interview. In general, I want him to know that the program requires initialization beyond what we see in the source, no matter what the platform is. I like to hear mention of setting the exception vectors to handle interrupts, initializing critical peripherals, initializing the stack, initializing variables, and if there are any C++ objects, calling creators for those. It is great if he can describe what happens implicitly (by the compiler) and what happens explicitly (in initialization code).

The best answers are step-by-step descriptions of everything that might happen, with an explanation of why they are important and how they happen in an embedded system. An experienced embedded engineer often starts at the vector table, with the reset vector, and moves from there to the power-on behavior of the system. This material is covered later in the book, so if these terms are new to you, don’t worry.

An electrical engineer (EE) who asks this question gives bonus points when a candidate can, during further discussion of power-on behavior, explain why an embedded system can’t be up and running 1 microsecond after the switch is flipped. The EE looks for an understanding of power sequencing, power ramp-up time, clock stabilization time, and processor reset/initialization delay.

Chapter 2. Creating a System Architecture

Even small embedded systems have so many details that it can be difficult to recognize where patterns can be applied. You’ll need a good view of the whole system to understand which pieces have straightforward solutions and which have hidden dependencies. A good design is created by starting with an OK design and then improving on it, ideally before you start implementing it. And a system architecture diagram is a good way to view the system and start designing the software.

Starting a project from scratch can be difficult. A product definition is seldom fixed at the start, so you may go round and round to hammer out ideas. Once the product functions have been sketched out on a whiteboard, you can start to think about the software architecture. The hardware folks are doing the same thing (hopefully in conjunction with you as the software designer, though their focus may be a little different). In short order, you’ll have a software architecture and a draft schematic. Depending on your experience level, the first few projects you design will likely be based on other projects, so the hardware will be based on an existing platform with some changes.

Embedded systems depend heavily on their hardware. Unstable hardware leaves the software looking buggy and unreliable. In this section, we’ll look at creating a system architecture from the general hardware level and going up to the function level. It is possible (and usually preferable) to go the other way, looking at the system functions and determining what hardware is necessary to support them. However, I’m going to focus on starting at the low-level hardware-interfacing software to reinforce the idea that your product depends on the hardware features being stable and accessible.

When you do a bottom-up design as described here, recognize that the hardware you are specifying is archetypal. Although you will eventually need to know the specifics of the hardware, initially accept that there will be some hardware that meets your requirements (i.e., some processor that does everything you need). Use that to work out the system, software, and hardware architectures before diving into details.

Creating System Diagrams

Just as hardware designers create schematics, you should make a series of software block diagrams that show the relationships between the various parts of the software system. Such diagrams will give you a view of the whole system, help you identify dependencies, and provide insight into the design of new features.

I recommend three different diagrams:

Architecture block diagram

Hierarchy of control organization chart

Software layering view

The Block Diagram

Design is straightforward at the start because you are considering the physical components of the system, and you can think in an object-oriented fashion—no matter whether you are using an object-oriented language—to model your software around the physical components. Each chip attached to the processor is an object. You can think of the wires that connect the chip to the processor (the communication methods) as another set of objects.

Start your design by drawing these as boxes where the processor is in the center, the communication objects are in the processor, and each peripheral is attached to a communication object.

For this example, I’m going to introduce a type of memory called flash memory. While the details aren’t important here, it is a relatively inexpensive type of memory used in many devices. Many flash memory chips communicate over the SPI bus, a type of serial communication (discussed in more detail in Chapter 6). Most processors cannot execute code over SPI, so the flash is used for off-processor storage. Our schematic, shown at the top of Figure 2-1, shows that we have some flash memory attached to our processor via SPI. Don’t be intimidated by the schematic! We’ll cover them in more detail in the next chapter.

In our software block diagram, we’ll add the flash as a peripheral (a box outside the processor) and a SPI box inside the processor to show that we’ll need to write some SPI code. Our software diagram looks very similar to the hardware schematic at this stage, but as we identify additional software components, it will diverge.

The next step is to add a flash box inside the processor to indicate that we’ll need to write some flash-specific code. It’s valuable to separate the communication method from the peripheral; if there are multiple chips connected via the same method, they should all go to the same communications block in the chip. The diagram will at that point warn us to be extra careful about sharing that resource, and to consider the performance and resource contention issues that sharing brings.

Figure 2-1 shows a snippet of a schematic and the beginnings of a software block diagram. Note that the schematic is far more detailed. At this point in a design, we want to see the high-level items to determine what objects we’ll need to create and how they all fit together. Keep the detailed pieces in mind (or on a different piece of paper), particularly where the details may have a system impact, but try to keep the details out of the diagram if possible.

Figure 2-1. Comparison of schematic and initial software block diagram

The next stage is to add some higher-level functionality. What is each peripheral chip used for? This is simple when each has only one function. For example, if our flash is used to store bitmaps to put on a screen, we can put a box on the architecture to represent the display assets. This doesn’t have an off-chip component, so its box goes in the processor. We’ll also need boxes for a screen and its communication method, and another box to convey flash-based display assets to the screen. It is better to have too many boxes at this stage than too few. We can combine them later.

Add any other software structures you can think of: databases, buffering systems, command handlers, algorithms, state machines, etc. You might not know exactly what you’ll need for those (we’ll talk about some of them in more detail later in the book), but try to represent everything from the hardware to the product function in the diagram. See Figure 2-2.

After you have sketched out this diagram on a piece of paper or a whiteboard (probably multiple times because the boxes never end up being big enough or in the right place), you may think you’ve done enough. However, another drawing often will give additional insight.

Looking at the various views may show you some hidden, ugly spots with critical bottlenecks, poorly understood requirements, or an innate failure to implement the product’s features on the intended platform. Often these deficiencies can be seen from only one view or are represented by the boxes that change most between the different diagrams. By looking at them from the right perspective, ideally you will identify the tricky modules and also see a path to a good solution.

Figure 2-2. Software block diagram

Hierarchy of Control

The next type of software architecture diagram looks like an organizational chart, as shown in Figure 2-3. It shows discrete components and which components call others. The whole system is a hierarchy, with “Main” at the highest level. If you know how the algorithm is going to use each piece, you can fill in the next level with algorithm-related objects. If you don’t think they are going to change, you can start with the product-related features and then drop down to the pieces we do know, putting the most complex on top. Next, fill in the lower-level objects that are used by a higher-level object. For instance, our SPI object is used by our flash object, which is used by our display assets object, and so on. You may need to add some pieces here, ones you hadn’t thought of before. You should determine whether those pieces need to go on the block diagram, too (probably).

Figure 2-3. Organizational diagram of software architecture

However, as much as we’d like to devote a peripheral to one function (e.g., the display assets in the flash memory), the limitations of the system (cost, speed, etc.) don’t always support that. Often you end up cramming multiple, not particularly compatible functions into one peripheral.

In the diagram, you can see where the text and images share the flash driver and its child SPI driver. This sharing is often necessary, but it is a red flag in the design because you’ll need special care to avoid contention around the resource and make sure it is available when needed. Luckily, this figure shows that the rendering code controls both and will be able to ensure that only one of the resources—text or images—is needed at a time, so a conflict between them is unlikely.

Let’s say that your team has determined that the system we’ve been designing needs each unit to have a serial number. It is to be programmed in manufacturing and communicated upon request. We could add another memory chip as a peripheral, but that would increase cost, board complexity, and the software complexity. The flash we already have is large enough to fit the serial number. This way, only software complexity has to increase.

In Figure 2-4, we print the system serial number through the flash that previously was devoted to the display assets. If the logging subsystem needs to get the serial number asynchronously from the display assets (say I have two threads or my display assets are used in an interrupt), the software will have to avoid collisions and any resulting corruption.

Figure 2-4. Organizational diagram with a shared resource

Each time something like this is added, some little piece where you are using A and B and have to consider a potential interaction with C, the system becomes a little less robust. This added awareness is very hard to document, and shared resources cause pains in the design, implementation, and maintenance phases of the project. The example here is pretty easily fixed with a flag, but all shared resources should make you think about the consequences.

Layered View

The last architecture drawing looks for layers and represents objects by their estimated size, as in Figure 2-5. This is another diagram to start with paper and pencil. Start at the bottom of the page and draw boxes for the things that go off the processor (such as our communication boxes). If you anticipate that one is going to be more complicated to implement, make it a little larger. If you aren’t sure, make them all the same size. Next, add to the diagram the items that use the lowest layer. If there are multiple users of a lower-level object, they should all touch the lower-level object (this might mean making an object bigger). Also, each object that uses something below it should touch all of the things it uses, if possible.

That was a little sneaky. I said to make the object size depend on its complexity. Then I said that if an object has multiple users, make it bigger. As described in the previous section, shared resources increase complexity. So when you have a resource that is shared by many things, its box gets bigger so it can touch all of the upper modules. This reflects an increase in complexity, even if the module seems straightforward. It isn’t only bottom layers that have this problem. In the diagram, I initially had the rendering box much smaller because moving data from the flash to the LCD is easy. However, once the rendering box had to control all the bits and pieces below it, it became larger. And sure enough, on the project that I took this snippet from, ultimately rendering became a relatively large module and then two modules.

Eventually, the layered view shows you where the layers in your code are, letting you bundle groups of resources together if they are always used together. For example, the LCD and parallel I/O boxes touch only each other. If this is the final diagram, maybe those could be combined to make one module. The same goes for the backlight and PWM output.

Also look at the horizontal groupings. Fonts and images share their higher-level and lower-level connections. It’s possible they should be merged into one module because they seem to have the same inputs and outputs. The goal of this diagram is to look for such points and think about the implications of combining the boxes in various ways. You might end up with a simpler design.

Finally, if you have a group of several modules that try to touch the same lower-level item, you might want to take some time to break apart that resource. Would it be useful to have a flash driver that dealt only with the serial number? Maybe one that read the serial number on initialization and then never reread it, so that the display subsystem could keep control of the flash? Understand the complexity of your design and your options for designing the system to modify that complexity. A good design can save time and money in implementation and maintainability.

Figure 2-5. Layered software architecture diagram

From Diagram to Architecture

So now, sitting with three different architecture drawings, where do you go next? Maybe you’ve realized there are a few pieces of code you didn’t think about initially. And maybe you have progressed a bit at figuring out how the modules interact. Before we consider those interactions (interfaces), there is one thing that is really worth spending some time on: what is going to change? At this stage everything is experimental, so it is a good bet that any piece of the system puzzle is going to change.

Given your product requirements, you may be pretty confident about some of the functions of your system. Our example, whatever it does, needs a display, and the best way to send bitmaps to it seems like flash. Many flash chips are SPI, so that seems like a good bet, too. However, exactly which flash chip is used will likely change. The LCD, the image, or font data also may change. Even the way you store the image or font data might change. The boxes in the diagram should represent the Platonic ideals of each thing instead of a specific implementation.

Encapsulate Modules

Our process leads to interfaces that don’t depend specifically on the content or behavior of the modules that they connect (this is encapsulation!). We use the three different architecture drawings to figure out the best places for those interfaces. Each box will probably have its own interface. Maybe those can be mashed together into one object. But is there a good reason to do it now instead of later?

Sometimes, yes. If you can reduce some complexity of your diagrams without giving up too much flexibility in the future, it may be worth collapsing some dependency trees. Here are some things to look for:

In the hierarchy chart, look for objects that are used by only one other object. Are these both fixed? If they are unlikely to change or are likely to change together as you evolve the system, consider combining them. If they change independently, it is not a good area to consider for encapsulation.

In the layered diagram, look for collections of objects that are always used together. Can these be grouped together in a higher-level interface to manage the objects? You’d be creating a hardware abstraction layer.

Which modules have lots of interdependencies? Can they be broken apart and simplified? Or can the dependencies be grouped together?

Can the interfaces between vertical neighbors be described in a few sentences? That makes it a good place for encapsulation to help you create an interface (for others to use your code or just for ease of testing).

In the example system, the LCD is attached to the parallel interface. In every diagram, it is simple, with no additional dependencies and no other subsystem requiring access to the parallel interface. You don’t need to expose the parallel interface; you can encapsulate it within the LCD module.

Conversely, imagine the system without the rendering module to encapsulate the display subsystem. The layered diagram might show this best (Figure 2-5). The fonts, images, LCD, and backlight would all try to touch each other, making a mess of the diagram.

Because each box you’re left with is likely to become a module (or object), look at the drawing. How can you make it simpler? Encapsulation in the software makes for a cleaner architecture drawing, and the converse is true as well.

Delegation of Tasks

The diagrams also help you divide up and apportion work to minions. Which parts of the system can be broken off into separate, describable pieces that someone else can implement?

Too often we want our minions to do the boring part of the job while we get to do all of the fun parts. (“Here, minion, write my test code, do my documentation, fold my laundry.”) Not only does this drive away the good minions, it tends to decrease the quality of your product. Instead, think about which whole box (or whole subtree) you can give to someone else. As you try to describe the interdependencies to your imaginary minion, you may find that they become worse than you’ve represented in the diagrams. Or you may find that a simple flag (such as a semaphore) to describe who currently owns the resource may be enough to get started.

Note

What if you have no chance of getting minions? It is still important to go through this thought process. You want to reduce interdependencies where possible, which may cause you to redesign your system. And where you can’t reduce the interdependencies, you can at least be wary of them when coding.

Looking for things that can be split off and accomplished by another person will help you identify sections of code that can have a simple interface between them. Also, when marketing asks how this project could be made to go faster, you’ll have an answer ready for them. However, there is one more thing our imaginary minion provides: assume he or she is slightly deficient and you need to protect yourself and your code from the faulty minion’s bad code.

What sort of defensive structures can you picture erecting between modules? Imagine the data being passed between modules. What is the minimum amount of data that can move between boxes (or groups of boxes)? Does adding a box to your group mean that significantly less data is passed? How can the data be stored in a way that will keep it safe and usable by all those who need it?

The minimization of complexity between boxes (or at least between groups of boxes) will make the project go more smoothly. The more that your minions are boxed into their own sets of code, with well-understood interfaces, the more everyone will be able to test and develop their own code.

Driver Interface: Open, Close, Read, Write, IOCTL

The previous section used a top-down view of module interfaces to train you to consider encapsulation and think about where you can get help from another person. Going from the bottom up works as well. The bottom here consists of the modules that talk to the hardware (the drivers).

Many drivers in embedded systems are based on an API used to call devices in Unix systems. Why? Because the model works well in many situations and saves you from reinventing the wheel every time you need access to the hardware. The interface to Unix drivers is straightforward:

open

Opens the driver for use. Similar to (and sometimes replaced by) init.

close

Cleans up the driver, often so another subsystem can call open.

read

Reads data from the device.

write

Sends data to the device.

ioctl (Pronounced “eye-octal”)

Stands for input/output (I/O) control and handles the features not covered by the other parts of the interface. It is somewhat discouraged by kernel programmers due to its lack of structure, but it is still very popular.

In Unix, a driver is part of the kernel. Each of these functions takes an argument with a file descriptor that represents the driver in question (such as /dev/tty01 for the first terminal on the system). That gets pretty cumbersome for an embedded system without an operating system. The idea is to model your driver upon Unix drivers. A sample functionality for an embedded system device might look like any of these:[2]

spi.open()

spi_open()

SpiOpen(WITH_LOCK)

spi.ioctl_changeFrequency(THIRTY_MHz)

SpiIoctl(kChangeFrequency, THIRTY_MHz)

This interface straightens outs the kinks that can happen at the driver level, making them less specific to the application and creating reusable code. Further, when others come up to your code and the driver looks like it has these functions, they will know what to expect.

Tip

The driver model in Unix sometimes includes two newer functions. The first, select (or poll), waits for the device to change state. That used to be done by getting empty reads or polling ioctl messages, but now it has its own function. The other one is mmap, which controls the memory map the driver shares with the code that calls it.

If your round peg can’t fit into this POSIX-compliant square hole, don’t force it. But if it looks like it might, starting with this standard interface can make your design just a little better and easier to maintain.

Adapter Pattern

One traditional software design pattern is called adapter (or sometimes wrapper). It converts the interface of an object into one that is easier for a client (a higher-level module). Often, adapters are written over software APIs to hide ugly interfaces or libraries that change.

Many hardware interfaces are like ungainly software interfaces. That makes each driver an adapter, as shown in Figure 2-6. If you create a common interface to your driver (even if it isn’t open, close, read, write, ioctl), the hardware interface can change without your upper-level software changing. Ideally, you can switch platforms altogether and need only to rework the underpinnings.

Note that drivers are stackable, as shown in Figure 2-7. In our example we have a display that uses flash memory, which in turn uses SPI communication. When you call open for the display, it will call its subsystems initialization code, which will call open for the flash, which will call open for the SPI driver. That’s three levels of adapters, all in the cause of portability and maintainability.

Figure 2-6. Drivers implement the adapter pattern

If the interface to each level is consistent, the higher-level code is pretty impervious to change. For example, if our SPI flash changes to an I²C EEPROM (a different communication bus and a different type of memory), the display driver might not need to change, or it might only need to replace flash functions with EEPROM ones.

In Figure 2-7, I’ve added a function called test to the interface of each of the modules. In Chapter 3, I’ll discuss some strategies for choosing automated tests to make your code more robust. For now, they are just placeholders.

Figure 2-7. Interfaces for display subsystem and below

Getting Started with Other Interfaces

Moving away from drivers, your definition of a module interface depends on the specifics of the system. It is pretty safe to say that most modules will also need an initialization function (though drivers often use open for this). Initialization may happen as objects are instantiated during startup, or it may be a function called as the system initializes. To keep modules encapsulated (and more easily reused), high-level functions should be responsible for initializing the modules they depend upon. A good init function should be able to be called multiple times if it is used by different subsystems. A very good init function can reset the subsystem (or hardware resource) to a known good state in case of partial system failure.

Now that you don’t have a completely blank slate anymore, it may be easier to fill in the interface of each of your boxes. Consider how to retain encapsulation for your module, your hypothetical minion’s contribution, and the driver model, then start working out the responsibilities of each box in your architecture diagrams.

Tip

Having created three versions of the architecture, you probably won’t want to maintain each one. As you fill in the interface, you may want to focus on whichever one is most useful to you (or clearest to your boss).

Example: A Logging Interface

The goal of the logging module is to implement a robust and usable logging system. In this section we’ll start off by defining the requirements of the interface, and then explore some options for the interface (and the local memory). It doesn’t matter what your communication method is. By coding to the interface in the face of limitations, you leave yourself open to reusing your code in another system.

Tip

Logging debug output can slow down a processor significantly. If your code behavior changes when you turn logging on and off, consider how the timing of various subsystems works together.

The implementation is dependent on your system. Sometimes the best you can do is toggle an I/O line attached to an LED and send your messages via Morse code (I kid you not). However, most of the time you get to write text debug messages to some interface. Making a system debuggable is part of making it maintainable. Even if your code is perfect, the person who comes after you might not be so lucky when they add a newly required feature. A logging subsystem will not only help you during development, it is an invaluable tool during maintenance.

The logging interface hides the details of how the logging is actually accomplished, letting you hide changes (and complexity). Your logging needs may change with the development cycle. For example, in the initial phase, your development kit may come with an extra serial port and your logging information can go to a computer. In a later phase, the serial port might not be available, so you might have to pare your output back to an LED or two.

I have occasionally wished for a complete mind meld when a system is acting oddly. Sadly, there are never enough resources available to output everything you might want. Further, your logging methodology may change as your product develops. This is an area where you should encapsulate what changes by hiding its functions called from the main interface. The small overhead you add will allow you greater flexibility. If you can code to the interface, changing the underlying pathway won’t matter.

Commonly, you want to output a lot of information over a relatively small pipe. As your system gets bigger, the pipe looks smaller. Your pipe may be a simple serial port connecting via RS-232 to your computer, a pathway available only with special hardware. It may be a special debug packet that happens over your network. The data may be stored in an external RAM source and be readable only when you stop the processor and read via JTAG. The log may be available only when you run on the development kit, and not on the custom hardware. So the first big requirement for the module is this: the logging interface should be able to handle different underlying implementations.

Second, as you work on one area of the system at a time, you may not need (or want) messages from other areas. So the logging methods should be subsystem-specific. Of course, you do need to know about catastrophic issues with other subsystems.

The third requirement is a priority level that will let you debug the minutiae of the subsystem you are working on without losing critical information from other parts of the code.

Typical calls needed for logging

Defining the main interface requirements of a module is often enough of a definition, particularly during design. However, those are a lot of words for something that can be summed up in one line of code:

void Log(enum eLogSubSystem sys, enum eLogLevel level, char *msg);

This prototype isn’t fixed in stone and may change as the interface develops, but it provides a useful shorthand to other developers.

The log levels might include: none, information only, debugging, warning, error, and critical. The subsystems will depend on your system but might include: communications, display, system, sensor, updating firmware, etc.

Note that the log message takes a string, unlike printf or iostream, which take variable arguments. You can always use a library to build the message if you have that functionality. However, the printf and iostream family of functions are some of the first things cut from a system that needs more code space and memory. If that is the case, you’ll probably end up implementing what you need most on your own, so this interface should have the bare minimum of what you’ll need. In addition to printing strings, usually you’ll need to be able to print out at least one number at a time:

void LogWithNum(enum eLogSubSystem sys, enum eLogLevel level, char *msg, int number);

Using the subsystem identifiers and the priority levels allows you to change the debug options remotely (if the system allows that sort of thing). When debugging something, you might start with all subsystems set to a verbose priority level (e.g., debug), and once a subsystem is cleared, raise its priority level (e.g., error). This way you get the messages you want when you want them. So we’ll need an interface to allow this flexibility on a subsystem and priority level:

void LogSetOutputLevel(enum eLogSubSystem sys, enum eLogLevel level)

Because the calling code doesn’t care about the underlying implementation, it shouldn’t have direct access to it. All logging functions should go through this log interface. Ideally, the underlying interface isn’t shared with any other modules, but your architecture diagram will tell you if that isn’t true. The log initialization function should call whatever it depends on, whether it’s to initialize a serial port driver or set up I/O lines.

Because logging can change the system timing, sometimes the logging system needs to be turned off in a global way. This allows you to say with some certainty that the debugging subsystem is not interfering in any way with any other part of the code. Although these might not be used often, an on/off switch is an excellent addition to the interface:

void LogGlobalOn();
void LogGlobalOff();

Version Your Code

At some point, someone will need to know exactly what revision of code is running. In the application world, putting a version string in the help/about box is straightforward. In the embedded world, the version should be available via the primary communication path (UART, I²C, other bus, etc.). If possible, this should print out automatically on boot. If that is not possible, try to make it available through a query. If that is not possible, it should be compiled into its own object file and located at a specific address so that it is available for inspection.

The ideal version is of the form A.B.C, where:

A is the major version (1 byte)

B is the minor version (1 byte)

C is a build indicator (2 bytes)

If the build indicator does not increment automatically, you should increment it often (numbers are cheap). Depending on your output method and the aesthetics of your system, you may want to add an interface to your logging code for displaying the version properly:

void LogVersion(struct sFirmwareVersion *v)

The runtime code is not the only piece of the system that should have a version. Each piece that gets built or updated separately should have a version that is part of the protocol. For example, if an EEPROM is programmed in manufacturing, it should have a version that is checked by the code before the EEPROM is used. Sometimes you don’t have the space or processing power to make your system backward compatible, but it is critical to make sure all of the moving pieces are currently compatible.

Designs of the other subsystems do not (and should not) depend on the way that logging is carried out. If you can say that about the interface to your modules (“The other subsystems do not depend on the way XYZ is carried out; they just call the given interface”), you’ve successfully designed the interfaces of the system.

State of logging

As you work on the architecture, some areas will be easier to define than others, especially if you’ve done something similar before. And as you define interfaces, you may find that ideas come to you and implementation will be easy (even fun), but only if you start right now.

Hold back a bit when you get this urge to jump into implementation; try to keep everything to the same level. If you finish gold-plating one module before defining the interface to another, you may find that they don’t fit together very well.

Once you’ve gotten a little further with all the modules in the system, it may be time to consider the state associated with each module. In general, the less state held in the module, the better (so that your functions do the same things every time you call them). However, eliminating all state is generally unavoidable (or at least extremely cumbersome).

Back to our logging module: can you see the internal state needed? The LogGlobalOn and LogGlobalOff functions set (and clear) a single variable. LogSetOutputLevel needs to have a level for each subsystem.

You have some options for how to implement these variables. If you want to eliminate local state, you could put them in a structure (or object) that every function calling into the logging module would need to have. However, that means passing the logging object to every function that might possibly need logging and passing it to every function that has a function that needs to log something.

Tip

You may think passing around the state like that is convoluted. And for logging, I’d agree. However, have you ever wondered what is in the file handler you get back when your code opens a file? Open files embody lots of state information.

Maybe passing all these parameters around isn’t a good idea. How can every user of the logging subsystem get access to it? As noted in the sidebar , with a little extra work, even C can create objects that make this parameter-passing problem simpler. Even in a more object-oriented language, you could have a module where the functions are globally available and the state is kept in a local object. However, there is another way to give access to the logging object without opening up the module completely.

Object-Oriented Programming in C

Why not use C++? Most systems have pretty good embedded C++ compilers. However, there is a lot of code already written in C, and sometimes you need to match what is already there. Or maybe you just like C for its speed. That doesn’t mean you should leave your object-oriented principles behind.

One of the most critical ideas to retain is data hiding. In an object-oriented language, your object (class) can contain private variables. This is a polite way of saying they have internal state that no one else can see. C has different kinds of global variables. By scoping a variable appropriately, you can mimic the idea of private variables (and even friend objects). First, let’s start with the C equivalent of a public variable, declared outside a function, usually at the top of your C file:

// everyone can see this global with an "extern tBoolean_t gLogOnPublic;" in the
// file or in the header
tBoolean gLogOnPublic;

These are the global variables upon which spaghetti code is built. Try to avoid them. A private variable is declared with the static keyword and is declared outside a function, usually at the top of your C file.

// file variables are globals with some encapsulation
static tBoolean gLogOnPrivate;

Warning

The static keyword means different things in different places; it’s actually kind of annoying that way. For functions and variables outside functions, the keyword means “hide me so no one else can see” and limits scope. For variables within a function, the static keyword maintains the value between calls, acting as a global variable whose scope is only the function it is declared in.

A set of loose variables is a little difficult to track, so consider a structure filled with the variables private to a module:

// contain all the global variables into a structure:
struct {
  tBoolean logOn;
  static enum eLogLevel outputLevel[NUM_LOG_SUBSYSTEMS];
} sLogStruct;
static struct sLogStruct gLogData;

If you want your C code to be more like an object, this structure would not be part of the module, but would be created (malloc’d) during initialization (LogInit, for the logging system discussed in this chapter) and passed back to the caller like this:

struct sLogStruct* LogInit(){
  int i;
  struct sLogStruct *logData = malloc(sizeof(*logData));
  logData->logOn = FALSE;
  for (i=0; i < NUM_LOG_SUBSYSTEMS; i++) {
    logData-> outputLevel = eNoLogging;
  }
  return logData;
}

The structure can be passed around as an object. Of course, you’d need to add a way to free the object; that just requires adding another function to the interface.

Pattern: Singleton

Another way to make sure every part of the system has access to the same log object is to use another design pattern, this one called singleton.

When it is important that a class have exactly one instance, the singleton pattern is commonly seen. In an object-oriented language, the singleton is responsible for intercepting requests to create new objects in order to preserve its solitary state. The access to the resource is global (anyone can use it), but all accesses go through the single instance. There is no public constructor. In C++ this would look like:

class Singleton {
public:
  static Singleton* Instance() {
    if (mInstance == 0) {
      mInstance = new Singleton;
    }
    return mInstance;
  }
protected:
  Singleton(); // no one can create this except itself
private:
  static Singleton* mInstance = 0;
}

For logging, the singleton lets the whole system have access to the system with only one instantiation. Often when you have a single resource (such as a serial port) that different parts of the system need to work with, a singleton can come in handy to avoid conflicts.

In an object-oriented language, singletons also permit lazy allocation and initialization so that modules that are never used don’t consume resources.

Sharing private globals

Even in a procedural language like C, the concept of the singleton has a place. The data-hiding goodness of object-oriented design was noted in Object-Oriented Programming in C. Protecting a module’s variables from modification (and use) by other files will give you a more robust solution.

However, sometimes you need a back door to the information, either because you need to reuse the RAM for another purpose (Chapter 8) or because you need to test the module from an outside agency (Chapter 3). In C++, you might use a friend class to gain access to otherwise hidden internals.

In C, instead of making the module variables truly global by removing the static keyword, you can cheat a little and return a pointer to the private variables:

static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState() {
  return &gLogData;
}

This is not a good way to retain encapsulation and keep data hidden, so use it sparingly. Consider guarding against it during normal development:

static struct sLogStruct gLogData;
struct sLogStruct* LogInternalState() {
#if PRODUCTION
  #error “Internal state of logging protected!”
#else
  return &gLogData;
#endif /* PRODUCTION */
}

As you design your interface and consider the state information that will be part of your modules, keep in mind that you will also need methods for verifying your system.

[2] Style is very important. Coding guidelines will save you debugging time, and they won’t quash your creativity. If you don’t have any, look at the style guide Google suggests for open source projects. Their explanations of why they chose what they did might help you formulate your own guide.

A Sandbox to Play In

That pretty much covers the low- and mid-level boxes, but we still have some algorithm boxes to think about. One goal of a good architecture is to keep the algorithm as segregated as possible. The common pattern of Model-View-Controller (MVC) is an excellent one to apply here. The purpose of the pattern is to isolate the gooey center of the application from the user interface so they can be developed and tested independently.

In this pattern, the view is the interface to the user, both input and output. In our device, the user might not be a person; it could be hardware sensors (input) and a screen (output). In fact, if you have a system without a screen but that sends data over a network, the view may have no visual aspect, but it is still a part of the system as the form of input and output. The model is the domain-specific data and logic. This is the part that takes raw data from the input and creates something useful, often using the algorithm that makes your product special. The controller is the glue that works between the model and the view: it handles how to get the input to the model for processing and the data from the model for display or outside communication. There are standard ways for these three elements to interact, but for now it is enough to understand the separation of functions.

Different Aspects of the Model-View-Controller

The good news about the MVC is that nearly everyone agrees that there are three parts to it. The bad news is that this might be the only thing everybody agrees on.

The model holds the data, state, and application logic. If you are building a weather station, the model has the temperature monitoring code and predictive models. For an MP3 player, the model consists of a database of music and the codec necessary to play it.

Traditionally thought of in contexts where there is a screen, the view represents the display-handling functions. The view is what the user sees of the model. A view could be a picture of a sun or a detailed readout of the statistics from a weather service. Those are both views of the same information.

The controller is the somewhat nebulous cloud that sits between (or near) the model and view to help them work together. The goal of the controller is to make the view and model independent of each other so that each can be reused. For that MP3 player, your company may want a consistent interface even when your audio-playing hardware is revamped. Or maybe it is the same hardware but marketing wants to make the system look more child-friendly. How can you achieve both of these, while touching the smallest amount of code? The controller enables the model/view separation by providing services such as translating a user button press into an action on the model.

Figure 2-8 shows a basic interpretation of the MVC and some common variants. There are a lot of different ways to put it together, some of them seemingly contradictory. The term “MVC” is kind of like the adjective “sanguine,” which can mean either murderous or cheerfully optimistic. You may need some context clues to know which it is, but either way, you are probably talking about someone’s mood.

There is another way to use the MVC pattern, which serves as a valuable way to develop and verify algorithms for embedded systems: use a virtual box, or sandbox. The more complex the algorithm, the more this is a necessity.

Figure 2-8. Overview of Model-View-Controller pattern

If you can make all of the inputs to an algorithm’s box and put them through a single interface, you can switch back and forth between a file on a PC and the actual system. Similarly, redirect the output of the algorithm to a file. Now you can test the algorithm on a PC (where the debugging environment may be a lot better than an embedded system), and rerun the same data over and over again until any anomalous behavior can be isolated and fixed. It is a great way to do regression tests to verify that little algorithm changes don’t have unforeseen side effects.

In the sandbox environment, your files and the way you read the data in are the view part of the MVC. The algorithm you are testing is the model, and this shouldn’t change. The controller is the part that changes when you put the algorithm on a PC. Consider an MP3 player (Figure 2-9) and what the MVC diagram might look like with a sandbox. Note that both the view and controller will have relatively strict APIs because you will be replacing those as you move from virtual device to actual device.

Your input view file might be like a movie script, directing the sandbox to act as the user might. The first field is a time when the second field, a method in your system, is fired:

Time, Action, Variable arguments // comment
00:00.00, PowerOnClean          // do all of the initialization
00:01.00, PlayPressed           // no action, no song loaded
00:02.00, Search, "Still Alive" // expect list back based on available matches
00:02.50, PlayPressed           // expect song to be played

The input file can look however you want. For instance, you might have multiple output files, one to describe the state and the changes sent to the user interface from the controller and the model and another one to output what the model would send to the digital-to-analog converter (DAC). The first output file might look like this:

Time, Subsystem, Log message
00:00:01, System, Initilization: sandbox version 1.3.45
00:01:02, Controller, Minor error: no song loaded
00:02:02, Controller, 4 songs found for "Still Alive" search, selecting ID 0x1234
00:02.51, Controller, Loading player class to play ID 0x1234

Again, you get to define the format so that it meets your needs (which will probably change). For instance, if you have a bug where the player plays the wrong song, the sandbox could output the song ID in every line.

The Model-View-Controller here is a very high-level pattern, specifically a system-level pattern. Once you look at breaking up the system this way, you might find that it has fractal qualities. For instance, what if you only want to be able to test the code that reads a file from the memory and outputs it to a DAC? The filename and the resulting bitstream of information that would go to the DAC could be considered the view (which consists of inputs and outputs, even if no humans are involved), the logic and data necessary to convert the file would be the model, and the file handler may be the controller since that would have to change between platforms.

With this idea of separation and sandboxing in mind, take a look at the architecture drawings and see how many inputs the algorithm has. If there is more than one, does it make sense to have a special interface object? Sometimes it is better to have multiple files to represent multiple things going on. Looking further into the diagrams, does it make sense to incorporate the virtual box at a lower level than just the product feature algorithms? This expands your model and, more importantly, lets you test more of your code in a controlled environment.

Your virtual box could be expensive to develop, so you may not want to start implementation immediately. In particular, your virtual box may have different sizes for your variables and its compiler potentially could act differently, so you may want to hold off building it unless your algorithms are complex or your hardware is a long time in coming. There is a trade-off between time to develop the virtual box and its extraordinary powers of debuggability and testability. But identifying how to get to a sandbox from the design and keeping it in mind as you develop your architecture will give you leeway for when things go wrong.

Figure 2-9. Model-View-Controller in a sandbox

Proceeding in the Face of Resource and Time Constraints

Creating an architecture diagram from scratch can be daunting. It can be even harder if you join a team with a close deadline, a bunch of code, and very little documentation with no time to write more. A system architecture diagram is still called for, though you may need to keep it in your notebook and fill it in as best you can. Your ability to see the structure will help you write your piece of the system in a way that keeps you on track to provide good-quality code and meet your deadline.

The schematic (or the hardware block diagram if they have it) is still a good place to start. After that, get a list of the code files. If there are too many to count, use the directory names and libraries. Someone decided that these are modules, and maybe they even tried to consider them as objects, so they go in our diagram as boxes. Where to put the boxes and how to order them is a puzzle, and one that may take a while to figure out.

You may need to consider formulating two architectures: one local to the code you are working on and a more global one that describes the whole product so you can see how your piece fits in. As the architecture gets bigger, it may need to have some depth to it, particularly for a mature design. If the drawing is too complex, bundle up some boxes into one box and then expand on them in another drawing.

Which style of drawing you use for this depends on what you feel comfortable with. You may need to try a few different options before settling on one.

Further Reading

This chapter touched on a few of the many design patterns. The rest of the book will as well, but this is a book about embedded systems, not about design patterns. Think about exploring one of these to learn more about standard software patterns:

Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley.

This is the original, seminal work on design patterns. It uses C++ as the reference language.

Freeman, Eric T., Elizabeth Robson, Bert Bates, and Kathy Sierra. 2004. Head First Design Patterns. Cambridge, MA: O’Reilly.

Using Java as the example language, this book gives great examples with an engaging style.

Search on Wikipedia for software design pattern.

Interview Question: Create an Architecture

Describe the architecture for this [pick an object in the room] conference phone.

Looking around an interviewing area is usually fairly dicey because it is devoid of most interesting things. The conference phone gets picked a lot because it is sometimes the most complicated system in the room. Another good target is the projector.

When asking this question, I want to know that the candidate can break a problem down into pieces. I want to see the process as she mentally deconstructs an object. In general, starting with the inputs and outputs is a safe bet. For a conference phone, the speaker and the display are outputs; the buttons and the microphone are inputs. I like to see these as boxes on a piece of paper. A candidate shouldn’t be afraid to pick up the object and see the connections it has. Those are inputs and outputs as well. Once she has the physical hardware down, she can start making connections by asking (herself) how each component works: how does the power button work, and how might the software interface to it? How does the microphone work, and what does it imply about other pieces of the system (e.g., is there an analog-to-digital converter)?

A candidate gets points for mentioning good software design practices. Calling the boxes drivers for the lowest level and objects for the next level is a good start. It is also good to hear some noises about parts of the system that might be reused in a future phone and how to keep them encapsulated.

I expect her to ask questions about specific features or possible design goals (cost). However, she gets a lot of latitude to make up the details. Want to talk about networking? Pretend it is a voice over IP (VoIP) phone. Want to skip that entirely? Focus instead on how it might store numbers in a mini-database or linked list. I’m happy when she talks about things she is interested in, particularly areas that I’ve failed to ask about in other parts of the interview.

In that same vein, although I want to see a good overview on the whole system, I don’t mind if a candidate chooses to dig deep into one area. This question gives a lot of freedom to expound on how her particular experience would help her design a phone. When I ask a question, I don’t mind if she admits some ignorance and talks about something she knows better.

Asking an interviewee about architecture is not about getting perfect technical details. It is crucial to draw something, even if it is only mildly legible. The intent of the question is about the candidate showing enthusiasm for problem solving and effectively communicating her ideas.

Chapter 3. Getting Your Hands on the Hardware

It can be tough to start working with embedded systems. Most software engineers need a crash course in electrical engineering, and most electrical engineers need a crash course in good software design. Working closely with a member of the opposite discipline will make getting into embedded systems much easier. Some of my best friends are electrical engineers.

If you’ve never been through a development cycle that includes hardware, it can be a bit daunting to try to intuit your roles and responsibilities. I’ll start out with an overview of a how a project usually flows. Then I’ll give you some more detailed advice on the skills you need to hold up your end of the team, including:

Reading a datasheet

Getting to know a new processor

Unraveling a schematic

Having a debugging toolbox

Testing the hardware (and software)

Hardware/Software Integration

The product starts out as an idea or a need to be filled. Someone makes a high-level product design based on features, cost, and time to market. At this point, a schedule is usually created to show the major milestones and activities (see Figure 3-1).

Figure 3-1. Ideal project schedule example

Ideal Project Flow

The hardware team goes through datasheets and reference designs to choose components, ideally consulting the embedded software team. Often development kits are purchased for the riskiest parts of the system, usually the processor and the least-understood peripherals (more on processors and peripherals in a bit).

The hardware team creates schematics while the software team works on the development kit. It may seem like the hardware team is taking weeks (or months) to make a drawing, but most of that time is spent wading through datasheets to find a component that does what it needs to for the product, at the right price, in the correct physical dimensions, with a good temperature range, etc. During that time, the embedded software team is finding (or building) a tool chain with compiler and debugger, creating a debugging subsystem, trying out a few peripherals, and possibly building a sandbox for testing algorithms (see Figure 3-1).

Schematics are entered using a schematic capture program (aka CAD package). These programs are often expensive to license and cumbersome to use, so the hardware engineer will usually generate a PDF schematic at checkpoints or reviews for your use. Although you should take the time to review the whole schematic (described in Reading a Schematic), the part that has the largest impact on you is the processor and its view of the system. Because hardware engineers understand that, most will generate an I/O map that describes the connection attached to each pin of the processor (Chapter 4 suggests taking the I/O map and making a header file from it).

Once the schematic is complete (and the development kits have shown that the processor and risky peripherals are probably suitable), the board can be laid out. In layout, the connections on the schematic are made into a map of physical traces on a board, connecting each component as they are in the schematic.

Tip

Board layout is often a specialized skill, different from electrical engineering, so don’t be surprised if someone other than your hardware engineer does the board layout.

After layout, the board goes to fabrication (fab), where the printed circuit boards (PCBs) are built. Then they get assembled with all of their components. A board and all of its loose components are kits. Getting the long lead time parts can make it difficult to create a complete kit, which can delay assembly. An assembled board is called a PCBA (or PCA), which is what will sit on your desk (or in your lab).

After the schematic is done and the layout has started, the primary task for the embedded software team is to define the hardware tests and get them written while the boards are being created. Working on the real code may seem more productive, but when you get the boards back from fabrication, you won’t be able to make progress on the software until you bring them up. The hardware tests will not only make the bring-up smoother, but they also will make the system more stable for development (and production). As you work on the hardware tests, ask your electrical engineer which parts are the riskiest. Prioritize development for the tests for those subsystems (and/or try them out on a development kit).

When the boards come back, the hardware engineer will power them on to verify there aren’t power issues, possibly verifying other purely hardware subsystems. Then you get a board (finally!) and bring-up starts.

Board Bring-Up

This can be a fun time, one where you are learning from an engineer whose skill set is different than yours. Or it can be a shout-fest where each team accuses the other of incompetence and maliciousness. Your call.

The board you receive may or may not be full of bugs. Electrical engineers don’t have an opportunity to compile their code, try it out, make a change, and recompile. Making a board is not a process with lots of iterations. Remember that people make mistakes, and the more fluffheaded the mistake, the more likely it will take an eternity to find and fix (what was the longest time you ever spent debugging an error that turned out to be a typo?).

Don’t be afraid to ask questions, though, or to ask for help finding your problem (yes, phrase it that way). An electrical engineer sitting with you might be what you need in order to see what is going wrong.

Finding a hardware (or software) bug at an early stage of design is like getting a gift. The fewer people who know about your bugs, the happier you’ll be. Your product team’s bugs reflect upon you, so be willing to give your time and coding skills to unravel a problem. It isn’t just polite and professional; it is a necessary part of being on a team. If there is a geographic or organizational separation and getting the hardware engineer’s help requires leaping through hoops, consider some back-channel communications (and trade in lunches). It might make you poor in the short term, but you’ll be a hero for getting things done, and your long-term outlook will be good.

Tip

Don’t be embarrassed when someone points out a bug; be grateful. If that person is on your team or in your company, that bug didn’t get out to the field where it could be seen by customers.

To make bring-up easier on both yourself and your hardware engineer, first make each component individually testable. If you don’t have enough code space, make another software subproject for the hardware test code (but share the underlying modules as much as possible).

Second, when you get the PCBA, start on the lowest-level pieces, at the smallest steps possible. For example, don’t try out your fancy motor controller software. Instead, set an I/O device to go on for a short time, and hook up an LED to make sure it flashes. Then (still taking things one step at a time), do the minimum necessary to make sure the motor moves (or at least twitches).

Finally, make your tools independent of you being present to run them, so that someone else is able to reproduce an issue. Once an issue is reproducible, even if the cause isn’t certain, fixes can be tried. (Sometimes the fix will explain the cause.) The time spent writing good tests will pay dividends. There is a good chance that this hardware verification code will be around for a while, and you’ll probably want it again when the next set of boards comes back. Also, these tests tend to fold into manufacturing tests used to check the board’s functionality during production.

How do you know what tests need to be written? It starts with an in-depth knowledge of the processor and peripherals. That leads to the topic of reading a datasheet.

Reading a Datasheet

With the pressure to get products released, it can be tough to slow down enough to read the component datasheets, manuals, and application notes. Worse, it can feel like you’ve read them (because you’ve turned all the pages), but nothing sticks, because it is a foreign language. When the code doesn’t work, you’re tempted to complain that the hardware is broken.

If you are a software engineer, consider each chip as a separate software library. Think of all the effort you would need to put into learning a software package (Qt, Gtk, Boost, STL, etc.). That is about how long it will take to become familiar with your processor and its peripherals. The processors will even have methods of talking to the peripherals that are somewhat like APIs. As with libraries, you can often get away with reading only a subset of the documentation—but you are better off knowing which subset is the most important one for you before you end up down a rabbit hole.

Datasheets are the API manuals for peripherals. So make sure you have the latest version of your datasheet (check the manufacturer’s site and do an online search just to be sure) before I let you in on the secrets of what to read twice and what to ignore until you need it.

Warning

Unfortunately, many manufacturers release their datasheets only under NDA, so their websites contain only an overview or summary of their product.

Hardware components are described by their datasheets. Datasheets can be intimidating, as they are packed densely with information. Reading datasheets is an art, one that requires experience and patience. The first thing to know about datasheets is that they aren’t really written for you. They are written for an electrical engineer, more precisely for an electrical engineer who is already familiar with the component or its close cousin.

In some ways, reading a datasheet is like coming into the middle of a technical conversation. Take a look at Figure 3-2, which resembles a real datasheet, even though it’s not a component you’re likely to interface with in your software (it is a prehistoric gait controller for leg actuators). Near the top of each datasheet is an overview or feature list. This is the summary of the chip, but if you haven’t already used a component with a datasheet that is 85% the same as this one, the overview isn’t likely to be very helpful. On the other hand, once you have used three or four analog triceratops (or accelerometers, or whatever you are really looking at), you can just read the overview of the next one and get an idea of how this chip is the same and different from your previous experience. If the datasheet took the time to explain everything a newcomer might want to know, each one would be a book (and most would have the same information). Additionally, it isn’t the newcomers who buy components in volume; it is the experienced engineers who are already familiar with the devices.

Figure 3-2. Top of the Analog Triceratops datasheet from Dino Industries

So I say you should skip the overview header (or at least come back to it later).

I like the functional diagrams that are often on the first page, but they are a lot like the overview. If you don’t know what you are looking for, the block diagram probably isn’t going to enlighten you. So the place to start is the description. This usually covers about half the first page, maybe extending a little on to the second page.

The description is not the place to read a few words and skip to the next paragraph. The text is dense and probably less than a page long. Read this section thoroughly. Read it aloud, underline the important information (which could be most of the information), or try to explain it to someone else (I keep a teddy bear in my office for this purpose, but some people use their EEs).

Datasheet Sections You Need When Things Go Wrong

After the first page, the order of information in each datasheet varies. And not all datasheets have all sections. Nonetheless, you don’t want to scrutinize sections that won’t offer anything that helps your software use the peripheral. Save some time and mental fatigue by skipping over the absolute maximum ratings, recommended operating conditions, electrical characteristics, packaging information, layout, and mechanical considerations.

The hardware team has already looked at that part of the sheet; trust them to do their jobs. Someday, when you aren’t under a lot of pressure to get the product done, it is worth going through those sections.

Although you can skip them now, remember that these sections exist. You may need them when things go wrong, when you have to pull out an oscilloscope and figure out why the driver doesn’t work, or (worse) when the part seems to be working but not as advertised. Note that these sections exist, but you can come back to them when you need them; they are safe to ignore on the first pass:

Pin out for each type of package available

You’ll need to know the pin out if you have to probe the chip during debugging. Ideally, this won’t be important, because your software will work and you won’t need to probe the chip.

The pin configuration shows some pin names with bars over them (Figure 3-3). The bars indicate that these pins are active low, meaning that they are on when the voltage is low. In the pin description, active low pins have a slash before the name. You may also see other things to indicate active low signals. If most of your pins have a NAME, look for a modifier that puts the name in the format nNAME, _NAME, NAME*, NAME_N, etc. Basically, if it has a modifier near the name, look to see whether it is active low, which should be noted in the pin descriptions section.

Note

If this is all brand new to you, consider picking up a beginner's guide to electronics, such as one of those listed in Further Reading.

Pin descriptions

This is a table that looks like Figure 3-4. Come back when you need this information, possibly as you look to see whether the lines should be active low or as you are trying to make your oscilloscope show the same image as the timing diagrams in the datasheet.

Performance characteristics

These tables and graphs, describing exactly what the component does, offer too much detailed information for your first reading. However, when your component communicates but doesn’t work, the performance characteristics can help you determine whether there might be a reason (e.g., the part is rated to work over 0°C–70°C but it is right next to your extremely warm processor, or the peripheral works when the input voltage is just right but falls off in accuracy when the input gets a little outside the specified range).

Sample schematics

Sometimes you get driver code, it works as you need it, and you don’t end up changing much, if anything. The sample schematics are the electrical engineering version of driver code. When your part is acting up, it can be reassuring to see that the sample schematics are similar to your schematics. However, as with vendor-provided driver code, there are lots of excellent reasons why the actual implementation doesn’t match the sample implementation. If you are having trouble with a part and the schematic doesn’t match, ask your electrical engineer about the differences. It may be nothing, but it could be important.

Figure 3-3. Analog Triceratops pin out

Figure 3-4. Analog Triceratops pin descriptions

Important Text for Software Developers

Eventually, possibly halfway through the datasheet, you will get to some text (Figure 3-5). This could be titled “Application Information” or “Theory of Operation.” Or possibly the datasheet just switches from tables and graphs to text and diagrams. This is the part you need to start reading. It is fine to start by skimming this to see where the information you need is located, but eventually you will really need to read it from start to end. As a bonus, the text there may link to application notes and user manuals of value. Read through the datasheet, considering how you’ll need to implement the driver for your system. How does it communicate? How does it need to be initialized? What does your software need to do to use the part effectively? Are there strict timing requirements, and how can your processor handle them?

As you read through the datasheet, mark areas that may make an impact on your code so you can return to them. Timing diagrams are good places to stop and catch your breath (Figure 3-6). Try to relate those to the text and to your intended implementation. More information on timing diagrams is provided in the sidebar How Timing Diagrams Help Software Developers.

Figure 3-5. Analog Triceratops theory of operation

Figure 3-6. Analog Triceratops timing diagrams

In addition to making sure you have the latest datasheet for your component, check the manufacturer’s web page for errata, which provide corrections to any errors in the datasheet. Search the web page for the part number and the word “errata” (Figure 3-7). Errata can refer to errors in the datasheet or errors in the part.

Figure 3-7. Analog Triceratops errata

Tip

Datasheets can have revisions, and components can have revisions. Get the latest datasheet for the component revision you are using.

When you are done, if you are very good or very lucky, you will have the information you need to use the chip. Generally, most people will start writing the driver for the chip and spend time re-reading parts as they write the interface to the chip, then the communication method, then finally actually use the chip. Reading datasheets is a race where the tortoise definitely wins over the hare.

Once you are all done with the driver and it is working, read through the feature summary at the top of the datasheet because now you have conquered this type of component and can better understand the summary. Even so, when you implement something similar, you’ll probably still need to read parts of the datasheet, but it will be simpler and you’ll get a much better overview from the summary on the datasheet’s front page.

Other resources may also be available as you work with peripherals:

If the chip has a user manual, be sure to look at that.

Application notes often have specific use cases that may be of interest.

There may be forums and examples from other people using the part.

Look around before diving in; the answer to something you don’t know yet may be out there.

How Timing Diagrams Help Software Developers

Timing diagrams show the relationship between transitions. Some transitions may be on the same signal, or the timing diagram may show the relationship between transitions on different signals. For instance, alternating states of the hands (HND1 and HND2) in normal mode at the top of Figure 3-6 show that one hand is raised shortly after the other is put down. When approaching a timing diagram, start on the left side with the signal name. Time advances from left to right.

Most timing diagrams focus on the digital states, showing you when a signal is high or low (remember to check the name for modifiers that indicate a signal is active low). Some diagrams include a ramp (such as hands and feet in the boot sequence in Figure 3-6) to show you when the signal is in a transitory state. You may also see signals that are both high and low (such as the horn in the boot sequence of Figure 3-6); these indicate the signal is in an indeterminate logical state (for output) or isn’t monitored (for input).

The important time characteristics are highlighted with lines with arrows. These are usually specified in detail in a table. Also look for an indication of signal ordering (often shown with dashed lines) and causal relationships (usually arrows from one signal to another). Finally, footnotes in the diagram often contain critical information.

Evaluating Components Using the Datasheet

It may seem odd to have this section about evaluating a component after its implementation. That isn’t the way it goes in projects. However, it is the way it goes in engineering life. Generally, before you get to the point where you choose pieces of the system, you have to cut your teeth on implementing pieces of systems designed by other people. So evaluating a component is a more advanced skill than reading the datasheet, a skill that electrical engineers develop before software engineers.

When you are evaluating a component, your goal is to quickly eliminate components that won’t work. Don’t waste valuable time determining precisely how a component does feature X if the component can’t be used because it requires 120 V AC and your system has 5 V of DC. Start off with a list of must-haves and a list of wants. Then you’ll want to generate your potential pool of parts that require further investigation.

Before you get too deep into that investigation, let’s talk about the things that datasheets don’t have. They usually don’t have prices, because those depend on many factors, especially how many you plan to order. And datasheets don’t have lead times, so be careful about designing around the perfect part; it may be available only after a six-month wait. Unless you are ordering online, you’ll need to talk to your vendor or distributor.

Talking with your vendor is a good opportunity to ask whether they have any guidance for using the part, initialization code, application notes, white papers, forums, or anything that might get you a little further along. Vendors recognize that these can be selling points, and their application engineers are generally willing to help. Distributors might even help you compare and contrast the different options available to you.

Tip

Even when working with a distributor, Digikey is often a great way to get some idea of price and lead times.

Back to the datasheet. This time those skipped sections become the most critical. Start with the absolute maximum ratings and electrical characteristics (see Figure 3-8). If they don’t match (or exceed) your criteria, set the datasheet aside. The present goal is to wade through the pile of datasheets quickly; if a part doesn’t meet your basic criteria, note where it fails and go on. (Keeping notes is useful; otherwise, you may end up rejecting the same datasheet repeatedly.) You may want to prioritize the datasheets by how far out of range they are. If you end up without any datasheets that meet your high standards, you can recheck the closest ones to see whether they can be made to work.

Once the basic electrical and mechanical needs are met, the next step is to consider the typical characteristics, determining whether the part is what you need. I can’t help you much with specifics, as they depend on your system’s needs and the particular component. Some common questions at this level are about your functional parameters: does the component go fast enough? Does the output meet or exceed that required by your system? In a sensor, is the noise acceptable?

Once you have two or three datasheets that pass the first round of checking, delve deeper into them. If there is an application section, that is a good place to start. Are any of these applications similar to yours? If so, carry on. If not, worry a bit. It is probably all right to be the first to use a peripheral in a particular way, but if the part is directed particularly to underwater sensor networks and you want to use it in your super-smart toaster, you might want to find out why they’ve defined the application so narrowly. More seriously, there may be a reason why the suggested applications don’t cover all uses. For example, chips directed toward automotive use may not be available in small quantities. The goal of the datasheet is to sell things to people already using similar things, so there may be a good reason for a limited scope.

Next, look at the performance characteristics and determine whether they meet your needs. Often you’ll find new requirements while reading this section, such as realizing your system should have the temperature response of part A, the supply voltage response of part B, and the noise resistance of part C. Collect these into the criteria, and eliminate the ones that don’t meet your needs (but also prioritize the criteria so you don’t paint yourself into a corner).

At this point, you should have at least two but no more than four datasheets per part. If you have more than four, ask around to see whether one vendor has a better reputation in your purchasing department, has shorter lead times, or has better prices. You may need to come back to your extras, but select out four that seem good.

If you have eliminated all of your datasheets or you have only one, don’t stop there. It doesn’t mean that everything is unusable (or that only one is). Instead, it may mean that your criteria are too strict and you’ll have to learn more about the options available to you. So choose the two best components, even if one or both fail your most discerning standards.

For each remaining datasheet, you want to figure out the tricky pieces of the implementation and see how well the component will fare in your system. This is where some experience dealing with similar parts is useful. You want to read the datasheet as though you were going to implement the code. In fact, if you don’t have experience with something pretty similar, you may want to type up some code to make it real. If you can prototype the parts with actual hardware, great! If not, you can still do a mental prototype, going through the steps of implementation and estimating what will happen.

Even though you will be doing this for two to four parts and probably using only one of them, this will give you a jump start on your code when the part is finally chosen.

Such in-depth analysis takes a significant amount of time but reduces the risk of the part not working. How far you take your prototype is up to you (and your schedule). If you still have multiple choices, look at the family of the component. If you run out of something (space, pins, or range), is there a pin-for-pin compatible part in the same family (ideally with the same software interface)? Having headroom can be very handy.

Finally, having selected the component, the feature summary is an exercise in comparative literature. Now that you have become that person who has already read several datasheets for similar components, the overview that starts the datasheet is for you. If you have any remaining datasheets to evaluate, start there. Compare the ones you’ve done against the new ones to get a quick feel for what each chip does and how different parameters interact (e.g., faster speed likely will be proportional to higher cost).

Figure 3-8. Analog Triceratops absolute maximum ratings

Your Processor Is a Language

As you get to know a new processor, expect that it will take almost the same level of effort as if you were learning a new programming language. And, like learning a language, if you’ve already worked with something similar, it will be simpler to learn the new one. As you learn several programming languages or several processors, you will find that learning the new ones becomes easier and easier.

Although this metaphor gives you an idea of the scale of information you’ll need to assimilate, the processor itself is really more like a large library with an odd interface. Talking to the hardware is a misnomer. Your software is actually talking to the processor software through special interfaces called registers, which I’ll cover in more detail in Chapter 4. Registers are like keywords in a language. You generally get to know a few (if, else, while), and then later you get to know a few more (enum, do, sizeof), and finally you become an expert (static, volatile, union).

The amount of documentation for a processor scales with the processor complexity. Your primary goal is to learn what you need to get things accomplished. With a flood of information available, you’ll need to determine which pieces of documentation get your valuable time and attention. Some documents to look for include:

User Manual (or User Guide) from the processor vendor

Often voluminous, the user manual provides most of what you’ll need to know. Reading the introduction will help you get to know the processor’s capabilities (e.g., how many timers it has and its basic memory structure).

Tip

Why get the user manual from the vendor? Take for example the NXP LPC1313 processor, which uses an ARM Cortex-M3 core. You don’t want to read the ARM user manual if you are using the LPC1313, because 88% of the information in the ARM manual is extraneous, 10% will be in the LPC13xx manual, and the last few percent you probably won’t ever need.

Often these are written for families of processors, so if you want to use an LPC1313, you’ll need to get an LPC13xx manual and look for the notes that differentiate the processors. Once you read the introduction, you’ll probably want to skip to the parts that will be on your system. Each chapter usually will have a helpful introduction of its own before moving into gritty details.

The user manual will have the information you need to work with the chip, though it may not help you get a system up and working.

Getting Started Guide or User Manual for the development kit

A development kit (dev kit) is often the place to start when working with a new processor. Using a dev kit lets you set up your compiler and debugger with confidence, before custom hardware comes in (and gives you something to compare against if your hardware doesn’t work immediately). The dev kit is generally a sales tool for the processor, so it isn’t too expensive and tends to have excellent documentation for setting the system up from scratch. The kit recommends compilers, debuggers, and necessary hardware, and even shows you how to connect all the cables. The development kit documentation is intended to be an orientation for programmers, so even if you don’t purchase a kit, the associated documentation may help orient your to the processor’s ecosystem.

Getting Started Guide (slides)

This document describes how to get started with using the processor for both electrical engineers and software developers. Although interesting and fast to read, this slide deck generally won’t answer questions about how to use the processor. It can be helpful when evaluating a processor for use in a project, as it does discuss what the processor is and common applications. Also, it might give you an idea of what dev kits are available.

Wikis and forums

While the main Wikipedia page to your processor probably won’t have enough information to help you get code written, it may give you a high-level overview (though usually the user manual’s introduction is more useful to you). The Wikipedia page may have valuable links to forums and communities using the processor where you can search for problems you might encounter and see how other people solved them.

The vendor may also have wiki pages or forums devoted to the processor. These can be valuable for another perspective on the information in the user manual or getting started guide. They are often easy to search, with links to lots of examples.

Vendor or distributor visits

Sit in on these. They may have little readily pertinent information, but the networking is useful later when you ask for code or support.

Processor datasheet

The datasheet for your processor is usually more focused on the electrical aspects. Since you’ll be writing the software, you want something more software oriented. So for processors, skip the datasheet and go to the user manual (or user guide).

Most processors now come with many examples, including a ton of driver code. Sometimes this is good code, sometimes not. Even with the not-so-great code, it is nice to have an example to start from. Though the examples generally work as described, they probably won’t be robust or efficient enough for your needs. If you are going to use the code, it becomes your code to own, so make sure you understand it.

Once you get oriented, preferably with a dev kit up and running, hunker down with the user manual and read the chapters for every interface you are using (even if your vendor gave you example code that does what you need). As we work through the specifics of an embedded system, there will be more details about what to expect from chapters in the user manual (inputs and outputs, interrupts, watchdogs and communications, etc.). For now, let’s go back to the bigger picture of the system we are about to bring up.

Reading a Schematic

If you come from the traditional software world, schematics can seem like an eye chart with hieroglyphics, interspersed with strange boxes and tangled lines. As with a datasheet, knowing where to start can be daunting. Beginning on page one of a multipage schematic can be dicey because many electrical engineers put their power-handling hardware there, something you don’t necessarily care about. Figure 3-9 shows you a snippet of one page of a schematic.

Some schematics have blocks of textual data (often in the corner of a page). Those are comments; read them carefully, particularly if the block is titled “Processor I/Os” or something equally useful. These are like block comments in code: they aren’t always there, so you should take your electrical engineer out to lunch when your schematic is well commented.

Figure 3-9. Example schematic snippet

Warning

Most of the time you’ll get a hardware block diagram to help you decode a schematic. On the other hand, a question I’ve been asked in several interviews is, “What does this schematic do?” So this section gives you tips on getting through that as well as a more friendly schematic.

As you go through a schematic for the first time, start by looking for boxes with lots of connections. Often this can be simplified further because box size on the schematic is proportional to the number of wires, so look for the largest boxes. Your processor is likely to be one of those things. Because the processor is the center of the software world, finding it will help you find the peripherals that you most care about.

Above the box is the component ID (U1), often printed on the PCB. Inside or under the boxes is usually the part number. As shown in Figure 3-9, the part number might not be what you are used to seeing. For example, your processor manual may say Atmel AT91R40008, but the schematic may have AT9140008-66AI, which is more of a mouthful. However, the schematic describes not only how to make the traces on the printed circuit board, but also how to put together the board with the correct components. The extra letters for the processor describe how the processor is packaged and attached to the board.

By now you’ve found two to four of the largest and/or most connected components. Look at their part numbers to determine what they actually are. Hopefully, you’ve found your processor. (If not, keep looking.) You may also have found some memory. Memory used to be on the address bus, so it would have almost as many connections as the processor, often with 8 or more address lines and 16 data lines, as in Figure 3-9. Many newer processors have enough embedded memory to alleviate the need for external RAM or flash, so you may not find any large components in addition to your processor.

Next, look at the connectors, which can look like boxes or like long rectangles. These are labeled with Js instead of Us (e.g., J3). There should be a connector for power to the system (at least two pins: power and ground). There is probably a connector for debugging the processor (J1 in Figure 3-9). The other connectors may tell you quite a bit about the board; they are how the world outside sees the board. Is there a connector with many signals that could indicate a daughter board? Is there a connector with an RS-232 signal to indicate a serial port? A connector filled with wire names that start with USB or LCD? The names are intended to give you a hint.

With connectors and the larger chips identified, you can start building a mental model of the system (or a hardware block diagram if you don’t already have one). Now go back to the boxes and see if you can use the names to estimate their function. Looking for names such as SENSOR or ADC might help. Chapter 6 will give you some other signal names that might help you find interesting peripherals.

Tip

In a schematic, wires can cross without connecting. Look for a dot to indicate the connections. Schematics seldom have a dot at a four-way cross, as it can be difficult to tell if there is a dot there when the schematic is printed.

In all this time, we’ve been ignoring the non-box-shaped components. Although it is useful to be able to understand a resistor network, an RC filter, or an op-amp circuit, you don’t need to know these right away. Figure 3-10 shows some common schematic components and their names, though they will be drawn a little differently in your schematic. This will let you express curiosity about the function of a particular resistor. Further Reading gives you some suggestions on how to increase your hardware knowledge.

Figure 3-10. Common schematic components

There are two exceptions to my recommendation that you ignore everything but boxes. First, LEDs, switches, and buttons are often connected to the processor. The schematic will tell you the processor line this type of component is connected to so you know where to read the state of a switch or turn on an LED.

The other kind of components to pay attention to are resistors connected to the processor and power, known as pull-ups because they pull the voltage up (toward power). Usually, pull-ups are relatively weak (high amounts of resistance) so the processor can drive a pulled-up I/O line to be low. The pull-up means that the signal on that line is defined to be high when the processor isn’t driving it. A processor may have internal pull-ups so that inputs to the processor have a default state even when unconnected.

Note that there are also pull-downs, which means resistors to ground. All of the pull-up information applies to them, except that their default logic level is low instead of high. Inputs without a pull-up or pull-down are driven to neither a logical high nor low, so they are floating (they can also be called hi-Z, high impedance, or tristated).

Having a Debugging Toolbox (and a Fire Extinguisher)

The datasheets, user manuals, and schematics we’ve examined so far are just paper (or electronic forms of paper). Let’s get to the hardware. Wait: before you grab it, be aware that touching hardware can give it an electrostatic zap and damage it (especially if you’ve just taken off a fleece jacket).

Keep Your Board Safe

Ask your hardware engineer for the tools to keep your board safe. Try to be conscious of what your board is sitting on. Always carry it around in the bag it came in. Anti-static mats are cheap and force you to allocate a portion of your desk for hardware (even if you don’t use the anti-static wrist band, it is still an improvement over crushing the hardware under a falling book or yanking it off the table).

If possible, when any wires are added to the board, get them glued down as well as soldered. Many an hour has been lost to a broken rework wire. In that same vein, if the connectors are not keyed so that they can be inserted only one way, take a picture of the board or make notes about which way is the correct way to plug in a connector.

I like having my hardware connected to a power strip I can turn off (preferably one my computer is not connected to). It is really good to have an emergency stop-all plan. And note where the fire extinguisher is, just in case.

Seriously, the problem usually isn’t flames but more like little puffs of smoke. Whatever you do to it, though, a damaged board is unlikely to win friends. When your manager or hardware engineer asks you how many boards you want allocated for the embedded software, always ask for a spare or two. Nominally this is so you can make sure the hardware tests work on more than one board. Once the system starts making baby steps, you are the person most likely to lose access to a board so it can be shown off to others. At least those are the reasons you should give for wanting a spare board and not because you may very well damage the first one (or two).

Tip

Often there are also manufacturing errors, especially early on in dense board designs. The bare PCB will be tested for continuity, but there is only so much that can be done, especially with a new design, to test for proper solder connections with no breaks and no shorts. If something doesn’t work on one piece of hardware, it is really useful to have a second to try it on, plus a third as a tie-breaker if you get different results on two boards.

Toolbox

I love my toolbox because it gives me some level of independence from my hardware engineer. With the toolbox on my desk, I can make small changes to my board in a safer way (using needle-nose pliers to move a jumper is much less likely to damage a board than using fingers). Not counting the detritus of assorted jumper wires, RS-232 gender changers, and half-dead batteries I’ve accumulated over the years, my toolbox contains:

Needle-nose pliers

Tweezers (one pair for use as mini-pliers, one pair for use as tweezers)

Box cutter

Digital multimeter (more on this in a minute)

Electrical tape

Sharpies

Assorted screwdrivers (or one with many bits)

Flashlight

Magnifying glass

Safety glasses

Cable ties (Velcro and zip tie)

If your company has a good lab with someone who keeps the tools in labeled areas, use those (but still have your own). If not, a trip to the hardware store may save some frustration in the future.

Digital Multimeter

Even if you opt not to get a more complete toolbox, I strongly suggest getting a digital multimeter (DMM).

You can get a cheap one that covers the basic functionality for about what you’d pay for a good lunch. Of course, you can blow your whole week’s food budget on an excellent DMM, but you shouldn’t need to. As an embedded software engineer, you need only a few functions.

First, you need the voltage mode. Ideally, your DMM at least should be able to read from 0–20 V with 0.1 V granularity and from 0–2 V with 0.01 V granularity. Usually the question you are looking to answer with the voltage mode of the DMM is simple: are any volts getting there at all? A DMM with 1 mV granularity might be nice occasionally, but 80% of your DMM use will be to answer this broader question: “Is the chip or component even powered?”

The second most important mode is the resistance check mode, usually indicated with the Ohm symbol and a series of three arcs. You probably don’t need to know what value a resistor is; the real use for this mode is to determine when things are connected to each other. Just as voltage mode tests whether something has power, this mode will answer the question, “Are these things even talking to each other?”

To determine the resistance between two points on the board, the DMM sends a small current through the test points. As long as the board is off, this is safe. Don’t run it in resistance mode with a powered board unless you know what you are doing.

When the DMM beeps, there is no significant resistance between the two test probes, indicating they are connected (you can check whether the beep is on by just touching the two probes together). Using this mode, your DMM can be used to quickly determine whether a cable or a trace has broken.

Finally, you want a DMM with a current mode denoted with an amp symbol (A or mA). This is for measuring how much power your system is taking; more on that in Chapter 10.

Oscilloscopes and Logic Analyzers

Sometimes you need to know what the electrical signals on the board are doing. There are three flavors of scopes that can help you see the signals as they move:

Traditional oscilloscope

Measures analog signals, usually two or four of them at a time. A digital signal can be measured via analog, but it is kind of boring to see it in one of two spots. Of course, if your digital signal isn’t high or low but somewhere in between, the oscilloscope will help you immensely.

Logic analyzer

Measures digital signals only, usually a lot of them at the same time (16, 32, or 64). At one time, logic analyzers were behemoth instruments that took days to set up, but generally found the problem within hours of setup being complete. Now many modern logic analyzers hook to your computer and help you do the setup, so that this step takes only a little while. Additionally, many logic analyzers have protocol analyzers that interpret the digital bus so you can easily see what the processor is outputting. Thus, if you have an SPI communication bus and you are sending a series of bytes, a protocol analyzer will interpret the information on the bus. A network analyzer is a specific type of protocol analyzer, one that focuses on the complex traffic associated with a network. (However, an RF network anaylzer is very different; it characterizes device response in the radio frequency range.)

Mixed signal oscilloscope

Combines the features of a traditional scope and logic analyzer with a couple of analog channels and 8 or 16 digital ones. A personal favorite of mine, the mixed signal scope can be used to look at different kinds information simultaneously.

These scopes tend to be shared resources, as they are relatively expensive. You can buy reasonably priced ones that hook to your computer, but sometimes their feature sets are limited in nonobvious ways. Generally, you get what you pay for.

Setting up a scope

Step one is to determine which signal(s) are going to help you figure out a problem. Next, you’ll need to attach the scope’s ground clip to ground. If you have more than one (AC ground, DC ground, analog ground) and you aren’t sure which one to connect the scope to, ask your electrical engineer. Choosing the wrong one can be detrimental to the health of your oscilloscope.

Next, attach the probes to the signals of interest. Many processors have such tiny pins that they require specialized probes. On the other hand, many hardware engineers put test points on their board, knowing that the software team is likely to need access to particular signals for debugging. Alternatively, you can get wires soldered on to the board at the signals you need and hook a probe to those.

Using a scope can be a bit daunting if you’ve never set one up before. Oscilloscope manuals vary, but they are the best place to look (and most of them are online). There is no generic manual to all of them, so I can only tell you the words to look for as you page through the manual.

Figure 3-11 shows a representation of an archetypal oscilloscope screen. The most important point is that time moves along the x-axis and voltage varies along the y-axis. The scales of these axes are configurable.

Figure 3-11. Simplified oscilloscope screenshot

Somewhere on the scope’s interface, there should be a knob to make the timescale change. Move it to the right, and each horizontal tick goes from, say, 1 s to 0.5 s (and then down into the millisecond or microsecond range). This controls the timescale for the whole screen. For debugging, the goal is to start with the largest possible time base that shows what you are looking for.

Warning

If you zoom in too far, you’ll see strange things as the supposedly digital signals show their true (and weird) analog colors.

Once you set the time base, you’ll need to set the scale of the voltage axis. This could be one knob that accesses all channels (or probes), making you shift the channel some other way, or you might get one knob per channel. Either way, as you turn it to the right, each vertical block magnifies (5 V goes to 2 V, down to millivolts).

If you aren’t sure, set the timescale to be about 100 ms/division and the voltage granularity to be about 2 V/block. You can zoom in (or out) from there if you need to.

A different knob will set where each channel is on the screen, in other words, where its zero line is. Figure 3-11 shows the zero line for each channel on the left side of the screen. There may be one knob per channel or a way to multiplex the knob between all the channels. Keep the channels a little offset from each other so you can see each one. (You can turn off the channels you don’t need, probably with a button.)

Next, look for a knob to set the zero point of your timescale. This will cause a vertical line to wander your screen. Set it near the middle. Alternatively, you can set it toward the left of your screen if you are looking for information that happens after an event, or toward the right if you want to know what goes on before an event.

At this point, you can probably turn on your system and see a line wiggle. It may go by so quickly that you don’t see what it does, but it can be a start. If nothing happens, look for the buttons that say Run/Stop. You want the scope to be in Run mode. The Stop mode freezes the display at a single point in time, which gives you the opportunity to think about what you’ve discovered. Near Run/Stop may be a button that says Single, which waits for a trigger and then puts the system in Stop mode. There may also be an Auto button, which will let the system trigger continuously.

Warning

If there is an Auto button, be very careful about pushing it. Check to see whether the name means auto-trigger or auto-set. The former is useful, but the latter tries to configure your scope for you automatically. I find that this tends to reset the configuration to something completely random.

To set up a trigger, look for the trigger knob. This will put up a horizontal line to show where the trigger voltage level is. It is channel dependent, but unlike the other channel-dependent knob, usually you can’t set a trigger for each channel. Instead, it requires additional configuration, usually via an on-screen menu and button presses. You want the trigger to be on the channel that changes just at the start (or end) of an interesting event. You’ll need to choose whether the trigger is activated going up or going down. You’ll also need to choose how often the scope will trigger. If you want to see the first time it changes, set a long time-out. If you want to see the last time it changes, set a short one.

There are a few other things to note. Unless you know what you are doing, you don’t want the scope in AC mode. And there is the possibility that the probes are giving signals that are 10x (or 1/10) the size on the screen. This depends on the probe, so if you are off by an order of magnitude, look for a 10x marker and toggle its state.

If you’ve set up your scope according to my instructions (and the scope manual) and it still doesn’t work the way you want it to, get someone with more experience to help you. Scopes are powerful and useful tools, but using one well requires a lot of practice. Don’t get discouraged if it is a bit frustrating to start.

Testing the Hardware (and Software)

I strongly recommend being ready to pull out the toolbox, DMM, and scope, but that can reasonably be left to your hardware engineer if you aren’t ready to do it alone. As a software person, it is more important that you get as far as possible in building software that will test the hardware in a manner conducive to easy debugging.

Three kinds of tests are commonly seen for embedded systems. First, the power-on self-test (POST) runs every time you boot the system, even after the code is released. This test verifies that all of the hardware components are there to run your system safely. The more the POST tests, the longer the boot time is, so there is a trade-off that might impact the customer. Once the POST completes, the system is ready to be used by the customer.

Tip

Ideally, all POST debug messages printed out at boot time should be accessible later. Whether it is the software version string or the type of sensor attached, there will be a time when you require that information without power cycling.

The second sort of test should be run before every software release, but they might not be suitable to run at every boot, perhaps because they take too long to execute, return the system to factory default, or put unsightly test patterns on the screen. These tests verify the software and hardware are working together as expected.

These are my unit tests, though those words mean different things to different people. To me, it is automated test code that verifies a unit of source code is ready for use. Some developers want tests to cover all possible paths through the code. This can lead to unit test suites that are large and unwieldy, which is the argument used to avoid unit testing in embedded systems. My sort of unit tests are meant to test the basic functionality and the corner cases most likely to occur. This process lets me build testing into my development and keep it there, even after shipping.

Warning

Find out what your industry (or your management) expects of your unit tests. If they mean for the tests to check all software paths, make sure you (and they) understand how much work and code that will take.

Some unit tests may be external to the hardware of the system (i.e., if you are using a sandbox to verify algorithms as suggested in Chapter 2). For the ones that aren’t, I would encourage you to leave them in the production code if you can, making them run in the field upon some special set of criteria (for instance, “hold down these two buttons and cross your eyes as the system boots”). Not only will this let your quality department use the tests, but you may also find that using the unit tests as a first-line check in the field will tell you whether the system’s hardware is acting oddly.

The third and final sorts of tests are those you create during bring-up, usually because a subsystem is not functioning as intended. These are sometimes throwaway checks, superseded by more inclusive tests or added to unit tests. Temporary bring-up code is OK. The goal of this whole exercise is not to write classic source code, but to build systems. And once you’ve checked such code into your version control system, deleting it is fine because you can always recover the file if you realize you need the test again.

Building Tests

As noted earlier, the code to control peripherals (and the associated tests) often is written while the schematic is being completed. The good news is that you’ve just spent time with the datasheets, so you have a good idea how to implement the code. The bad news is that while you wait for the boards to arrive, you may end up writing drivers for six peripherals before you get to integrate your software with the hardware.

In Chapter 2, we had a system that communicated to a flash memory device via the SPI communication protocol (a partial schematic is reproduced in Figure 3-12). What do we need to test, and what tools do we need to verify the results?

I/O lines are under software control (external verification with a DMM)

SPI can send and receive bytes (external verification with a logic analyzer)

Flash can be read and written (internal verification; use the debug subsystem to output results)

Figure 3-12. Flash memory schematic snippet

To make bring-up easy, you’ll need to be able to do each of these. You might choose to run the all-inclusive flash test first. If that works, you know the other two work. However, if it doesn’t, you’ll need the others available for debugging as necessary.

Other chapters cover controlling I/O lines (Chapter 4) and a bit more about SPI (Chapter 6), so for now let’s focus on the tests that we can write to test the flash memory. How flash works isn’t critical, but you can test your skills with datasheets by looking over the Micron M25P80 flash memory datasheet (search for the part number on Google, Digikey, or the vendor’s website).

Flash Test Example

Flash memory is a type of nonvolatile memory (so it doesn’t get cleared when the power is turned off). Some other types of nonvolatile memory include ROM (read-only memory) and electrically erasable programmable read-only memory (EEPROM).

Tip

Volatile memory doesn’t retain its value after a power cycle. There are different kinds of volatile memory, but they are all one type of RAM or another.

Like an EEPROM, flash can be erased and written to. However, most EEPROMs let you erase and write a byte at a time. With flash, you may be able to write a byte at a time, but you have to erase a whole sector in order to do so. The sector size depends on the flash (usually the larger the flash, the larger each sector). For our tests component, a sector is 65,536 bytes, and the whole chip contains 8 Mbit (1 Mbyte or 16 sectors). Flash usually has more space than an EEPROM and is less power hungry. However, EEPROMs come in smaller sizes, so they are still useful.

When we write a bring-up test, nothing in the flash chip needs to be retained. For the POST, we really shouldn’t modify the flash, as the system might be using it (for storing version and graphic data). For a unit test, we’d like to leave it the way we found it after testing, but we might be able to set some constraints on that.

Tests typically take three parameters: the goal flash address, so that the test can run in an unpopulated (or noncritical) sector; a pointer to memory; and the memory length. The memory length is the amount of data the flash test will store to RAM. If the memory is the same size as the sector, no data will be lost in the flash. The following prototypes illustrate three types of test: an umbrella test that runs the others (and returns the number of errors it encounters), a test that tries to read from the flash (returning the number of bytes that were actually read), and a test that tries to write to the flash (returning the number of bytes written):

int FlashTest(uint32_t address, uint8_t *memory, uint16_t memLength);
uint16_t FlashRead(uint32_t addr, uint8_t *data, uint16_t dataLen);
uint16_t FlashWrite(uint32_t addr, uint8_t *data, uint16_t dataLen);

We’ll use the FlashTest extensively during bring-up then put it in unit tests and turn it on as needed when things change or before releases. We don’t want this test as part of the POST. Flash wears out after some number of write cycles (often 100,000; this information is in the datasheet). Further, at least two of these tests have the potential to be destructive to our data.

Plus, we don’t need to do this testing on boot. If the processor can communicate with the flash at all, then it is reasonable to believe the flash is working. (You can check basic communication by putting a header in the flash that includes a known key, a version, and/or a checksum.)

There are two ways to access this flash part: individual bytes and multibyte blocks. Later, when running the real code, you’ll probably want to use the faster block access. During initial bring-up and testing, you might want to start out with the simpler byte method to ensure a good foundation for your driver.

Test 1: Read existing data

Testing that you can read data from the flash actually verifies that the I/O lines are configured to work as a SPI port, that the SPI port is configured properly, and that you understand the basics of the flash command protocol.

For this test, we’ll read out as much data as we can from the sector so we can write it back later. Here is the start of FlashTest:

  // Test 1: Read existing data in a block just to make sure it is possible
  dataLen = FlashRead(startAddress, memory, memLength);
  if (dataLen != memLength) {
    // read less than desired, note error
    Log(LogUnitTest, LogLevelError, "Flash test: truncation on byte read");
    memLength = dataLen;
    error++;
  }

Note that there is no verification of the data, since we don’t know if this function is being run with an empty flash chip. If you were writing a POST, you might do this read and then check that the data is valid (and then stop testing, because that is enough for a power-on check).

Test 2: Byte access

The next test starts by erasing the data in the sector. Then it fills the flash with data, writing one byte at a time. We want to write it with something that changes so we can make sure the command is effective. I like to write it with an offset based on the address. (The offset tells me that I’m not accidentally reading the address back.)

  FlashEraseSector(startAddress);

  // want to put in an incrementing value but don't want it to be the address
  addValue = 0x55;
  for (i=0; i< memLength; i++) {
    value = i + addValue;
    dataLen = FlashWrite(startAddress + i, &value, 1);
    if (dataLen != 1) {
      Log (LogUnitTest, LogLevelError, "Flash test: byte write error.");
      error++;
    }
  }

To complete this check, you just need to read the data back, byte by byte, and verify the value is as expected at each address (address + addValue).

Test 3: Block access

Now that the flash contains our newly written junk, confirm that block access works by putting back the original data. That means starting with an erase of the sector again:

  FlashEraseSector(startAddress);
  dataLen = FlashWrite(startAddress, memory, memLength);
  if (dataLen != memLength) {
    LogWithNum(LogUnitTest, LogLevelError,
      "Flash test: block write error, len ", dataLen);
    error++;
  }

Finally, verify this data using another byte-by-byte read. We know that works from test 2. If the results are good, then we know that block writes work from this test and that block reads work from test 1. The number of errors is returned to the higher-level verification code.

Note

This tests the flash driver software. It doesn’t check that the flash has no sticky bits (bits that never change, even though they should). A manufacturing test can confirm that all bits change, but most flash gets verified that way before it gets to you.

Test wrap-up

If the board passes these three tests, you can be confident that the flash hardware and software both work, satisfying your need for a bring-up test and a unit test.

For most forms of memory, the pattern we’ve seen here is a good start:

Read the original data.

Write some changing but formulaic junk.

Verify the junk.

Rewrite the original.

Verify the original.

However, there are many other types of peripherals, more than I can cover here. Although automated tests are best, some will need external verification, such as an LCD that gets a pattern of colors and lines to verify its driver. Some tests will need fake external inputs so that a sensing element can be checked.

For each of your peripherals and software subsystems, try to figure out what test will give you the confidence that it is working reliably and as expected. It doesn’t sound difficult, but it can be. Designing good tests is one of those things that can make your software great.

Command and Response

Let’s say you take the advice in the previous section and create test functions for each piece of your hardware. During bring-up, you’ve made a special image that executes each test on power-up. If one of them fails, you and the hardware engineer may want to run just that test over and over. So you recompile and reload. Then you want to do the same thing for a different test. And yet another one. Wouldn’t it be easier to send your embedded system commands and have it run the tests as needed?

Embedded systems often do not have the rich user interface experience found in computers (or even smartphones). Many are controlled through a command line. Even those with screens often use a command-line interface for debugging. The first part of this section describes how to send commands in C using function pointers in a command-handling jump table. This nifty problem-and-solution gives me an opportunity to show the standard command pattern, which is a useful pattern to know, whatever language you are using.

Figure 3-13 shows some high-level goals for our automated command handler, one that will let us send a command from a serial terminal on the PC and get a response from the unit under test.

Figure 3-13. Goals for a command handler

Once you have some data read in, the code will need to figure out which function to call. A small interpreter with a command table will make the lives of those around you easier. By separating the interface from the actual test code, you will be able to extend it more easily in unforeseen directions.

Creating a command

Let’s start with a small list of commands you want to implement:

Version of the code

Outputs the version information

Test the flash

Runs the flash unit test, printing out the number of errors upon completion

Blink LED

Sets an LED to blink at a given frequency

Help

Lists available commands with one-line descriptions

Once you have a few commands implemented, adding commands as you go along should be pretty easy.

I’m going to show how to do this in C because it is probably the scariest way you’d implement it. Object-oriented languages such as C++ and Java offer friendlier ways to implement this functionality using objects. However, the C method will be smaller and generally faster, so factor that in when you are choosing how to implement this pattern. For readers unfamiliar with the C language’s function pointers, the sidebar Function Pointers Aren’t So Scary provides an introduction.

Each command we can call will be made up of a name, a function to call, and a help string:

typedef void(*functionPointerType)(void);
struct commandStruct {
  char const *name;
  functionPointerType execute;
  char const *help;
};

An array of these will provide our list of commands:

const struct commandStruct commands[] ={
  {"ver", &CmdVersion,
    "Display firmware version"},
  {"flashTest", &CmdFlashTest,
    "Runs the flash unit test, printis number of errors upon completion"},
  {"blinkLed", &CmdBlinkLed,
    "Sets the LED to blink at a desired rate (parameter: frequency (Hz))"},
  {"",0,""} //End of table indicator. MUST BE LAST!!!
};

The command execution functions CmdVersion, CmdFlashTest, and CmdBlink are implemented elsewhere. The commands that take parameters will have to work with the parser to get their parameters from the character stream. Not only does this simplify this piece of code, but it also gives greater flexibility, allowing each command to set the terms of its use, such as the number and type of parameters.

Note that the list in the previous code example doesn’t contain the help command. It is a special macro command that prints out the name and help strings of all items in the table.

Function Pointers Aren’t So Scary

Can you imagine a situation where you don’t know what function you want to run until your program is already running? For example, say you wanted to run one of several signal-processing algorithms on some data coming from your sensors. You can start off with a switch statement controlled by a variable:

  switch (algorithm) {
    case eFIRFilter:
      return fir(data, dataLen);
    case eIIRFilter:
      return iir(data, dataLen);
    ...
  }

Now when you want to change your algorithm, you send a command or push a button to make the algorithm variable change. If the data gets processed continually, the system still has to run the switch statement, even if you didn’t change the algorithm.

In object-oriented languages, you can use a reference to an interface. Each algorithm object would implement a function of the same name (for this signal-processing example, we’d call it filter). Then when the algorithm needed to change, the caller object would change. Depending on the language, the interface implementation could be indicated with a keyword or through inheritance.

C doesn’t have those features (and using inheritance in C++ may be verboten in your system due to compiler constraints). So we come to function pointers, which can do the same thing.

To declare a function pointer, you need the prototype of a function that will go in it. For our switch statement, that would be a function that took in a data pointer and data length and didn’t return anything. However, it could return results by modifying its first argument. The prototype for one of the functions would look like this:

void fir(uint16_t* data, uint16_t dataLen);

Now take out the name of the function and replace it with a star and a generic name surrounded by parentheses:

void (*filter)(uint16_t* data, uint16_t dataLen);

Instead of changing your algorithm variable and calling the switch statement to select a function, you can change the algorithm only when needed and call the function pointer:

  filter = &fir;
  *filter(data, dataLen);

Once you are comfortable with the idea of function pointers, they can be a powerful asset when the code is supposed to adapt to its environment. Some common uses for function pointers include the command structure described in this chapter, callbacks to indicate a completed event, and mapping button presses to context-sensitive actions.

One caution: excessive use of function pointers can cause your processor to be slow. Some processors try to predict where your code is going and load the appropriate instructions before execution. Function pointers inhibit branch prediction because the processor can’t guess where you’ll be after the call.

However, other methods of selecting on-the-fly function calls also interrupt branch prediction (such as the switch statement we started with). Unless you have reached the stage of hand-tuning the assembly code, it generally isn’t worth worrying about the slight slowdown caused by the awesome powers of the function pointer.

Invoking a command

Once the client on the command line indicates which command to run by sending a string, you need to choose the command and run it. To do that, go through the table, looking for a string that matches. Once you find it, call the function pointer to execute that function.

Compared to the previous section, this part seems almost too easy. That is the goal. Embedded systems are complex, sometimes hideously so, given their tight constraints and hidden dependencies. The goal of this (and many other patterns) is to isolate the complexity. You can’t get rid of the complexity; a system that does nothing isn’t very complex, but it isn’t very useful either. There is still a fair amount of complexity in setting up the table and the parsing code to use it, but those details can be confined to their own spaces.

Each command may perform the desired action itself or call another function (the receiver) to perform the action. For example, the ver command may simply print out the version. On the other hand, the blinkLed command will probably call whatever function already exists to set the interface to the LED and set a timer to make it blink. This function, the receiver, is the code that the user really wanted to talk to.

If a command implements the receiver (like the version command does), it is called a smart command. This is a misnomer, as separating the command and the receiver is usually smarter because it leads to a more easily extensible system.

Command Pattern

What we’ve been looking at is a formal, classic design pattern. Where the command handler I’ve described is a tactical solution to a problem, the command pattern is the strategy that guides it and other designs like it.

The overarching goal of this pattern is to decouple the command processing from the actual action to be taken. The command pattern shows an interface for executing operations. Any time you see a situation where one piece of a system needs to make requests of another piece without the intermediaries knowing the contents of those requests, consider the command pattern.

As shown in Figure 3-14, there are four pieces:

Figure 3-14. How the command handler works

Client

Maps the receivers onto commands. Clients create concrete command objects and create the association between receivers and command objects. A client may run at initialization (as was done in our example by creating an array) or create the associations on the fly.

Invoker

Determines when the command needs to run and executes it. It doesn’t know anything about the receiver. It treats every command the same.

Command

An interface for executing an operation. This is a C++ class, a Java interface, or a C structure with a function pointer. An instantiation of the command interface is called a concrete command.

Receiver

Knows how to service the request. Running the receiver code is the goal of the command-sender.

Figure 3-15 shows the purpose of each element of the command pattern.

Figure 3-15. Command pattern and the knowledge barrier

Tip

Consider the client as protecting the secrets of the system by disguising all of the commands to look alike. This decouples the commands from your code making it much easier to expand later (which you will need to do).

The command interface can be richer, possibly adding a log command, a help function, and an undo function. The invoker will know when to call these, but it will be up to the client to build each of these commands with the actual implementation.

Because the details are hidden, it is pretty straightforward to add a macro command that implements multiple commands. To do this, the client creates a list of the basic commands to be combined. When invoked, the macro calls the execute function on each of its subcommands (or undo or log). For example, a macro to print the version (ver) and set the LED blinking (blinkLED) would call the execute functions of those commands, using the available parameters in the order the functions are called. This provides a better layer of decoupling than if the macro called the receiver functions directly.

Dealing with Errors

The longevity of code shocks me. For all that it sometimes seems like we are rewriting the same old things in different ways, one day you may discover that a piece of code you wrote at a start-up over a decade ago is being used by a Fortune 500 company. Once it works well enough, why fix the depths of code?

It only makes it scarier to know that at some point your code will fail. An error will occur, either from something you wrote or from an unexpected condition in the environment. There are two ways to handle errors. First, the system can enter a state of graceful degradation, where the software does the best it can. Alternatively, the system could fail loudly and immediately. Long-term sensor-type systems require the former, whereas medical systems require the latter. Either way, a system should fail safely.

But how to implement either one? More importantly, what criteria should be used to determine which subsystems implement which error-handling method? What you do depends on your product; my goal is to get you to think about error handling during design.

Consistent Methodology

Functions should handle errors as best they can. For example, if a variable can be out of range, the range should be fixed and the error logged as appropriate. Functions can return errors to allow a caller to deal with the problems. The caller should check and deal with the error, which may mean passing it further upstream in a multilayered application. In many cases, if the error is not important enough to check, it is not important enough to return. On the other hand, there are cases where you want to return a diagnostic code that is used only in testing. It can be noted in the comments that the returned error code is not for normal usage or should be used only in ASSERT() calls.

ASSERT() is not always implemented in embedded systems, but it is straightforward to implement in a manner appropriate to the system. This might be a message printed out on the debugger console, a print to a system console or log, a breakpoint instruction such as BKPT, or even an I/O line or LED that toggles on an error condition. Printing changes the embedded system’s timing, so it is often beneficial to separate the functions for error communications to allow other methods of output (e.g., an LED).

Error return codes for an application or system should be standardized over the code base. A single high-level errorCodes.h file (or some such) can be created to provide consistent errors in an enumerated format. Some suggested error codes are:

No error (should always be 0)

Unknown error (or unrecognized error)

Bad parameter

Bad index (pointer outside range or null)

Uninitialized variable or subsystem

Catastrophic failure (this may cause a processor reset unless it is in a development mode, in which case it probably causes a breakpoint or spin loop)

There should be a minimum number of errors—generic errors—so that the application can interpret them. Although specificity is lost (UART_FAILED_TO_INIT_BECAUSE_SECOND_PARAMETER_WAS_TOO_HIGH), the generalization makes error handling and usage easier (if the error PARAMETER_BAD occurs in a subsystem, you have a good place to start to look for it). Essentially, by keeping it simple, you make sure to hand the developer the important information (the existence of a bug), and additional debugging can dig into where and why.

Error-Handling Library

An error-handling library is also a good idea. One way to implement this is to have each function return an error code. Instead of calling the function and checking the results like this:

error = FunctionFoo();
if (error != NO_ERROR) {
  ErrorSet (&globalErrorCode, error);
}

you’d call the function inside an error-checking function:

ErrorSet(&globalErrorCode, FunctionFoo());

The ErrorSet function would not overwrite a previous error condition if this function returned without a failure. This allows you to call several functions at a time, checking the error at the end instead of at each call.

In such an error-handling library, there would be four functions, implemented and used as makes sense with the application: ErrorSet, ErrorGet, ErrorPrint, and ErrorClear. The library should be designed for debugging and testing, though the mechanism should be left in place, even when development is complete. For example, ErrorPrint may change from writing out a log of information on a serial port to a small function that just toggles an I/O line. This is not an error for the final user to deal with; it is for a developer confronted with production units that do not perform properly.

Debugging Timing Errors

When debugging hardware/software interactions (or any software that is time critical), serial output such as logging or printf can change the timing of the code. In many cases, a timing problem will appear (or disappear) depending on where your output statements and/or breakpoints are located. Debugging becomes difficult when your tools interact with the problem.

One of the advantages to using an error library (or a logging library, as described in Chapter 2) is that you don’t have to output the data; you can store it locally in RAM, which is usually a much faster proposition. In fact, if you are having trouble with timing, consider making a small buffer (4–16 bytes, depending on your RAM availability) to contain signals from the software (one or two bytes). In your code, fill the buffer at interesting trigger points (e.g., an ASSERT or ErrorSet). You can dump this buffer when the code exits the time-sensitive area. If you are most interested in the last error, use a circular buffer to continually capture the last few events (circular buffers are discussed in more detail in Chapter 6).

Alternatively, if you have some input into the board design, I highly recommend pushing for spare processor I/Os to be available on the board and easily accessible with a header. They will come in useful for debugging (especially tricky timing issues where serial output breaks the timing), showing the status of the system in testing, and profiling processor cycles.

Further Reading

If you want to learn more about handling hardware safely, reading schematics, and soldering, I suggest Make: Electronics by Charles Platt, published by O’Reilly (2009). This step-by-step introduction is an easy read, though better if you follow along with components in hand.

To get a better look into hardware design, I suggest Designing Embedded Hardware by John Catsoulis, published by O’Reilly (2005).

For more about writing good tests, pick up Test-Driven Development for Embedded C by James W. Grenning, published by Pragmatic Bookshelf (2011).

Learning to use your oscilloscope probably should be done with its manual, but I’ve found one useful, more general write-up that is very detailed. Alternatively, there are some videos on YouTube about setting up O-scopes; sometimes watching someone do it is easier than reading about it.

Interview Question: Talking About Failure

Tell me about a project that you worked on that was successful. Then, tell me about a project that you worked on that was not so successful. What happened? How did you work through it? (Thanks to Kristin Anderson for this question.)

The goal of this question is not really about judging an applicant’s previous successes and failures. The question starts there because the processes that lead to success (or failure) depend on knowledge, information, and communication. By talking about success or failure, the applicant reveals what he learns from available information, how he seeks out information, and how he communicates important information to others on the team. If he can understand the big picture and analyze the project’s progress, he’s more likely to be an asset to the team.

The successful half of the question is interesting to listen to, particularly when the interviewee is excited and passionate about his project. I want to hear about his part in making it successful and the process he used. However, like many of the more technical questions, the first half of the question is really about getting to the second half of the question.

Did he understand all the requirements before jumping into the problem, or were there perhaps communication gaps between the requirements set by the marketing group and the plans developed by the engineers? Did the applicant consider all the tasks and even perhaps Murphy’s Law before developing a schedule and then add some time for risk?

Did he bring up anything relative to any risks or issues he faced and any mitigation strategies he considered? Some of these terms are program management terms, and I don’t really expect the engineers to use all the right buzz words. But I do expect them to go beyond talking about the technical aspects and be able to tell me other factors in successful projects.

Often, in talking about successful projects, an applicant will say he understood the requirements and knew how to develop the right project and make people happy. That is enough to go on to the next question.

Then, when I ask what went wrong, I really want to hear how the applicant evaluated the factors involved in trying for a successful project and whether he can tell me why he thought the project failed (poor communication, lack of clear goals, lack of clear requirements, no management support, etc.). There is no right or wrong answer; what I’m interested in is how he analyzes the project to tell me what went wrong. I am disappointed by the interviewees who cannot think of an unsuccessful project, because no project goes perfectly, so I tend to think they weren’t paying attention to the project as a whole. Understanding more than just what he has to do technically makes an engineer a much better engineer.

Chapter 4. Outputs, Inputs, and Timers

From pin configuration registers, to debouncing buttons, to timers, this chapter will describe the most basic embedded concepts, focusing on the input and output aspects of the system. To keep it interesting, we’ll work through an example product, going through the phases of a project as the vision appears, changes, and finally crystallizes. Even the simple-sounding subject of making an LED blink is not immune to changes in product goals.

Toggling an Output

Marketing has come to you with an idea for a product. When you see through the smoke and mirrors, you realize that all they need is a light to blink.

Most processors have pins whose digital states can be read (input) or set (output) by the software. These go by the name of I/O pins, general-purpose I/O (GPIO), and occasionally general I/O (GIO). The basic use case is usually straightforward, at least when an LED is attached:

Initialize the pin to be an output (as an I/O pin, it could be input or output).

Set the pin high when you want the LED on. Set the pin low when you want the LED off. (Although the LED also can be connected to be on when the pin is low, but this example will focus on noninverted logic.)

Through this chapter, I’ll give you examples from three different user manuals so you get an idea of what to expect in your processor’s documentation. Atmel’s ATtiny AVR microcontroller manual describes an 8-bit microcontroller with plenty of peripherals. The TI MSP430x2xx User’s Guide describes a 16-bit RISC processor designed to be ultra low power. The NXP LPC13xx User Manual describes a 32-bit ARM Cortex microcontroller. You won’t need these documents to follow along, but I thought you might like to know the processors the examples are based upon.

Starting with Registers

To do anything with an I/O line, we need to talk to the appropriate register. As described in Reading a Datasheet, you can think of registers as an API to the hardware. Described in the chip’s user manual, registers come in all flavors to configure the processor and control peripherals. They are memory-mapped, so you can write to a specific address to modify a particular register.

As you work with registers, you will need to think about things at the bit level. Often you’ll turn specific bits on and off. If you’ve never used bitwise operations, now is the time to look them up. In C and C++, a bitwise-OR is | and bitwise-AND is &. The logical NOT operator (!) turns a 1 into a 0 (or a true into a false), and vice versa. The bitwise NOT (~) sets each bit to its opposite value:

register = register | (1 << 3); // turn on the 3rd bit in the register
register |= 1 << 3;             // same, but more concisely
register &= ~(1 << 3);          // turn off the 3rd bit in the register

Enough review. If this isn’t pretty obvious, look into bitwise operations and Boolean math. You will need to know these pretty well to use registers.

An Introduction to Binary and Hexadecimal

Becoming familiar with basic binary and hexadecimal (hex) math will make your career in embedded systems far more enjoyable. Shifting individual bits around is great when you need to modify only one or two places. But if you need to modify the whole variable, hex comes in handy because each digit in hex corresponds to a nibble (four bits) in binary. (Yes, of course a nibble is half of a byte. Embedded folks like their puns.)

Binary | Hex | Decimal | Remember this number
0000 | 0 | 0 | This one is easy.
0001 | 1 | 1 | This is (1 << 0).
0010 | 2 | 3 | This is (1<< 1). Shifting is the same as multiplying by 2shiftValue.
0011 | 3 | 3 | Notice how in binary this is just the sum of one and two.
0100 | 4 | 4 | (1 << 2)
0101 | 5 | 5 | This is an interesting number because every other bit is set.
0110 | 6 | 6 | See how this looks like you could shift the three over to the left by one? This could be put together as ((1<<2)|(1<<1)), or ((1<<2) + (1<<1)), or (3 << 1)).
0111 | 7 | 7 | Look at the pattern of binary bits. They are very repetitive. Learn the pattern, and you’ll be able to generate this table if you need to.
1000 | 8 | 8 | (1 << 3). See how the shift and the number of zeros are related? If not, look at the binary representation of 2 and 4.
1001 | 9 | 9 | We are about to go beyond the normal decimal numbers. Because there are more digits in hexadecimal, we’ll borrow some from the alphabet. In the meantime, 9 is just 8 + 1.
1010 | A | 10 | This is another special number with every other bit set.
1011 | B | 11 | See how the last bit goes back and forth from 0 to 1? It signifies even and odd.
1100 | C | 12 | Note how C is just 8 and 4 combined in binary? So of course it equals 12.
1101 | D | 13 | The second bit from the right goes back and forth from 0 to 1 at half the speed of the first bit: 0, then 0, then 1, then 1, then repeat.
1110 | E | 14 | The third bit also goes back and forth, but at half the rate of the second bit.
1111 | F | 15 | All of the bits are set. This is an important one to remember.

Note that with four bits (one hex digit) you can represent 16 numbers, but you can’t represent the number 16. Many things in embedded systems are zero-based, including addresses, so they map well to binary or hexadecimal numbers.

A byte is two nibbles, the left one being shifted up four spaces from the other. So 0x80 is (0x8 << 4). A 16-bit word is made up of two bytes, so 0x1234 is (0x12 << 8) + (0x34). A 32-bit word is 8 characters long in hex but 10 characters long in decimal.

Since memory is generally viewed in hex, some values are used to identify anomalies in memory. In particular, expect to see (and use) 0xDEADBEEF or 0xDEADC0DE as an indicator (they are a lot easier to remember in hex than in decimal). Two other important bytes are 0xAA and 0x55. Because the bits in these numbers alternate, they are easy to see on an oscilloscope and good for testing when you want to see a lot of change in your values.

Set the Pin to Be an Output

Most I/O pins can be either inputs or outputs. The first register you’ll need to set will control the direction of the pin so it is an output. First, determine which pin you will be changing. To modify the pin, you’ll need to know the pin name (“I/O pin 2,” not its number on the processor (for example, pin 12). The names are often inside the processor in schematics where the pin number is on the outside of the box (as shown in Figure 4-1).

Figure 4-1. Schematic of a processor with an LED attached

The pins may have multiple numbers in their name, indicating a port (or bank) and a pin in that port. (The ports may also be letters instead of numbers.) In the figure, the LED is attached to processor pin 10, which says “SCLK/IO1_2.” This pin is shared between the SPI port (remember, this is a communication method, discussed in Chapter 6) and the I/O subsystem (IO1_2). The user manual will tell you whether the pin is an I/O by default or a SPI pin (and how to switch between them). Another register may be needed to indicate the purpose of the pin. Most vendors are good about cross-referencing the pin setup, but if it is shared between peripherals, you may need to look in the peripheral section to turn off unwanted functionality. In our example, we’ll say the pin is an I/O by default.

In the I/O subsystem, it is the second pin (2) of the first bank (1). We’ll need to remember that and to make sure the pin is used as an I/O pin and not as a SPI pin. Your processor user manual will describe more. Look for a section with a name like “I/O Configuration,” “Digital I/O Introduction,” or “I/O Ports.” If you have trouble finding the name, look for the word “direction,” which tends to be used to describe whether you want the pin to be an input or an output.

Once you find the register in the manual, you can determine whether you need to set or clear a bit in the direction register. In most cases, you need to set the bit to make the pin an output. You could determine the address and hardcode the result:

*((int*)0x0070C1) |= (1 << 2);

However, please don’t do that.

The processor or compiler vendor will almost always provide a header that hides the memory map of the chip so you can treat registers as global variables. If they didn’t give you a header, make one for yourself so that your code looks more like one of these lines:

LPC13xx processor

LPC_GPIO1->DIR |= (1 << 2); // set IO1_2 to be an output

MSP430 processor

P1DIR |= BIT2;              // set IO1_2 to be an output

ATtiny processor

DDRB |= 0x4;                // set IOB_2 to be an output

Note that the register names are different for each processor, but the effect of the code in each line is the same. Each processor has different options for setting the second bit in the byte (or word).

In each of these examples, the code is reading the current register value, modifying it, and then writing the result back to the register. This read-modify-write cycle needs to happen in atomic chunks. If you read the value, modify it, and then do some other stuff before writing the register, you run the risk that the register has changed and the value you are writing is out of date. The register modification will change the intended bit, but also might have unintended consequences.

Turn On the LED

The next step is to turn on the LED. Again, we’ll need to find the appropriate register in the user manual.

LPC13xx processor

LPC_GPIO1->DATA |= (1 << 2); // IO1_2 high

MSP430 processor

P1OUT |= BIT2;             // IO1_2 high

ATtiny processor

PORTB |= 0x4;              // IOB_2 high

The header file provided by the processor or compiler vendor shows how the raw addresses get masked by some programming niceties. In the LPC13xx.h, the I/O registers are accessed at an address through a structure (I made some reorganization and simplifications to the file):

typedef struct{
  __IO uint32_t DATA;
  uint32_t RESERVED0[4095]; // The same data appears at 4096 locations
                            // in the GPIO address space, and 12 bits
                            // of the address bus can be used for
                            // bit masking. (See manual 7-4.1)

  __IO uint32_t DIR;  // direction set for output, clear for input
  __IO uint32_t IS;   // interrupt sense (1 = interrupt pending)
  __IO uint32_t IBE;  // interrupt on both falling and rising edges
  __IO uint32_t IEV;  // interrupt event register
  __IO uint32_t IE;   // interrupt enable
  __IO uint32_t RIS;  // raw status register
  __IO uint32_t MIS;  // masked interrupt status register
  __IO uint32_t IC;   // interrupt clear (set bit to clear interrupt)
} LPC_GPIO_TypeDef;

#define LPC_AHB_BASE          (0x50000000UL)
#define LPC_GPIO0_BASE        (LPC_AHB_BASE  + 0x00000)
#define LPC_GPIO1             ((LPC_GPIO_TypeDef   *) LPC_GPIO1_BASE )

The header file describes many registers we haven’t looked at. These are also in the user manual section, with a lot more explanation. I prefer accessing the registers via the structure because it groups related functions together, often letting you work with the ports interchangeably.

Tip

You may want to update a vendor header file to use this structured approach. Approach this free code as a starting point, not the final draft. Modify it as needed to work in your system.

Once we have the LED on, we’ll need to turn it off again. You just need to clear those same bits, as shown in the register section (Starting with Registers):

LPC13xx processor

LPC_GPIO1->DATA &= ~(1 << 2);    // IO1_2 low

MSP430 processor

LPC13xx P1OUT &= ~(BIT2);      // IO1_2 low

ATtiny processor

PORTB &= ~0x4;                 // IOB_2 low

Blinking the LED

To finish our program, all we need to do is put it all together. The pseudocode for this is:

main:
  initialize the direction of the I/O pin to be an output
loop:
  set the LED on
  do nothing for some period of time
  set the LED off
  do nothing for the same period of time
  repeat

Once you’ve programmed it for your processor, you should compile, load, and test it. You might want to tweak the delay loop so the LED looks about right. It will probably require several tens of thousands of processor cycles, or the LED will blink faster than you can perceive it.

Troubleshooting

If you have a debugging system such as JTAG set up, finding out why your LED won’t turn on is likely to be straightforward. Otherwise, you may have to use the process of elimination.

First, check to see whether a pin is shared between different peripherals. While we said that the pin was an I/O by default, if you are having trouble, verify that its functionality is as expected.

As long as you have the manual open, verify that the pin is configured properly. If the LED isn’t responding, you’ll need to read the peripherals chapter of the user manual. Processors are different, so check that the pin doesn’t need additional configuration (e.g., a power control output) or have a feature turned on by default (do not make the GPIO an open-drain output by accident).

Most processors have I/O as a default because that is the simplest way for their users (us!) to verify the processor is connected correctly. However, particularly with low-power processors, they want to keep all unused subsystems off to avoid power consumption. (And other special-purpose processors may have other default functionality.) The user manual will tell you more about the default configuration and how to change it. These are often other registers that need a bit set (or cleared) to make a pin act as an I/O.

Next, make sure the system is running your code. Do you have another way to verify that the code being run is the code that you compiled? If you have a debug serial port, try incrementing the revision to verify that the code is getting loaded.

Make the code as simple as possible to be certain that the processor is running the function handling the LEDs. Eliminate any noncritical initialization of peripherals in case the system is being delayed while waiting for a nonexistent external device. Turn off interrupts and asserts. Make sure the watchdog is off (see Watchdog). Put the LED code in the larger piece of code as early as possible, to reduce the likelihood that any other code is freezing the processor.

Double-check your math. Even if you are completely comfortable with hex and bit shifting, a typo is always possible. In my experience, typos are the most difficult bugs to catch, often harder than memory corruptions. Check you are using the correct pin on the schematic. And make sure there is power to the board. (You may think that advice is funny, but you’d be surprised at how often this plays a role!)

With many microcontrollers, pins can sink more current than they can source (provide). Therefore, it is not uncommon for the pin to be connected to the cathode rather than the anode of the LED. In these instances, you turn on an LED by writing a zero rather than a one.

If the output still doesn’t work, consider whether there is a hardware issue. Even in hardware, it can be something simple (installing LEDs backward is pretty easy). It may be a design problem such as the processor pin being unable to provide enough current to drive the LED; the datasheet (or user manual) might be able to tell you this. There might be a problem on the board, such as a broken component or connection. With the high-density pins on most processors, it is very easy to short pins together. Ask for help, or get out your multimeter (or oscilloscope).

Separating the Hardware from the Action

Marketing liked your first prototype, though they might want to tweak it a bit later. The system went from a prototype board to a PCB. During this process, somehow the pin number changed (to IO1_3). They need to be able to run both systems.

It is trivially simple to fix the code for this project, but for a larger system, the pins may be scrambled to make way for a new feature. Let’s look at how to make modifications simpler.

Board-Specific Header File

Using a board-specific header file lets you avoid hardcoding the pin. If you have a header file, you just have to change a value there instead of going through your code to change it everywhere it’s referenced. The header file might look like this:

#define LED_SET_DIRECTION  (P1DIR)
#define LED_REGISTER       (P1OUT)
#define LED_BIT            (1 << 3)

The lines of code to configure and blink the LED can be processor-independent:

LED_SET_DIRECTION |= LED_BIT; // set the I/O to be output
LED_REGISTER |= LED_BIT;      // turn the LED on
LED_REGISTER &= ~LED_BIT;     // turn the LED off

That could get a bit unwieldy if you have many I/O lines or need the other registers. It might be nice to be able to give only the port (1) and position in the port (3) and let the code figure it out. The code might be more complex, but it is likely to save time (and bugs). For that, the header file would look like this:

// ioMapping_v2.h
#define LED_PORT 1
#define LED_PIN 3

If we want to recompile to use different builds for different boards, we can use three header files. The first is the old board pin assignments (ioMapping_v1.h). Next, we’ll create one for the new pin assignment (ioMapping_v2.h). We could include the one we need in our main .c file, but that defeats the goal of modifying that code less. If we have the main file always include a generic ioMapping.h, we can switch the versions in the main file by including the correct header file:

// ioMapping.h
#if COMPILING_FOR_V1
#include "ioMapping_v1.h"
#elif COMPILING_FOR_V2
#include "ioMapping_v2.h"
#else
#error "No I/O map selected for the board. What is your target?"
#endif /* COMPILING_FOR_*/

Using a board-specific header file hardens your development process against future hardware changes. By sequestering the board-specific information from the functionality of the system, you are creating a more loosely coupled and flexible code base.

Tip

Keeping the I/O map in Excel is a pretty common way to make sure the hardware and software engineers agree on the pin definitions. With a little bit of creative scripting, you can generate your version-specific I/O map from a CSV file to ensure your pin identifiers match those on the schematic.

I/O-Handling Code

Instead of writing directly to the registers in the code, we’ll need to handle the multiple ports in a generic way. So far we need to initialize the pin to be an output, set the pin high so the LED is on, and set the pin low so the LED is off. Oddly enough, we have a large number of options for putting this together, even for so simple an interface.

In the implementation, the initialization function configures the pin to be an output (and sets it to be an I/O pin instead of a peripheral if necessary). With multiple pins, you might be inclined to group all of the initialization together, but that breaks the modularity of the systems.

Although the code will take up a bit more space, it is better to have each subsystem initialize the I/Os it needs. Then if you remove or reuse a module, you have everything you need in one area. However, we’ve seen one situation where you should not separate interfaces into subsystems: the I/O mapping header file, where all of the pins are collected together to make the interface with the hardware more easily communicated.

Moving on with the I/O subsystem interface, setting a pin high and low could be done with one function: IOWrite(port, pin, high/low). Alternatively, this could be broken out so that there are two functions: IOSet(port, pin) and IOClear(port, pin). Both methods work. Imagine what our main function will look like in both cases.

The goal is to make the LED toggle. If we use IOWrite, we can have a variable that switches between high and low. In the IOSet and IOClear case, we’d have to save that variable and check it in the main loop to determine which function to call. Alternatively, we could hide IOSet and IOClear within another function called IOToggle.

XOR

XOR (exclusive or) is a somewhat magical bitwise operation. It doesn’t have a logical analog and isn’t used as much as the others we’ve seen, so remembering it can be tough.

Imagine your nemesis puts on his blog that he is going to the movies this evening, which is just what you had planned to do. If you both avoid going, the movie won’t play, because there won’t be an audience. One of you can go to see the movie. But if you both go, the movie won’t play (the theater is still not happy about the fuss you caused last time).

The truth table looks like:

Input A | Input B | Output
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

XOR is often represented as a Venn diagram, as shown in Figure 4-2. Note that XOR is true inside each circle, but not where they overlap.

Figure 4-2. XOR Venn diagram

XOR has some nifty applications in computer graphics and finding overflows (math errors). You can also toggle our LED on and off using XOR:

register = register ^ (1 << 2);

Note that (1 << 2) would always be a 1 in the table. If it is Input A, we are using only the bottom half of the chart. If the register (Input B) already has that pin set, what would the output be?

We don’t have any particular constraints with our hardware, so we don’t need to consider optimizing the code in this example. For education’s sake, however, consider the options we are giving ourselves with these potential interfaces.

The IOWrite option does everything in one function, so it takes less code space. However, it has more parameters, so it takes more stack space, which comes out of RAM. Plus it has to keep around a state variable (also RAM).

With the IOSet/IOClear/IOToggle option, there are more functions (more code space), but fewer parameters and possibly no required variables (less RAM). Note that the toggle function is no more expensive in terms of processor cycles than the set and clear functions.

This sort of evaluation requires you to think about the interface along another dimension. Chapter 8 will go over more details on how to optimize for each area. During the prototyping phase, it is too soon to optimize the code, but it is never too soon to consider how the code can be designed to allow for optimization later.

Main Loop

The modifications in the previous sections put the I/O-handling code in its own module, though the basics of the main loop don’t change. The implementation might look like the following:

void main(void){
  IOSetDir(LED_PORT, LED_PIN, OUTPUT);
  while (1) { // spin forever
    IOToggle(LED_PORT, LED_PIN);
    DelayMs(DELAY_TIME);
  }
}

The main function is no longer directly dependent on the processor. With this level of decoupling, the code is more likely to be reused in other projects. In (a) of Figure 4-3, the original version of software architecture is shown, with its only dependency being the processor header. The middle, labeled (b), is our current version. It is more complicated, but the separation of concerns is more apparent. Note that the header files are put off to the side to show that they feed into the dependencies.

Our next reorganization will create an even more flexible and reusable architecture, illustrated by (c).

Figure 4-3. Comparison of different architectures

Facade Pattern

As you can imagine, our I/O interface is going to get more complex as the product features expand. (Currently we have only one output pin, so it can’t really get any simpler.) In the long run, we want to hide the details of each subsystem. There is a standard software design pattern called facade that provides a simplified interface to a piece of code. The goal of the facade pattern is to make a software library easier to use. Along the lines of the metaphor I’ve been using in this book, that interfacing to the processor is similar to working with a software library, it makes sense to use the facade pattern to hide some details of the processor and the hardware.

In the section From Diagram to Architecture, we saw the adapter pattern, which is a more general version of the facade pattern. Whereas the adapter pattern acted as a translation between two layers, the facade does this by simplifying the layer below it. If you were acting as an interpreter between scientists and aliens, you might be asked to translate “x=y+2, where y=1.” If you were an adapter pattern, you’d restate the same information without any changes. If you were a facade pattern, you’d probably say “x=3” because it is simpler and the details are not critical to using the information.

Hiding details in a subsystem is an important part of good design. It makes the code more readable and more easily tested. Furthermore, the calling code doesn’t depend on the internals of the subsystem, so the underlying code can change, while leaving the facade intact.

A facade for our blinking LED would hide the idea of I/O pins from the calling code by creating an LED subsystem, as shown in the right side of Figure 4-3. Given how little the user needs to know about the LED subsystem, the facade could be implemented with only two functions:

LEDInit()

Calls the I/O initialization function for the LED pin (replaces IOSetDir(…))

LEDBlink()

Blinks the LED (replaces IOToggle(…))

Adding a facade will often increase the size of your code, but may be worth it in terms of debuggability and maintainability.

The Input in I/O

Marketing has determined that they want to change the way the system blinks in response to a button. Now, when the button is held down, the system should stop blinking altogether.

Our schematic is not much more complex with the addition of a button (see Figure 4-4). Note that the button uses IO2_2 (I/O port 2, pin 2), which is denoted with S1 (switch 1). The icon for a switch makes some sense; when you push it in, it conducts across the area indicated. Here, when you press the switch, the pin will be connected to ground.

Figure 4-4. Schematic with an LED and a button

Many processor I/O pins have internal pull-up resistors. When a pin is an output, the pull-ups don’t do anything. However, when the pin is an input, the pull-up gives it a consistent value (1), even when nothing is attached. The existence and strength of the pull-up may be settable, but this depends on your processor (and possibly on the particular pin). Some processors even have an option to allow internal pull-downs on a pin. In that case, our switch could have been connected to power instead of ground.

Tip

Inputs with internal pull-ups take a bit of power, so if your system needs to conserve a few microamps, you may end up disabling the unneeded pull-ups.

Your processor user manual will describe the pin options. The basic steps for setup are:

Add the pin to the I/O map header file.

Configure it to be an input. Verify that it is not part of another peripheral.

Configure a pull-up explicitly (if necessary).

Once you have your I/O pin set up as an input, you’ll need to add a function to use it, one that can return the state of the pin as high (true) or low (false):

boolean IOGet(uint8_t port, uint8_t pin);

The button will connect to ground when the button is pressed. This signal is active low, meaning that when the button is actively being held down, the signal is low.

A Simple Interface to a Button

To keep the details of the system hidden, we’ll want to make a button subsystem that can use our I/O-handling module. On top of the I/O function, we can put another facade so that the button subsystem will have a simple interface.

The I/O function returns the level of the pin. However, we want to know whether the user has taken an action. Instead of the button interface returning the level, you can invert the signal to determine whether the button is currently pressed. The interface could be:

void ButtonInit()

Calls the I/O initialization function for the button

boolean ButtonPressed()

Returns true when the button is down

As shown in Figure 4-5, both the LED and button subsystems use the I/O subsystem and I/O map header file. This is a simple illustration of how the modularization we did earlier in the chapter allows reuse.

Figure 4-5. Architecture with the button

At a higher level, there are a few ways to implement the main function:

main:
  initialize LED
  initialize button
loop:
  if button pressed, turn LED off
  else toggle LED
  do nothing for a period of time
  repeat

With this code, the LED won’t go off immediately, but will wait until the delay has expired. The user may notice some lag between pushing the button and the LED turning off.

Tip

A system that doesn’t respond to a button press in less than a quarter of a second (250 ms) feels sluggish and difficult to use. A response time of 100 ms is much better, but still noticeable to impatient people. A response time under 50 ms feels very snappy.

To decrease the response time, we could check constantly to see whether the button was pressed:

loop:
  if button pressed, turn LED off
  else
    if enough time has passed,
      toggle LED
      clear how much time has passed
  repeat

Both of these methods check the button to determine whether it is pressed. This continuous querying is called polling and is easy to follow in the code. However, if the LED needs to be turned off as fast as possible, you may want the button to interrupt the normal flow.

Wait! That word (interrupt) is a really important one. I’m sure you’ve heard it before, and we’ll get into a lot more detail soon (and again in Chapter 5). Before that, look at how simple the main loop can be if you use an interrupt to capture and handle the button press:

loop:
  if button not pressed, toggle LED
  do nothing for a period of time
  repeat

The interrupt code (aka ISR, or interrupt service routine) would call the function to turn off the LED. However, this makes the button and LED subsystems depend on each other, coupling the systems together in an unobvious way. There are times where you’ll have to do this so an embedded system can be fast enough to handle an event.

Chapter 5 will describe how and when to use interrupts in more detail. This chapter will continue to look at them at a high level only.

Momentary Button Press

Instead of using the button to halt the LED, marketing wants to test different blink rates by tapping the button. For each button press, the system should decrease the amount of delay it has (until it gets to near zero, at which point it should go back to the initial delay).

In the previous assignment, all you had to check was whether the button was pressed down. This time you have to know both when the button will be pressed and when it will be released. Ideally, we’d like the switch to look like the top part of Figure 4-6. If it did, we could make the system note the rising edge of the signal and take an action there.

Interrupt on a Button Press

This might be another area where an interrupt can help us catch the user input so the main loop doesn’t have to poll the I/O pin so quickly. The main loop becomes straightforward if it uses a global variable to learn about the button presses:

interrupt when the user presses the button:
  set global button pressed = true

loop:
  if global button pressed,
    set the delay period (reset or decrease it)
    set global button pressed = false
  if enough time has passed,
    toggle LED
    clear how much time has passed
  repeat

The input pins on many processors can be configured to interrupt when the signal at the pin is at a certain level (high or low) or has changed (rising or falling edge). If the button signal looks like it does in the ideal button signal at the top of Figure 4-6, where would you want to interrupt? Interrupting when the signal is low may lead to multiple activations if the user holds the button down. I prefer to interrupt on the rising edge so that when the user presses the button down, nothing happens until she releases it.

Warning

To check a global variable accurately in this situation, you’ll need the volatile C keyword, which perhaps you've never needed before now when developing software in C and C++. The keyword tells the compiler that the value of the variable or object can change unexpectedly and should never be optimized out. All registers and all global variables shared between interrupts and normal code should be marked as volatile. If your code works fine without optimizations and then fails when optimizations are on, check that the appropriate globals and registers are marked as volatile.

Configuring the Interrupt

Setting a pin to be an interrupt usually is separate from setting the function to set a pin to be an input. Although both are part of initialization, you should save the complexity of interrupt configuration for the pins that require it.

Configuring a pin for interrupting the processor adds three more functions to our I/O subsystem:

IOConfigureInterrupt(port, pin, trigger type, trigger state)

Configures a pin to be an interrupt. The interrupt will trigger when it sees a certain trigger type, such as edge or level. For an edge trigger type, the interrupt can occur at the rising or falling edge. For a level trigger type, the interrupt can occur when the level is high or low. Some systems also provide a parameter for a callback, which is a function to be called when the interrupt happens; other systems will hardcode the callback to a certain function name, and you’ll need to put your code there.

IOInterruptEnable(port, pin)

Enables the interrupt associated with a pin.

IOInterruptDisable(port, pin)

Disables the interrupt associated with a pin.

If interrupts are not per-pin (they could be per-bank), the processor may have a generic I/O interrupt, in which case the interrupt service routine (ISR) will need to untangle which pin caused the interrupt. It depends on your processor. If each I/O pin can have its own interrupt, the modules can be more loosely coupled.

Debouncing Switches

Many buttons do not provide the clean signal shown in the ideal button signal in the top part of Figure 4-6. Instead, they look more like those labeled “Bouncy digital button signal.” If you interrupted on that signal, your system could waste processor cycles by interrupting on the glitches at the start and end of the button press. Switch bouncing can be due to a mechanical or electrical effect.

Figure 4-6. Different views of button signals

Figure 4-6 shows an analog view of what could happen when a button is pressed and only slowly goes into effect. (What really happens can be a lot more complicated, depending on whether the bouncing is due primarily to a mechanical or electrical cause.) Note that there are parts of the analog signal where the signal is neither high nor low, but somewhere in between. Since your I/O line is a digital signal, it can’t represent this indeterminate value and can behave badly. A digital signal full of edges would cause the code to believe multiple presses happened per user action. The result would be inconsistent and frustrating to the user. Worse, interrupting on such a signal can lead to processor instability, so let’s go back to polling the signal.

Debouncing is the technique used to eliminate the spurious edges. Although it can be done in hardware or software, we’ll focus on software. See Jack Ganssle’s excellent web article on debouncing (in the section Further Reading) for hardware solutions.

Tip

Many modern switches have a very short period of uncertainty. Switches have datasheets, too; check yours to see what the manufacturer recommends. Beware that trying to empirically determine whether you need debouncing might not be enough, as different batches of switches may act differently.

You still want to look for the rising edge of the signal, where the user releases the button. To avoid the garbage near the rising and falling edges, you’ll need to look for a relatively long period of consistent signal. How long that is depends on your switch and how fast you want to respond to the user.

To debounce the switch, take multiple readings (aka samples) of the pin at a periodic interval several times faster than you’d like to respond. When there have been several consecutive, consistent samples, alert the rest of the system that the button has changed.

You will need three variables:

The current raw reading of the I/O line

A counter to determine how long the raw reading has been consistent

The debounced button value used by the rest of the code

How long debouncing takes (and how long your system takes to respond to a user) depends on how high the counter needs to increment before the debounced button variable is changed to the current raw state. The counter should be set so that the debouncing occurs over a reasonable time for that switch.

If there isn’t a specification for it in your product, consider how fast buttons are pressed on a keyboard. If advanced typists can type 120 words per minute, assuming an average of five characters per word, they are hitting keys (buttons) about 10 times per second. Figuring that a button is down half the time, you need to look for the button to be down for about 50 ms. (If you really are making a keyboard, you probably need a tighter tolerance because there are faster typists.)

For our system, the mythical switch has an imaginary datasheet stating that the switch will ring for no more than 12.5 ms when pressed or released. If the goal is to respond to a button held down for 50 ms or more, we can sample at 10 ms (100 Hz) and look for five consecutive samples.

Using five consecutive samples is pretty conservative. You may want to adjust how often you poll the pin’s level so you need only three consecutive samples to indicate that the button state has changed.

Tip

Strike a balance in your debouncing methodology: consider the cost of being wrong (annoyance or catastrophe?) and the cost of doing it better (developer time and processor cycles).

In the previous falling-edge interrupt method of handling the button press, the state of the button wasn’t as interesting as the change in state. To that end, we’ll add a fourth variable to simplify the main loop.

read button:
  if raw reading same as debounced button value,
    reset the counter
  else
    decrement the counter
    if the counter is zero,
      set debounced button value to raw reading
      set changed to true
      reset the counter

main loop:
  if time to read button,
    read button
    if button changed and button is no longer pressed
      set button changed to false
      set the delay period (reset or halve it)
  if time to toggle the LED,
    toggle LED
  repeat

Note that this is the basic form of the code. There are many options that can depend on your system’s requirements. For example, do you want it to react quickly to a button press but slowly to a release? There is no reason the debounce counter needs to be symmetric.

In the preceding pseudocode, the main loop polls the button again instead of using interrupts. However, many processors have timers that can be configured to interrupt. Reading the button could be done in a timer to simplify the main function. The LED toggling could also happen in a timer. More on timers soon, but first, marketing has another request.

Runtime Uncertainty

Marketing has a number of LEDs to try out. The LEDs are attached to different pins. Use the button to cycle through the possibilities.

We’ve handled the button press, but the LED subsystem knows only about the output on pin 1_2 on the v1 board or 1_3 on the v2 board. Once you’ve initialized all the LEDs as outputs, you could put a conditional (or switch) statement in your main loop:

If number button presses = 0, toggle blue LED
If number button presses = 1, toggle red LED
If number button presses = 2, toggle green LED

To implement this, you’ll need to have three different LED subsystems or (more likely) your LED toggle function will need to take a parameter. The former represents a lot of copied code (almost always a bad thing), whereas the latter means the LED function will need to map the color to the I/O pin each time it toggles the LED (which consumes processor cycles).

Our goal here is to create a method to use one particular option from a list of several possible objects. Instead of making the selection each time (in the main loop or in the LED function), you can select the desired LED when the button is pressed. Then the LED toggle function is agnostic about which LED it is changing:

main loop:
  if time to read button,
    read button
    if button changed and button is no longer pressed
      set button changed to false
      change which LED

  if time to toggle the LED,
    toggle LED
  repeat

By adding a state variable, we use a little RAM to save a few processor cycles. State variables tend to make a system confusing, especially when the change which LED section of the code is separated from toggle LED. Unraveling the code to show how a state variable controls the option can be tedious for someone trying to fix a bug (commenting helps!). However, the state variable simplifies the LED toggle function considerably, so there are times where a state variable is worth the complications it creates.

Dependency Injection

However, we can go beyond a state variable to something even more flexible. Earlier we saw that abstracting the I/O pins from the board saves us from having to rewrite code when the board changes. We can also use abstraction to deal with dynamic changes (such as which LED is to be used). For this, we’ll use a technique called dependency injection.

Before, we were hiding the I/O pin in the LED code (creating a hierarchy of functions that depend only on the lower levels). With dependency injection, we’ll remove the dependency of LED code upon the I/O pin by passing an I/O handler as a parameter to the LED initialization code. The I/O handler will know which pin to change and how to change it, but the LED code will know only how to call the I/O handler.

An oft-used example to illustrate dependency injection relates engines to cars. The car, the final product, depends on an engine to move. The car and engine are made by the manufacturer. Though the car cannot choose which engine to install, the manufacturer can inject any of the dependency options that the car can use to get around (e.g., the 800-horsepower engine or the 20-horsepower one).

Tracing back to the LED example, the LED code is like the car and depends on the I/O pin to work, just as the car depends on the engine. However, the LED code may be made generic enough to avoid dependence on a particular I/O pin. This allows the main function (our manufacturer) to install an I/O pin appropriate to the circumstance instead of hardcoding the dependency at compile time. This technique allows you to compose the way the system works at runtime.

In C++ or other object-oriented languages, to inject the dependency, we pass a new I/O pin handler object to the LED whenever a button is pressed. The LED module would never know anything about which pin it was changing or how it was doing so. The variables to hide this are set at initialization time (but do remember that these are variables, consuming RAM and cluttering the code).

Tip

A structure of function pointers is often used in C to achieve the same goal.

Dependency injection is a very powerful technique, particularly if your LED module were to do something a lot more complicated, for instance, output Morse code. If you passed in your I/O pin handler, you could reuse the Morse code LED output routine for any processor. Further, during testing, your I/O pin handler could print out every call that the LED module made to it instead of (or in addition to) changing the output pin.

However, the car engine example illustrates one of the major problems with dependency injection: complexity. It works fine when you only need to change the engine. But once you are injecting the wheels, the steering column, the seat covers, the transmission, the dashboard, and the body, the car module becomes quite complicated, with little intrinsic utility of its own.

The aim of dependency injection is to allow flexibility. This runs contrary to the goal of the facade pattern, which reduces complexity. In an embedded system, dependency injection will take more RAM and a few extra processor cycles. The facade pattern will almost always take more code space. You will need to consider the needs and resources of your system to find a reasonable balance.

Using a Timer

Using the button to change the blinking speed was helpful, but marketing has found a problem in the uncertainty introduced into the blink rate. Instead of cutting the speed of the LED in half, they want to use the button to cycle through a series of precise blink rates: 6.5 times per second (Hz), 8.5 Hz, and 10 Hz.

This request seems simple, but it is the first time we’ve needed to do anything with time precision. Before, the system could handle buttons and toggle the LED generally when it was convenient. Now the system needs to handle the LED in real time. How close you get to “precise” depends on the parameters of your system, mainly on the accuracy and precision of your processor input clock. We’ll start by using a timer on the processor to make it more precise than it was before, and then see if marketing can accept that.

Timer Pieces

In principle, a timer is a simple counter measuring time by accumulating a number of clock ticks. The more deterministic the master clock is, the more precise the timer can be. Timers operate independently of software execution, acting in the background without slowing down the code at all.

To set the frequency of the timer, you will need to determine the clock input. This may be your processor clock (aka the system clock, or master clock), or it may be a different clock from another subsystem (for instance, many processors have a peripheral clock).

System Statistics

When embedded systems engineers talk about the stats of our systems to other engineers, we tend to use a shorthand consisting of the vendor, the processor (and its core), the number of bits in each instruction, and our system clock speed. Earlier in this chapter, I gave register examples from the LPC13xx, MSP430, and ATtiny processor families. Systems with these processors could have stats like this:

NXP LPC1313 (Cortex-M3), 32-bit, 72 MHz

Texas Instrument MSP430 G2201, 16-bit, 16 MHz

Atmel ATtiny45, 8-bit, 4 MHz

That last number is the processor clock, which describes the theoretical number of instructions the processor can handle in a second. The actual performance may be slower if your memory accesses can’t keep up, or faster if you can use processor features to bypass overhead. The system clock is not the same as the oscillator on the board (if you have one). Thanks to an internal PLL circuit, your processor speed might be much faster than an onboard oscillator. PLL stands for phase lock loop and denotes a way that a processor can multiply a slower clock (i.e., a slow oscillator) to get a faster clock (a processor clock). Because slower oscillators are generally cheaper (and consume less power) than faster ones, PLLs are ubiquitous.

Many small microcontrollers use an internal RC oscillator as their clock source. Although these make life easier for the hardware designer, their accuracy leaves a lot to be desired. Considerable drift can accumulate over time, and this can lead to errors in communication and some real-time applications.

For example, the ATtiny45 has a maximum processor clock of 4 MHz. We want the LED to be able to blink at 10 Hz, but that means interrupting at 20 Hz (interrupt to turn it on, then to turn it off). This means we’ll need a division of 200,000. The ATtiny45 is an 8-bit processor; it has two 8-bit timers and a 16-bit timer. Neither size of timer will work to count up that high (see the sidebar System Statistics). However, the chip designers recognized this issue and gave us another tool: the prescale register, which divides the clock so that the counter increments at a slower rate.

The effect of the prescale register is seen in Figure 4-7. The system clock toggles regularly. With a prescale value of two, the prescaled clock (the input to our timer subsystem) toggles at half the system clock speed. The timer counts up. The processor notes when the timer matches the compare register (set to 3 in the diagram). When the timer matches, it may either continue counting up or reset.

Figure 4-7. Timer prescaling and matching

Before getting back to the timer on the ATtiny45, note that the registers needed to make a timer work consist of the following:

Timer counter

This holds the changing value of the timer (the number of ticks since the timer was last reset).

Compare (or match) register

When the timer counter equals this register, an action is taken. There may be more than one compare register for each timer.

Action register

This register sets up an action to take when the timer and compare register are the same. (For some timers, these actions are also available when the timer overflows, which is like having a compare register set to the maximum value of the timer counter.) There are four types of possible actions to be configured:

Interrupt (or not)

Stop or continue counting

Reset the counter (or not)

Set an output pin to high, low, toggle, or nothing

Clock configure register (optional)

This register tells a subsystem which clock source to use, though the default may be the system clock. Some processors have timers that even allow a clock to be attached to an input pin.

Prescale register

As shown in Figure 4-7, this divides the clock so that it runs more slowly, allowing timers to happen for relatively rare events.

Control register

This sets the timer to start counting once it has been configured. Often the control register also has a way to reset the timer.

Interrupt register (may be multiple)

If you have timer interrupts, you will need to use the appropriate interrupt register to enable, clear, and check the status of each timer interrupt.

Setting up a timer is processor-specific, and the user manual generally will guide you through setting up each of these registers. Your processor user manual may give the registers slightly different names.

Warning

Instead of a compare register, your processor might allow you to trigger the timer actions only when the timer overflows. This is an implicit match value of two to the number of bits in the timer register minus 1 (e.g., for an 8-bit timer, (2⁸)–1 = 255). By tweaking the prescaler, most timer values are achievable without too much error.

Doing the Math

Timers are made to deal with physical timescales, so you need to relate them from a series of registers to an actual time. Remember that the frequency (for instance, 14 Hz) is inversely proportional to the period.

The basic equation for the relationship between the timer frequency, clock input, prescaler, and compare register is:

timerFrequency = clockIn/(prescaler * compareReg)

This is an optimization problem. You know the clockIn and the goal, timerFrequency. You need to adjust the prescaler and compare register until the timer frequency is close enough to the goal. If there were no other limitations, this would be an easy problem to solve (but it isn’t always so easy).

How Many Bits in That Number?

Better than counting sheep, figuring out powers of two is often how I fall asleep. If you don’t share the habit, there are some powers of two that you really should memorize.

The number of different values a variable can have is 2 to the power of the number of bits it has (e.g., 8 bits offers 2⁸, so 256 different values can be in an 8-bit variable). However, that number has to hold a 0 as well, so the maximum value of an 8-bit variable is 2⁸–1, or 255. Even that is true only for unsigned variables. A signed variable uses one bit for the sign (+/–), so an 8-bit variable would have only seven bits available for the value. Its maximum would be 2⁷–1, or 127. Zero has to be represented only once, so the minimum value of a signed 8-bit variable is –128.

Bits | Power of two | Maximum value | Significance
4 | 2⁴ = 16 | 15 | A nibble.
7 | 2⁷ = 128 | 127 | Signed 8-bit variable.
8 | 2⁸ = 256 | 255 | Byte and an unsigned 8-bit variable.
10 | 2¹⁰ = 1024 | 1023 | Many peripherals are 10-bit.
12 | 2¹² = 4096 | 4095 | Many peripherals are 12-bit.
15 | 2¹⁵ = 32768 | 32767 | Signed 16-bit variable.
16 | 2¹⁶ = 65536 | 65535 | Unsigned 16-bit variable.
24 | 2²⁴ = 16777216 | ~1.6 million | Color is often 24-bit.
31 | 2³¹ = 2147483648 | ~2 billion | Signed 32-bit variable.
32 | 2³² = 4294967296 | ~4 billion | Unsigned 32-bit variable.

I usually fall asleep before 2²⁰, so I remember the higher-order numbers as estimates only. There will be times when a 32-bit number is too small to hold the information you need. Even a 64-bit number can fall down when you build a machine to do something as seemingly simple as shuffling cards.

Remember that variables (and registers) have sizes and that those sizes matter.

Returning to the ATtiny45’s 8-bit timer, 4 MHz system clock, and goal frequency of 20 Hz, we can export the constraints we’ll need to use to solve the equation:

These are integer values, so the prescaler and compare register have to be whole numbers. This constraint is true for any processor.

The compare register has to lie between 0 and 255 (because the timer register is eight bits in size).

The prescaler on the ATtiny45 is 10 bits, so the maximum prescaler is 1023. (The size of your prescaler may be different.)

The prescaler for this subsystem is not shared with other peripherals, so we don’t need to be concerned about this potential constraint for our solution.

There are several heuristics for finding a prescaler and compare register that will provide the timer frequency needed (see Figure 4-8).

Note

I asked two math professors how to solve this problem in a generic manner. The answers I got back were interesting. The most interesting part was learning that this problem is NP-complete for two reasons: integers are involved, and it is a nonlinear, two-variable problem. Thanks, Professor Ross and Professor Patton!

Figure 4-8. Timer heuristics

We can determine the minimum prescaler by rearranging the equation and setting the compare register to its maximum value:

prescaler = clockIn/(compareReg*timerFrequency) minPrescaler
      = 4 MHz/(255 * 20 Hz)

Unfortunately, the resulting prescaler value is a floating-point number (784.31). If you round down (784), the timer value will be above the goal. If you round up, you may be able to decrease the compare register to get the timer value to be about right.

In this case, we end up with a timer of 19.98 Hz, which is an error of less than a tenth of a percent off. However, marketing asked for high precision, and there are some methods to find a better prescaler.

First, note that you want the product of the prescaler and compare register to equal the clock input divided by the goal frequency:

prescaler * compareReg = 4 MHz/20 Hz = 200,000

This is a nice, round number, easily factored into 1,000 (prescaler) and 200 (compare register). This is the best and easiest solution to optimizing prescaler and compareReg: determine the factors of (clockIn/timerFrequency) and arrange them into prescaler and compareReg. However, this requires the (clockIn/timerFreq) to be an integer and the factors to split easily into the sizes allowed for the registers. It isn’t always possible to use this method.

We can see this as we move along to another blink frequency requested by marketing (8.5 Hz, or an interrupt frequency of 17 Hz):

prescaler * compareReg = 4 MHz/17 Hz = 235294.1

There is no simple factorization of this floating-point number. We can verify that a result is possible by calculating the minimum prescaler (we did that earlier by setting the compare register to its maximum value). The result (923) will fit in our 10-bit register. We can calculate the percent error using the following:

error = 100*(goal frequency - actual)/goal

With the minimum prescaler, we get an error of 0.03%. This is pretty close, but we may be able to get closer.

Set the prescaler to its maximum value, and see what the options are. In this case, a prescaler of 1,023 leads to a compare value of 230 and an error of less than 0.02%, a little better. But can we reduce the error further?

For larger timers, you might try a binary search for a good value, starting out with the minimum prescaler. Double it. Look at the prescaler values that are +/– 1 to find a compare register that is the closest whole number. If the resulting timer is not close enough, repeat the doubling of the modified prescaler. Unfortunately, with our example, we can’t double our prescaler and stay within the bounds of the 10-bit number.

Finally, another way to find the solution is to use a script or program (e.g., Matlab or Excel) and use brute force to try out the options, as shown in Figure 4-9. Start by finding the minimum prescaler value and the maximum prescaler value (by setting the compare register to 1). Limit the minimum and maximum so they are integers and fit into the correct number of bits. Then, for each whole number in that range, calculate the compare register for the goal timer frequency. Round the compare register to the nearest whole number, and calculate the actual timer frequency. This method led to a prescaler of 997, a compare register of 236, and a tiny error of 0.0009%. A brute-force solution like this will give you the smallest error, but will probably take the most developer time. Determine what error you can live with, and go on to other things once you’ve met that goal.

Figure 4-9. Brute-force timer solution

A Long Wait Between Timer Ticks

Brute force works well for 17 Hz, but when you get the goal output of 13 Hz, the minimum prescaler that you calculate is more than 10 bits. The timer cannot fit in the 8-bit timer. This is shown as an exception in the flowchart (Figure 4-8). The simplest solution is to use a larger timer if you can. The ATtiny45’s 16-bit timer can alleviate this problem because its maximum compare value is 65535 instead of the 8-bit 255, so we can use a smaller prescaler.

If a larger timer is unavailable, another solution is to disconnect the I/O line from the timer and call an interrupt when the timer expires. The interrupt can increment a variable and take action when the variable is large enough. For example, to get to 13 Hz, we could have a 26 Hz timer and toggle the LED every other time the interrupt is called. This method is less precise because there could be delays due to other interrupts.

Using the Timer

Once you have determined your settings, the hard part is over, but there are a few more things to do:

Remove the code in the main function to toggle the LED. Now the main loop will need only a set of prescale and compare registers to cycle through when the button is pressed.

Configure the pin. Some processors will connect any timer to any output, whereas others will allow a timer to change a pin only with special settings. For the processor that doesn’t support a timer on the pin, you will need to have an interrupt handler in the code that toggles only the pin of interest.

Configure timer settings and start the timer.

Using Pulse-Width Modulation

Market research has shown that potential customers are bothered by the brightness of the LED, saying its laser-like intensity is blinding them. Marketing wants to try out different brightness settings (100%, 80%, 70%, and 50%), using the button to switch between the options.

This assignment gives us a fine opportunity to explore pulse-width modulation (PWM), which determines how long a pin stays high or low. PWMs operate continuously, turning a peripheral on and then off on a regular schedule. The cycle is usually very fast, on the order of milliseconds.

PWM signals often drive motors and LEDs (though motors require a bit more hardware support). Using PWM, the processor can control the amount of power the hardware gets. Using some inexpensive electronics, the output of a PWM pin can be smoothed to be the average signal. For LEDs, though, no additional electronics are necessary. The brightness is simply relative to the amount of time the LED is on during each cycle.

A timer is a set of pulses that are all alike, so the timer signal is 50% up and 50% down (this is known as 50% duty cycle). In PWM, the pulses’ widths change depending on the situation. So a PWM can have a different ratio. A PWM with a 100% duty cycle is always on, like a high level of an output pin. And a 0% duty cycle represents a pin that has been driven (or pulled) low. The duty cycle represents the average value of the signal, as shown by the dashed line in Figure 4-10.

Figure 4-10. PWM duty cycles: 20%, 50% ,and 80%

Given the timer from the previous chapter, we could implement a PWM with an interrupt. For our 20 Hz LED example, we had a compare register of 200 so that every 200 ticks, the timer would do something (toggle the LED). If we wanted the LED to be on 80% of the time with a 20 Hz timer (instead of the 50% we were doing earlier), we could ping-pong between two interrupts that would set the compare register at every pass:

Timer interrupt 1

Turn on LED.

Set the compare register to 160 (80% of 200).

Reset the timer.

Timer interrupt 2

Turn LED off.

Set the compare register to 40 (20% of 200).

Reset the timer.

With a 20 Hz timer, this would probably look like a very quick series of flashes instead of a dim LED. The problem is that the 20 Hz timer is too slow. The more you increase the frequency, the more the LED will look dim instead of blinking. However, a faster frequency means more interrupts.

There is a way to carry out this procedure in the processor. In the previous section, the configurable actions included whether to reset the counter as well as how to set the pin. Many timers have multiple compare registers and allow different actions for each compare register. Thus, a PWM output can be set with two compare registers, one to control the switching frequency and one to control the duty cycle.

For example, the bottom of Figure 4-10 shows a timer counting up and being reset. This represents the switching frequency set by a compare register. We’ll name this compare register A and set it to 100. When this value is reached, the timer is reset and the LED is turned on. The duty cycle is set with a different register (compare register B, set to 80) that turns the LED off but allows the timer to continue counting.

Which pins can act as PWM outputs depends on your processor, though often they are a subset of the pins that can act as timer outputs. The PWM section of the processor user manual may be separate from the timer section. Also, there are different PWM controller configurations, often for particular applications (motors are often finicky about which type of PWM they require).

For our LED, once the PWM is set up, the code only needs to modify the duty cycle when the button is pressed. As with timers, the main function doesn’t control the LED directly at all.

Although dimming the LED is what marketing requested, there are other neat applications you can try out. To get a snoring effect, where the LED fades in and out, you’ll often need to modify the duty cycle. If you have tricolor LEDs, you can use PWM control to set the three LED colors to different levels, providing a whole palette of options.

Shipping the Product

Marketing has found the perfect LED (blue), blink rate (8 Hz), and brightness (100%). They are ready to ship the product as soon as you set the parameters.

It all seems so simple to just set the parameters and ship the code. However, what does the code look like right now? Did the timer code get morphed into the PWM code, or is the timer still around? With a brightness of 100%, the PWM code isn’t needed any longer. In fact, the button code can go. The ability to choose an LED at runtime is no longer needed. The old board layout can be forgotten in the future. Before shipping the code and freezing development, let’s try to reduce the spaghetti into something less tangled.

A product starts out as an idea and often takes a few iterations to solidify into reality. Engineers often have good imaginations for how things will work. Not everyone is so lucky, so a good prototype can go a long way toward defining the goal.

However, keeping around unneeded code clutters the code base (see the left side of Figure 4-11). Unused code (or worse, code that has been commented out) is frustrating for the next person who doesn’t know why things were removed. Avoid that as much as possible. Instead, trust that your version control system can recover old code. Don’t be afraid to release a development version internally. It will help you find the removed features after you prune the code for shipment.

Figure 4-11. Comparing spaghetti prototyping code with a simpler design

In this example, many things are easy to remove because they just aren’t needed. One thing that is harder to decide about is the dependency injection. It increases flexibility for future changes, which is a good reason for leaving it in. However, when you have to allocate a specific timer to a specific I/O pin, the configuration of the system becomes more processor-dependent and rigid. The cost of forcing it to be flexible can be high if you try to build a file to handle every contingency. In this case, I considered the idea and weighed the cost of making a file I’d never want to show anyone with the benefit of reducing the chance of writing bugs in the I/O subsystem. I chose to make more readable files, even if it means a few initial bugs, but I can respect either option.

On the right side of Figure 4-11, the code base is trimmed down, using only the modules it needs. It keeps the I/O mapping header file, even with definitions for the old board because the cost is low (it is in a separated file for easy maintenance and requires no additional processor cycles). Embedded systems engineers tend to end up with the oldest hardware (karmic payback for the time at the beginning of the project when we had the newest hardware). You may need the old board header file for future development. Pretty much everything else that isn’t critical can go into version control and then be removed from the project.

It hurts to see effort thrown away. But you haven’t! All of the other code was necessary to make the prototypes that were required to make the product. The code aided marketing, and you learned a lot while writing and testing it. The best part about developing the prototype code is that the final code can look clean because you explored the options.

You need to balance the flexibility of leaving all of the code in with the maintainability of a code base that is easy to understand. Now that you’ve written it once, trust that your future self can write it again (if you have to).

Further Reading

“A Guide to Debouncing” offers mechanics, real-world experimentation, other methods for implementing your debouncing code, and some excellent methods for doing it in the hardware and saving your processor cycles for something more interesting. His whole website is worth some of your time.

LPC13xx preliminary user manual (UM10375), Rev. 00.07, 31 July 2009.

MSP430 430x2xx Family User’s Guide (slau144e.pdf), 2008.

Atmel user manual: 8-bit Microcontroller with 2/4/8K Bytes In-System Programmable Flash (ATtiny25/V, ATtiny45/V, ATtiny85/V), Rev. 2586M-AVR-07/10.

Atmel application note: AVR 130: Setup and Use the AVR Timers

Interview Question: Waiting for a Register to Change

What is wrong with this piece of code?

void IOWaitForRegChange(unsigned int* reg, unsigned int bitmask){
  unsigned int orig = *reg & bitmask;
  while (orig == (*reg & bitmask)) { /* do nothing */ ; }
}

“What’s wrong with this code?” is a tough question because the goal is to figure out what the interviewer thinks is wrong with the code. For this code, I can imagine an interviewee wondering where the comment header is. And whether there really should exist a function that waits forever without any sort of timeout or error handling.

If an interviewee flails, pointing out noncritical things, I would tell him that the function never returns, even though the register changes, as observed on an oscilloscope. If he continues to flounder, I would tell him that the code compiles with optimizations on.

In the end, this is not a see-how-you-think question, but one with a single correct answer: the code is missing the volatile keyword. To succeed in an embedded systems interview, you have to know what that keyword does (it is similar in C, C++, and Java).

Chapter 5. Managing the Flow of Activity

In Chapter 2, we looked at different ways to break up a system into manageable chunks. That chapter described the what and why of design; this chapter covers the how. Once you’ve identified the pieces, getting them all to work together as a system can be daunting.

Scheduling and Operating System Basics

Structuring an embedded system without an operating system requires an understanding of some of the things that an operating system can do for you. I’m only going to give brief highlights; if any of this first section is brand new to you, you may want to review a book about operating systems (see Further Reading).

Tasks

When you turn on your computer, if you are like me, you load up the email program, web browser, and compiler. Several other programs start automatically (such as my instant message client). Each of these programs runs on the computer, seemingly in parallel, even if you have only one processor.

Warning

Three words that mean slightly different things, but that overlap extensively, are sometimes used interchangeably. A task is something the processor does. A thread is a task plus some overhead, such as memory. A process typically is a complete unit of execution with its own memory space, usually compiled separately from other processes. I’m focusing on tasks; threads and processes generally imply an operating system.

The operating system you are running has a scheduler that does the switching between processes (or threads), allowing each to run in its proper turn. There are many ways to implement schedulers, far beyond the scope of this book (let alone this small section). The key point is that a scheduler knows about all of the things your system should do and chooses which one it does right now.

Without an operating system, you are going to need to do the scheduling yourself. The very simplest scheduler has only one task. As with the blinking LED project in Chapter 4, you can do the same thing every time (on, wait, off, wait, repeat). However, things become more complex as the system reacts to changes in its environment (e.g., the button presses that change the LED timing).

Communication Between Tasks

The button-press interrupt and the LED-blinking loop are two tasks in the tiny system we have been examining. When we used a global variable to indicate that the button state changed, the two tasks communicated with each other. However, sharing memory between tasks is dangerous; you have to be careful about how you do it.

Figure 5-1 shows the normal course of events where the interrupt sets the shared memory when a button is pressed. Then the main loop reads and clears the variable. The user presses the button again, which causes the interrupt to set the memory again. Suppose that just as the main loop is clearing the variable, the interrupt fires again. At the very instant that the variable is being cleared, an interrupt stops the processing, swooping in to set the variable. Does it end up as set or cleared?

Figure 5-1. Race condition in shared memory

The answer depends on the precise timing of what happens. The uncertainty is the problem. If it can be this complicated with a simple Boolean value, consider what could happen if the code needs to set two variables or a whole array of data.

When this happens, it is called a race condition. Any memory shared between tasks can exhibit the uncertainty, leading to unstable and inconsistent behavior.

In this example, given the way the interrupt and button work together, it is likely that the system will miss a button press (if the interrupt wins the race to set the variable, the main function will clear it, even though it should be set). Though only a slight annoyance to a user in this particular system, race conditions can lead to unsafe conditions in a more critical system.

Avoiding Race Conditions

We need a way to prohibit multiple tasks from writing to the same memory. It isn’t only writing that can be an issue; the main loop reads both the variable that says the button is changed and the value of the button. If an interrupt occurs between those two reads, it may change the value of the button between them.

Any time memory shared between tasks is read or written, it creates a critical section of code, meaning code that accesses a shared resource (memory or device). The shared resource must be protected so only one task can modify it at a time. This is called mutual exclusion, often shortened to mutex.

In a system with an OS, when two tasks are running but neither is an interrupt, you can use a mutex to indicate which task owns a resource. This can be as simple as a variable indicating whether the resource (or global variable) is available for use. However, when one of the two tasks is an interrupt, we have already seen that not even a Boolean value is safe, so this resource ownership change has to be atomic. An atomic action is one that cannot be interrupted by anything else in the system.

Note

Operating systems have heavier-weight mutexes called semaphores that can handle situations where a thread or process can be preempted (forcibly changed by the scheduler).

From here on, we are going to focus on systems where a task is interruptible but otherwise runs until it gives up control. In that case, race conditions are avoided by disallowing interrupts while accessing the shared global variables. This has a downside, though: when you turn off interrupts, the system can’t respond as quickly to button presses because it has to wait to get out of a critical section. Turning off interrupts increases the system latency (time it takes to respond).

Latency is important as we talk about real-time systems. A real-time system must respond to an event within a fixed amount of time. Although the required response time depends on the system, usually it is measured in microseconds or milliseconds. As latency increases, the time it takes before an event can be noticed by the system increases, and so the total time between an event and its response increases.

Priority Inversion

Some processors allow interrupts to have different priorities (like operating systems do for processes). Although this flexibility can be very useful, it is possible to create a lot of trouble for yourself. Figure 5-2 shows a typical operating system’s definition of priority inversion. It is OK for a high-priority process to stop because it needs access to something a low-priority process has. However, if a medium-priority process starts, it can block the low-priority process from completing its use of the resource needed by the high-priority task. The medium-priority task blocks the high-priority task.

Figure 5-2. Priority inversion

For example, say we have the high-priority button-press interrupt and our low-priority main loop. When the main function accesses the button-press variable, it turns off the button-press interrupt to avoid a race condition. Now we add another interrupt to output debug data through a communication port. This should be a background task of medium priority, something that should get done but shouldn’t block the system from handling a button press. However, if it interrupts the main loop, it is doing exactly that.

What is the most important thing for the processor to be doing? That is its highest priority. Or at least it should be the highest priority. If someone had asked whether debug output is more important than button presses, we would have said no. If that is true, why is the processor running the debug interrupt and not the button handler? The simple fix in this case is to disable all interrupts (or all interrupts that are lower in priority than the button handler) when the button interrupt occurs. This prioritization happens automatically on some processors, but not all.

As we look at different ways of task management without an operating system, there are more ways to get into the position where the processor is inadvertently running a task that isn’t the highest priority. Watch for this.

State Machines

One way to keep your system organized while you have more than one thing going on is to use a state machine. This is a standard software pattern, one that embedded systems use a lot. According to Design Patterns: Elements of Reusable Object-Oriented Software, the intent of the State pattern is to “allow an object to alter its behavior when its internal state changes. The object will appear to change its class.”

Put more simply, when you call a state machine, it will do whatever it thinks it should do based on its current state. It will not do the same thing each time, but will base the change of behavior on its context, which consists of the environment and the state machine’s history (internal state). If this all sounds clinical and theoretical, there is an easier way to think about state machines: flow charts.

Almost any state machine can be represented as a flow chart. (Conversely, a problem you solve with a flow chart is probably destined to be a state machine.) State machines can also be represented as finite state automata (as shown in Figure 5-3), where each state is a circle and a change between states (a state transition) is an arrow.

Figure 5-3. Stoplight system

We’ll look at each element of this figure in the following section. The arrows in the diagram are just as important as the circles. State machines are not only about the states that a system can occupy, but also about the events that the system handles.

State Machine Example: Stoplight Controller

To talk about state machines properly, we need an example that is a bit more complicated than a blinking LED and a button. Figure 5-3 shows a stoplight controller. When the light is red and the controller gets a message to go, it turns the light green. When the controller gets a message to stop, it turns the light yellow for a time and then red. We’ve implemented this stoplight so it stays green as long as no one needs it to change. Presumably, a command to stop will be generated when a car arrives on the cross street.

One state transition—which is the formal term for what each arrow shows—is a bit subtle. When the light is yellow and the controller receives a message to go, it should not change the light back to green. Yellow lights should change to red, not green, so the message to go just leaves the light in the yellow stage. This subtlety is an example of the gotchas that state machines create for you. You have to be prepared for every event that can happen in every state, including very rare cases—even cases that shouldn’t take place but that may happen because of errors.

To work with a state machine, the first thing to do is to figure out the states and the events that can change states. With a system as simple as this, drawing a diagram is the best thing to do. Once you’ve identified the states (red, yellow, green), look at its connections. The stop and go commands should happen only in the green and red states (respectively). Even so, the commands are asynchronous, coming from outside the system, so each state should be able to handle them, even if they occur improperly. The easiest way of handling improper commands here is to ignore them, putting a loop back to the associated state.

Warning

If an improper event occurs, it may be better to generate an error of some sort. Use your judgment.

State-Centric State Machine

Most people think of a state machine as a big if-else statement, or switch statement:

  while (1) {
    look for event

    switch (state) {
    case (green light):
      if (event is stop command)
        turn off green light
        turn on yellow light
        set state to yellow light
        start timer
      break;
    case (yellow light):
      if (event is timeout)
        turn off yellow light
        turn on red light
        set state to red light
      break;
    case (red light):
      if (event is go command)
        turn off red light
        turn on green light
        set state to green light
      break;
    default (unhandled state)
      error!
    }
  }

The form of the state machine here is:

    case (state):
      if event valid for this state
        handle event
        prepare for new state
        set new state

The state machine can change its context (move to a new state). This means that each state needs to know about its sibling states.

State-Centric State Machine with Hidden Transitions

Another way to implement the state machine is to separate the state transition information from the state machine. This is theoretically better than the model in the previous section because it has more encapsulation and fewer dependencies. The previous model forced each state to know how and when to trigger each of the other states that can be reached from it. In the model I’ll show in this section, some higher-level system keeps track of which state reaches which other state. I’ll call this the “next state” function. It handles every state and puts the system into the next state that it should be in. By creating this function, you can separate the actions taken in each state from the state transitions.

The generic form of this would look like this:

    case (state):
      make sure current state is actively doing what it needs
      if event valid for this state
        call next state function

In the stoplight example, this model would create simpler code for each state, and it would also be similar for almost all states. For instance, the green light state would look like this:

    case (green light):
      turn on green light (even if it is already on)
      if (event is stop)
        turn off green light
        call next state function
      break;

The “next state” function should be called when a change occurs. The code will be familiar from the previous section because it is a simple switch statement as well:

next state function:
    switch (state) {
    case (green light):
      set state to yellow light
      break;
    case (yellow light):
      set state to red light
      break;
    case (red light):
      set state to green light
      break;

Now you have one place to look for the state and one for the transitions, but each one is pretty simple. In this example, the state transitions are independent of the event, one clue that this is a good implementation method.

This model is not always the best. For example, if a go command in the yellow state led back to green, the next state function would depend on both the current state and event. The goal to this method is to hide transitions, not to obfuscate your code. Sometimes it is better to leave it all together (as described in the previous section).

Event-Centric State Machine

Another way to implement a state machine is to turn it on its side by having the events control the flow. With this approach, each event has an associated set of conditionals:

    case (event):
      if state transition for this event
        go to new state

For example:

    switch (event)
    case (stop):
      if (state is green light)
        turn off green light
        go to next state
      // else do nothing
      break;

Most state machines create a comfortable fit between switch statements and states, but in some cases it may be cleaner to associate the state machines with events, as shown here. The functions still might need a switch statement to handle the dependency on the current state:

function to handle stop event
  if (state == green light)
    turn off green light
    go to next state

Unlike the state-centric options, the event-centric state machine implementation can make it difficult to do housekeeping activities (such as checking for a time-out in the yellow state). You could make housekeeping an event if your system needs regular maintenance, or you could stick with the state-centric implementation shown in the previous section.

State Pattern

An object-oriented way to implement a state machine is to treat each state as an object and create methods in the object to handle each event. Each state object in our example would have these member functions:

Enter

Called when the state is entered (turns on its light)

Exit

Called when leaving the state (turns off its light)

EventGo

Handles the go event as appropriate for the state

EventStop

Handles the stop event

Housekeeping

Periodic call to let the state check for changes (such as time-outs)

A higher-level object, the “context,” keeps track of the states and calls the appropriate function. It must provide a way for the states to indicate state transitions. As before, the states might know about each other and choose appropriately, or the transitions may be hidden, so the state might just indicate a need to go to the next state. Our system is straightforward, so a simple next-state function will be enough. In pseudocode, the class looks like:

class Context {
  class State Red, Yellow, Green;
  class State Current;

constructor:
  Current = Red;
  Current.Enter();

destructor:
  Current.Exit();

Go:
  if (Current.Go() indicates a state change)
    NextState();

Stop:
  if (Current.Stop() indicates a state change)
    NextState();

Housekeeping:
  if (Current.Housekeeping() indicates a state change)
    NextState();

NextState:
  Current.Exit();
  if (Current is Red)    Current = Green;
  if (Current is Yellow) Current = Red;
  if (Current is Green)  Current = Yellow;
  Current.Enter();
}

Allowing each state to be treated exactly the same frees the system from the switch statement, letting the objects do the work the conditionals used to do.

Table-Driven State Machine

Although flow charts and state diagrams are handy for conceiving of a state machine, an easier way to document and fully define a state machine is to use a table.

In the example table in Figure 5-4, each state has a row with an action and multiple events. The action column describes what should occur in that state (which particular light should be on). The other columns show the transition that needs to occur when the system is in a state and an event occurs. Often the transition is simply to move to a new state (and perform the new associated action).

Figure 5-4. The state machine as a data table

Tip

When I worked on children’s toys, they often offered 30 or more buttons (ABCs, volume, control, etc.). A table like this helped us figure out which events didn’t get handled in the flow chart. Even though a button may be invalid for a state, someone somewhere will still inexplicably press it. So tables like these not only aided implementation and documentation, they were critical to designing the game play.

Defining the system as a table here hints toward defining it as a data table in the code. Instead of one large, complex piece of code, you end up with smaller, simpler pieces: a data table showing the action that occurs in a state and what state to go to when an event happens, and an engine that reads the data table and does what it says. The best part is that the engine is reusable, so if you are going to implement many complex state machines, this is a great solution.

The stoplight problem is a little too simple to make this method worthwhile for implementation, but it is a straightforward example. Let’s start with the information in each table:

struct sStateTableEntry {
  tLight light;       // all states have associated lights
  tState goEvent;     // state to enter when go event occurs
  tState stopEvent;   // ... when stop event occurs
  tState timeoutEvent;// ... when timeout occurs
};

In addition to the next event for each table, I put in the light associated with the current state so that each state can be handled exactly the same way. (This state machine method really shines when there are lots of states that are all very similar.) Putting the light in the state table means our event handlers do the same thing for each state:

// event handler
void HandleEventGo(struct sStateTableEntry *currentState){
  // turn off the light (unless we're just going to turn it back on)
  if (currentState->light != currentState->go.light) {
    LightOff(currentState->light);
  }
  currentState = currentState->go;
  LightOn(currentState->light);
  StartTimer();
}

What about the actual table? It needs to start by defining an order in the data table:

typedef enum { kRedState = 0, kYellowState = 1, kGreenState = 2 } tState;

struct sStateTableEntry stateTable[] = {
  { kRedLight,    kGreenState,  kRedState,    kRedState},   // Red
  { kYellowLight, kYellowState, kYellowState, kRedState},   // Yellow
  { kGreenLight,  kGreenState,  kYellowState, kGreenState}, // Green
}

Typing in this table is a pain, and it is easy to get into trouble with an off-by-one error. However, if your state machine is a spreadsheet table and you can save it as a comma- or tab-separated variable file, a small script can generate the table for you. It feels a bit like cheating to reorganize your state machine as a spreadsheet, but this is a powerful technique when the number of states and events is large.

Making the state machine a table creates a dense view of the information, letting you take in the whole state machine at a glance. This will not only help you see holes (unhandled state/event combinations), but it will also let you see that the product handles events consistently.

The table can show more complex interactions as well, detailing what needs to happen. However, as complexity increases, the table loses the ability to simply become data, and it likely needs to return to one of the implementation mechanisms shown earlier, one oriented around control flow statements. For example, if we add some detail and error-finding to the stoplight, we get a more complex picture:

State/events | Command: go | Command: stop | Time-out
Red | Move to green | Do nothing | Invalid (log error), do nothing
Yellow | Do not clear event (defer handling to red) | Do nothing | Move to red
Green | Do nothing | Move to yellow, set timer | Invalid (log error), do nothing

Even when the state machine can’t be implemented as a data-table-driven system, representing it as a table provides better documentation than a flow chart.

Choosing a State Machine Implementation

Each option I showed for a state machine offers the same functionality, even though the implementations are different. When you consider your implementation, be lazy. Choose the option that leads to the least amount of code. If they are all about the same, choose the one with the least amount of replicated code. If one implementation lets you reuse a section of code without copying it, that’s a better implementation for your system. If that still doesn’t help you choose, consider which form of code will be the most easily read by someone else.

State machines are powerful because they let your code act according to its history (state) and the environment (event). However, because they react differently depending on those things, they can be very difficult to maintain, leading to spaghetti code and dependencies between states that are not obvious to the casual observer. Documentation is key, which is why I focused in these sections on the human-readable representation of the state machine before showing a code implementation.

Interrupts

In our system so far, we haven’t worried about how the events occur. The state machine doesn’t care whether there is a person pushing buttons that say “stop” and “go” or there is a wireless Ethernet controller parsing a data stream looking for these commands. This sort of encapsulation is great for the state machine. Now, though, it is time to consider the hows and whys of the events.

Interrupts can be kind of scary. They are one of the things that make embedded systems different from traditional application software. Interrupts often seem to swoop in from nowhere to change the flow of the code. They can call only certain functions (and usually not the debug functions). Interrupts need to be fast, so fast that they are a piece of code that is still sometimes written in assembly language. And bugs in interrupts are often quite difficult to find because, by definition, they occur outside the normal flow of code.

However, they are not the bogeyman they’ve been made out to be. If you understand a bit about what happens when an interrupt occurs, you’ll find where they can be a useful part of your software design.

Looking back, I want you to remember that processors and interfaces are like software APIs (see Reading a Datasheet) and that function pointers aren’t scary (see Function Pointers Aren’t So Scary). You’ll need both of those ideas in your head as we walk through what happens when an interrupt occurs:

An interrupt request (IRQ) happens inside the processor, based on a peripheral, the software, or a fault in the system.

The processor saves where it was (the context).

The processor looks in the interrupt vector table to find the callback function associated with the interrupt.

The callback function (aka interrupt service routine [ISR], or interrupt handler) runs.

Some interrupts are like small high-priority tasks. Once their ISR is complete, the processor restores the context it saved and continues on its way as though nothing had happened. Most peripheral interrupts are like that: input lines, communication pathway, timers, peripherals, ADCs, etc.

But some interrupts are more like exceptions, handling system faults and never returning to normal execution. For example, an interrupt can occur when there is a memory error, there is a divide by zero error, the processor tries to execute an invalid instruction, or the power level is not quite sufficient to run the processor properly (brownout detection). Since these errors mean the processor can’t run properly, often they are handled with infinite loops or processor resets.

Most interrupts you need to handle will be more like tasks than exceptions. They will let your processor run multiple tasks, seemingly in parallel. Before we get to that, let’s go through the process of interrupt handling in more detail.

An IRQ Happens

Usually an interrupt request happens because you’ve configured the interrupt. If it wasn’t you, then your compiler’s startup code probably did the work. While often invisible to high-level language programmers, the startup code configures some hardware-oriented things (such as setting up the default interrupts, usually the fault interrupts).

For task-like interrupts, your initialization code can configure them to occur under the conditions you specify. What exactly those conditions are depend on your processor. Your processor’s user manual will be critical to setting up interrupts.

In our stoplight example, we want a timer interrupt to signal an event when the yellow light has been on for long enough. Because processors are different, I’ll focus on an NXP’s LPC17xx processor, which is middle of the road in its interrupt-handling complexity (Atmel’s ATMega is much simpler, and the TI C2xxx is more complex).

The first step to setting up an interrupt is often, somewhat oddly, disabling the interrupt. Even though part of the power-on sequence is to disable and clear all interrupts, it is sensible to take the precaution anyway. If the interrupt is enabled, it might fire before the initialization code finishes setting it up properly, possibly leading to a crash.

Setting up interrupts uses registers that are memory mapped, similar to those we saw in Function Pointers Aren’t So Scary. As noted in that sidebar, accessing the memory address directly will make for illegible code. Most processor vendors and compiler vendors will give you a header file of #define statements, often allowing you to access individual registers as members of structures. Let’s take apart a typical line of code:

NVIC->ICER[0] = (1<<4); // disable timer 3 interrupt

Many things are going on in that one line. First, NVIC is a pointer to a structure located at a particular address. The header file from the compiler vendor unravels the hard-coded memory-mapped address:

#define SCS_BASE (0xE000E000)        /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE +  0x0100) /*!< NVIC Base Address  */
#define NVIC ((NVIC_Type *) NVIC_BASE) /*!< NVIC configuration struct */

The same header file defines the structure that holds the registers and where they are in the address space, so we can identify the next element in our line of code, ICER[0]:

typedef struct{
  __IO uint32_t ISER[8]; /*!< Offset: 0x000  Interrupt Set Enable Register */
       uint32_t RESERVED0[24];
  __IO uint32_t ICER[8];/*!< Offset: 0x080  Interrupt Clear Enable Register*/
…
}  NVIC_Type;

Tip

This header file structure can be built by reading the user manual if you can’t find where someone else has done it for you.

This processor has separate clear and set registers, as described in Writing an Indirect Register, so what our code does is set a bit in the clear (ICER) register that disables the timer interrupt. This is known as masking the interrupt.

Writing an Indirect Register

Memory-mapped registers can have some interesting properties. Registers don’t always act like variables.

If you want to modify a normal variable, the steps hidden in your memoryReg |= 0x01 line of code are:

Read the current value of the memory into a processor register.

Modify the value.

Write the variable back into memory.

However, for a register that sets interrupt handling, this multistep process can cause problems if an interrupt occurs between any two of these steps. To prevent this race condition—where an interrupt occurs during an inconsistent state—actions on registers must be atomic (executing in a single instruction).

This is accomplished by having set of indirect registers: a register that can set bits and a register that can clear bits, each of which acts on a third function register that is not directly writable (nor memory mapped). To set a bit in the function register, you have to write the bit in the set register. To clear a bit in the function register, you write that bit in the clear register. In either register (set or clear), the unset bits don’t do anything to the function register. (Part B of Figure 5-5 gives an example of setting and clearing bits and the contents of the function register at each step.)

Figure 5-5. Methods for setting register values

Sometimes the function register is reflected in the set and clear registers, so if you read either one of those, you can see the currently set bits. It can be confusing, though, to read something that is not what you have written to the same register.

A function register also can be write-only without any way to be read. Some processors save memory space by having a register act as a function register when it is written but a status register when it is read. In that case, you will need to keep track of the bit set in the function register using a shadow variable, a global variable in your code that you modify whenever you change the register. Instead of setting and clearing intermediate registers, you will need to modify your shadow and then write the register with the shadow’s value.

There are also registers where the act of reading the register modifies its contents. This is commonly used for status registers, where the pending status is cleared by the processor once your code reads the register (see part C of Figure 5-5). This can prevent a race condition from occurring in the time between the user reading the register and clearing the relevant bit.

Your user manual will tell you which types of registers exhibit special behaviors.

Once the interrupt is disabled, we can configure it to cause an IRQ when the timer has expired. (Timer configuration was covered in Chapter 4). All operations use the structure that points to the memory map of the processor registers for this peripheral (LPC_TIM3). The user manual says that the match control register describes whether an interrupt should happen (yes) and whether the timer should reset (no) and start again (no):

LPC_TIM3->MCR |=   0x01;      // set the interrupt to occur
LPC_TIM3->MCR &= ~(0x02);     // timer should not reset after it expires
LPC_TIM3->MCR |=   0x04;      // timer should stop incrementing after it expires

Even though the first line of this snippet sets the timer interrupt to occur when it modified the register, it didn’t really turn on the interrupt. Many processors require two steps to turn on the interrupt: a peripheral-specific interrupt-enable like the one just shown, plus a global interrupt-enable like the following:

NVIC->ISER[0] = (1<<4); // enable timer 3 interrupt

Note that the peripheral interrupt configuration is in the peripheral part of the user manual, but the other register is in the interrupts section. You need to set both so that an interrupt can happen. Before putting that line in our code, we might want to configure a few more things for our timer interrupt.

Multiple sources for one interrupt

Some processors have only one interrupt. The machinery necessary to stop the processor takes up space in the silicon, and a single interrupt saves on cost (and power). When this interrupt happens, the ISR starts by determining which of the peripherals activated the interrupt. This information is usually stored in a cause or status register, where the ISR looks at each bit to separate out the source of the interrupt (“Timer 1, did you trigger an interrupt? No? What about you, timer 2?”).

Having an interrupt for each possible peripheral is a luxury of many larger processors. When there are many possible sources, it is inefficient to poll all of the options. However, even when you have identified a peripheral, there is still a good chance you’ll need to unravel the real source of the interrupt. For example, if your timer 3 is used for multiple purposes, the yellow light time-out may be indicated when timer 3 matches the first match register. To generate the appropriate event when the interrupt happens, you’ll need to look at the peripheral’s interrupt register to determine why the timer 3 interrupt occurred:

if (LPC_TIM3->IR & 0x1) { // this interrupt is due to a match at match reg 0
                          // on timer 3

Most peripherals require this second level of checking for the interrupt. To look at another example, in a specific communication mechanism such as SPI, you’ll probably get a single interrupt to indicate that something interesting happened. Then you’ll need to check the SPI status register to determine what it was: it ran out of bytes to send, it received some bytes, the communication experienced errors, etc.

Whether you have one interrupt with many sources or many interrupts with even more sources, don’t stop looking at the cause register when you find the first hit. There could be multiple causes. That leads us back to the question of priorities: which interrupt source will you handle first?

Interrupt priority

As noted in the section Scheduling and Operating System Basics, some processors have a priority system for interrupts. For example, in our stoplight controller, a timer interrupt could be used to indicate when the yellow state should be completed, turning the light to red. This timer interrupt is pretty important. The other lights in the intersection have to stay in sync. If one of the controllers stayed yellow when the cross traffic turned to green, it would cause accidents.

Some processors handle interrupt priority by virtue of the peripherals themselves (so timer 1 has a higher priority than timer 2, which in turn is more important than timer 3). Other processors allow you to set the priority on a per-interrupt basis. See Figure 5-6 for a snippet of the user manual for the LPC17xx.

Figure 5-6. Priority register from the LPC17xx manual

In the initialization code, we can set the priority level, which according to the usual manual is in the lowest eight bits of IPR1l:

NVIC->IPR[1] &= ~(0x000000FF);  // set priority of timer 3 to zero, highest priority

Of course, if you set all of the interrupts to the highest priority, the processor will choose which ones get set based on its own internal documented criteria. (That also seems like a good metaphor for the life of a software engineer at a small start-up.)

Nested interrupts

Some processors allow interrupts within interrupts. Instead of priority being important only when an ISR is called, the priority is used to determine whether an interrupt can supersede the one currently running.

This is a powerful tool that is likely to cause unnecessary complexity. Unless nested interrupts solve a clear problem particular to your system, it is customary to disable other interrupts while in an interrupt service routine. Most processors that allow nested interrupts give you a method for disabling them at system initialization.

Nonmaskable interrupts

Some processors define certain interrupts as so important that they can’t be disabled; these are called nonmaskable interrupts (NMI). (I suppose not-disable-able interrupts was a little unwieldy to say.)

The processor exceptions noted earlier are one form of NMI. Often there is one I/O pin that can be linked to an NMI, which usually leads to an “on” button on the device. These interrupts cannot be ignored at any time and must be handled immediately, even in critical sections of code.

Stacks (and Heaps)

A stack is a data structure that holds information in a last-in-first-out (LIFO) manner, as shown in Figure 5-7. You push data on to the stack (adding it to the stack’s memory and increasing the pointer to where the next set of data will go). To get the last piece of data out, you pop the stack (which decreases the pointer).

Figure 5-7. Stack basics

A stack is a simple data structure any student can implement, but the stack (note the definite article) refers to the call stack that lies behind every running program in a designated section of RAM. For each function called, the compiler creates a stack frame that contains the local variables, parameters, and the address to return to when the function is finished. There is a stack frame for every function call, starting with the reset vector, then the call to main, and then whatever you call after that.

A heap is a tree data structure. The heap is where dynamically allocated memory comes from (so named because it can be implemented as a heap data structure).

Usually the heap grows up (see Figure 5-8), whereas the stack grows down. When they meet, your system crashes. Well, actually your system will probably crash before they meet because there are other things between them (global and static variables and possibly code).

Figure 5-8. Memory map of a system

If the stack gets too large, it can grow into other areas of memory, for example, where a global array is located. If the stack overwrites the memory of the array, the corrupt data in the array is likely to give you incorrect results. On the other hand, if the global array overwrites the stack, the function return address may be corrupted. When the function returns, the program will return to a bogus address and crash (usually due to an inappropriate instruction).

Save the Context

After the interrupt request happens (after you’ve set it up and the event happens), the processor saves where it was before it finds the appropriate ISR and calls it. Like a bookmark saving your spot, the processor saves its context to the stack; see Stacks (and Heaps). The context includes the program counter (which points to the next instruction to execute) and a subset of the processor registers (which are like the processor’s own cached local RAM). The registers are saved so that the interrupt code can use them in its execution.

These steps don’t come for free. The amount of time it takes between the IRQ and the ISR is the processor’s interrupt latency, usually advertised in the user manual as taking a certain number of processor cycles.

The system latency is the worst-case amount of time it takes from when an interrupt event happens to the start of the ISR. It includes the processor’s interrupt latency and the maximum amount of time interrupts are ever disabled.

Tip

If interrupt nesting is disallowed, then new interrupts are disabled while an interrupt is being handled. The largest contributor to system latency may be the time spent in the longest interrupt. Keep interrupts short!

The interrupt latency processor cycles are lost. If you had a processor that could do a hundred instructions a second (100 Hz) with an interrupt latency of 10 cycles and you set up a timer that interrupted every second, you’d lose 10% of your cycles to context switching. Actually, since you have to restore the context, it could be worse than that. Even though 10 cycles is a decent interrupt latency (not great, but not bad), 100 Hz systems are rare. However, doing this math with a 30 MHz processor, handling audio in an interrupt at 44,100 Hz with a 10-cycle latency uses 1.47% of the processor’s cycles simply calling the interrupt handler. That doesn’t even include the time spent executing the ISR or returning from it.

Processor designers work to keep the interrupt latency low. One way to do that is to store the minimum amount of context. This means that instead of saving all of the processor registers, the processor will save only a subset, requiring the software to store the other ones it needs. This is usually handled by the compiler, increasing the effective latency.

Some compilers will place limitations on the interrupt-handling code you can write to minimize the latency, possibly limiting the number of variables you can have or the number of nested functions.

Calculating system latency

Calling functions while in an interrupt is often discouraged. Each function call has some overhead (discussed more fully in Chapter 8), so function calls make your interrupt take longer. If you have other interrupts disabled during your interrupt, your system latency increases with each function call.

As your system latency increases, its ability to handle events in real time also decreases. Going back to the 30 MHz processor with its 44,100 Hz interrupt, if each interrupt uses 10 cycles for interrupt overhead and 10 cycles calling five short functions (each) that collectively use 275 cycles actually processing information, no other interrupt can be handled for at least 335 cycles (10 + 5*10 + 275). The time consumed by this is 11us (335/30 MHz). Also note that the system spends nearly 50% of its time in the interrupt (44,100*11 us=0.492 s). Reducing the overhead of the interrupt will free up processor cycles for other tasks.

Reentrant functions

Sometimes it is worth the overhead to call a function, though you have to be careful about which ones are called. We’ve already seen some of the damage that a race condition can cause when interrupts and normal code try to share a global variable. However, functions that are called by interrupts must have an additional layer of protection.

A reentrant function is one that can be safely called multiple times while it is running. The following garden-variety swap function, for instance, is nonreentrant because it has a temporary variable t whose value must not change before it is used to set the final y variable:

int t;

void swap(int* x, int* y) {
  t = *x;
  *x = *y;
  // hardware interrupt might invoke isr() here while
  // the function has been called from main or other
  // non-interrupt code
  *y =  t;
}

void isr {
  int x=1, y=2;
  swap(&x, &y);
}

Functions that use static or global variables are nonreentrant. Our state machine with its internal state is nonreentrant. Many C++ objects are nonreentrant by virtue of their private data. Worse, some standard library calls are nonreentrant, including malloc (or new) and printf (any I/O or file function).

When the system saves the context, it does the bare minimum. The compiler can do more, but even that does not give you an entirely clean slate to work with. That is actually a good thing because you usually want a global variable or two to signal the noninterrupt code that the interrupt occurred. However, when writing an ISR, you should be extremely wary about functions that use global variables without protecting them against interrupts.

Warning

Count on interrupts to occur at the worst time. That way, you can design your system to minimize the havoc they can cause.

Get the ISR from the Vector Table

After the IRQ happens and the context is saved, the third step to handling an interrupt is to determine which ISR to call by looking in the interrupt vector table (IVT). This table is located at a specific area of memory. When an interrupt occurs, the processor looks in the table to call the function associated with the interrupt.

The interrupt vector table is a list of callback functions, one for each type of interrupt. Really, the vector table is just a list of function pointers.

Initializing the vector table

The start-up code (which probably came with your compiler) sets up the vector table for you, usually with dummy interrupt handlers. When debugging, you probably want your unhandled interrupts to loop so that you find them and turn off the interrupt or handle it properly.

When the product hits the market, having an unhandled interrupt handler go into an endless loop will cause your system to become unresponsive. You may want unhandled interrupts to simply return to normal execution. It wastes processor cycles and is still a bug, but at least the bug’s effect on the customer is a slight slowdown instead of a system that needs a reboot.

In some cases, if you name your handler function the same as the one in the table, some preprocessor magic will make the compiler use yours in the IVT. With other processors and compilers, though, you will need to insert your ISR into the function table at the correct slot for your interrupt.

In the LPC17xx processor, using the CodeRed compiler, we’d only need to name the ISR TIMER3_IRQHandler and the compiler would put the correct function address in the vector table. In some processors, such as Atmel’s AT91SAM7S, you have to create a function and put it in the table manually:

interrupt void Timer1ISR(){
  …
}

void YellowTimerInit(){
  …
  AT91C_BASE_AIC->AIC_SVR[AT91C_ID_TC1] = // Set TC1 IRQ handler address in the IVT
      (unsigned long)Timer1ISR;     // Source Vector Register, slot AT91C_ID_TC1 (12)
  …
}

This has to occur when the interrupt is initialized and before the interrupt is enabled.

Looking up the ISR

The vector table is located at a particular address in memory, set by the linker script. Since this is probably done for you in the start-up code, usually you don’t need to know how to do it. However, in case you are curious, let’s dig into the start-up code for the LPC17xx for a moment:

__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
  // Core Level - CM3
  &_vStackTop,   // The initial stack pointer
  ResetISR,      // The reset handler
  NMI_Handler, // The NMI handler

…
  TIMER2_IRQHandler,  // 19, 0x4c - TIMER2
  TIMER3_IRQHandler,  // 20, 0x50 - TIMER3
  UART0_IRQHandler,  // 21, 0x54 - UART0
…
}

First in the listing is a nonstandard line that communicates to the compiler that the following variable needs to go someplace special and that the linker script will indicate where it is (at the location specified by .isr_vector). We’ll talk more about linker file scripts later (see Linker Scripts), but right now it is safe to say that the .isr_vector linker variable is located where the user manual says the vector table should be (0x00000000).

Inside the array of void * elements, there is the location for the stack, also set in the linker script. After that is the reset vector, which is the address of the code that is called when your system boots up or resets for another reason. It is one of the exception interrupts mentioned earlier. Most people don’t think of turning on the power or pushing the reset button as an interrupt to their code. However, the processor responds to a reset by loading a vector from the table, in the same way that it responds to an interrupt by loading a vector. Later in the table, there are the peripheral interrupts, such as our timer. Figure 5-9 shows how this would look in the processor’s memory.

Figure 5-9. Vector table in memory

Each interrupt has an interrupt number. For our timer 3 interrupt, it is 20. When the interrupt happens, it is signaled by just this number, and the part of the processor that generates the IRQ sends it to the part of the processor that looks up the handler in the vector table as the number. The address to the handler is found by multiplying the number by 4 (20*4=80 or 0x50) and looking at that slot in the vector table.

In this table, the compiler vendor has filled in all slots with dummy interrupt handlers that run infinite loops, to help the programmer catch misconfigured or spurious interrupts. Some compiler trickery lets you name your ISR functions the same without namespace collisions.

Calling the ISR

So far we’ve gone through what you need to know about the workings of an ISR and how to set it up. Now we’ll get to the meaty part of actual ISR that you’ll need to implement. We’ve already seen the most important rules surrounding ISRs:

Keep ISRs short because longer ones increase your system latency. Generally avoid function calls, which may have hidden depths and increase overhead.

Don’t call nonreentrant functions (such as printf), because global variables can be corrupted by interrupts.

Turn off other interrupts during the ISR to avoid priority inversion problems.

This gives us the design guidelines to implement an ISR. Our timer interrupt is easy to implement because all we need to do is set a flag indicating it is time to change to a red light:

volatile tBoolean gYellowTimeout = FALSE; // global variable set by the interrupt
                                          // handler, which is cleared by normal code
                                          // when event handled
void TIMER3_IRQHandler(void){
  __disable_irq();        // disallow nesting of interrupts
  gYellowTimeout = TRUE;
  __enable_irq();
}

The interrupt doesn’t print out a debug message or change the state to red. As much as it would make sense to do that, keeping it short means we need to let other parts of the system take care of these things.

Our system so far has been entirely devoted to controlling the lights at the signal, so we haven’t had to worry about system latency. However, few controllers in the wild have such a limited scope, so for this section we’ll add some functionality. Say the stoplight controller is also taking pictures of red-light runners with a traffic camera, dispensing gum balls to good citizens who use the crosswalk, and studying to pass the Turing test. With the small processor so overloaded, we might have to make allowances for the state machine not running as quickly as we’d like.

After reviewing the product requirements and making sure we can’t alleviate the problem by reducing the feature set (do we really need sentient stoplights?), we might opt to make sure the system is left in the safest possible mode by turning off the yellow light and turning on the red light in the ISR.

Since the state machine is nonreentrant, the interrupt would have to circumvent it and force the lights to the new color, getting them out of sync with the state recorded by the state machine. Not only does this simple time-out ISR become coupled to the event handling and the light code, it creates an oddity in the system that is a little hard to explain to someone taking over maintenance. That is why I’d verify the requirements before implementing the workaround. On the other hand, interrupts can do more than set flags and unblock communications drivers. Using them to make a system safer is a worthy trade-off (although fixing the design to avoid the heavyweight state change would still be preferable).

Many processors require that you acknowledge (or clear) the interrupt. Usually this is accomplished by reading a status register. As noted in Writing an Indirect Register, this may have the side effect of clearing the bit.

Disabling interrupts

Nested interrupts are allowed on the LPC17xx, but we don’t want to deal with the complexity of interrupts within interrupts. The __disable_irq() and __enable_irq() macros come from the compiler vendor and insert a single instruction, so the overhead to prevent nesting is minimal.

Critical sections

We’ve already seen how race conditions can happen in critical sections. To avoid that, we’ll need to turn off interrupts there as well. We could disable a particular interrupt, but that might lead to the priority inversion noted earlier. Unless you have a good reason for your particular design, it is usually safer to turn off all interrupts.

There are two methods for disabling interrupts. The first method uses the macros we’ve already seen. However, there is one problem with those: what if you have critical code inside critical code? For example:

HandyHelperFunction:
  disable interrupts
  do critical things
  enable interrupts

CriticalFunction:
  disable interrupts
  call HandyHelperFunction
  do supposedly critical things  // unprotected!
  enable interrupts

Note that as soon as interrupts are enabled in HandyHelperFunction, they are also enabled in the calling function (CriticalFunction), which is a bug. Critical sections should be short (to keep system latency at a minimum), so you could avoid this problem by not nesting critical sections, but this is something easier said than done.

Tip

Because some processors won’t let you nest critical pieces of code, you’ll need to be sure to avoid doing it accidentally. To avoid this issue, I recommend naming the functions in a way that indicates they turn off interrupts.

Alternatively, if your processor allows it, implement the global disable and enable functions (or macros) a little differently by returning the previous status of the interrupts in the code that disables them:

HandyHelperFunction:
  interrupt status  = disable interrupts()
  do critical things
  enable interrupts(interrupt status)

CriticalFunction:
  interrupt status = disable interrupts()
  call HandyHelperFunction
  do critical things
  enable interrupts(interrupt status)

Here the helper function gets an interrupt status indicating the interrupts are already off. When the helper function calls the method to enable the interrupts, the interrupt status parameter causes no action to be taken. The critical code remains safe throughout both functions. Also important, if the helper is called by a different function, the helper’s critical area is still protected.

Restore the Context

After your ISR has run to completion, it is time to return to normal execution. Some compilers extend C/C++ to include an interrupt keyword (or __IRQ, or _interrupt) to indicate which functions implement interrupt handlers. The processor gives these functions special treatment, both when they start (some context is saved before the ISR starts running) and when they return.

As noted in the section on saving the context, the program counter points to the machine instruction you are about to run. When you call a function, the address of the next instruction (program counter + 1 instruction) is put on the stack as the return address. When you return from the function (rts), the program counter is set to that address.

Tip

It is unusual for different assembly languages to have similar opcodes. However, rts and rti tend to be pretty common. They stand for “ReTurn from Subroutine” and “ReTurn from Interrupt,” respectively.

However, the interrupt isn’t a standard function call; it is a jump to the interrupt handler caused by the processor. If the interrupt simply returned as though it were a function, the things that the processor did to store the context would not get undone. So interrupts have a special instruction (rti) to indicate that they are returning from an interrupt. This lets the processor know that it must restore the context to its state prior to the function call before continuing on its way.

If your compiler doesn’t require you to indicate that a function is an interrupt, you can rest assured it is finding some other way to make the return from interrupt happen. That is, the compiler is probably wrapping the interrupt function in assembly code that merely calls your function. Once your handler returns from the function call, the assembly wrapper returns from the interrupt. The processor resets the stack the way it was, and program execution continues from exactly the point it left off.

When to Use Interrupts

Now that we’ve set up our stoplight to use an interrupt for the timer and created the code to handle the interrupt, we need to backtrack. We forgot a design step: should the yellow light time-out be an interrupt?

There are many circumstances in which the simplest solution is an interrupt. Communication pathways often have buffers that need to be filled (or emptied). An interrupt can act as a background task to feed the buffers while the foreground task generates (or uses) the data. Changes to input lines may need interrupts if they need to be handled quickly. The more real-time the requirement to handle a change on the line, the more likely an interrupt is appropriate for a solution.

Under that criterion, the button press we saw in Chapter 4 that needed only a simple response within 50 ms would not need an interrupt. A button press happens pretty rarely in the world of your processor. However, if checking to see whether it has been pressed takes time from other activities, it may be better to have an interrupt.

We’ve also seen that interrupts have some serious downsides. I already mentioned the overhead of each interrupt, which can add up if you have a lot of them. Interrupts also make your system less deterministic. One of the great things about not having an operating system[3] is being able to say that once instruction x happens, y will happen. If you have interrupts, you lose the predictability of your system. And because the code is no longer linear in flow, debugging is harder. Worse, some catastrophic bugs will be very difficult to track down if they depend on an interrupt happening at a very specific time in the code (i.e., a race condition). Plus, the configuration is largely compiler- and processor-dependent (and the implementation may be as well), so interrupts tend to make your code less portable.

In the end, the development cost of implementing (and maintaining) interrupts is pretty high, sometimes higher than figuring out how to solve the problem at hand without them. Save interrupts for when you need their special power: when a system is time-critical, when an event is expensive to check for and happens very rarely, and when a short background task will let the system run more smoothly.

[3] Some real-time operating systems (RTOSs) are deterministic, usually the more expensive ones.

How Not to Use Interrupts

So if you don’t need their special power, how can you avoid interrupts? Some things can be solved in hardware, such as using a faster processor to keep up with time-critical events. Other things require more software finesse.

Returning to our stoplight example, we’ve considered the events as interrupts. Two events are communicated to the system (the commands stop and go), and one is generated by the system (yellow’s timeout). Do any of these events need to be interrupts?

Let’s assume the system doesn’t do anything besides handle the events. It just waits for these events to happen. The implementation of the system could be much simpler to maintain if we can always see what the code is waiting for.

Note

When you get to Chapter 10, interrupts become a way to wake the processor from sleep, so you have to use them.

Polling

Asking a human “are you done yet?” is generally considered impolite after the fourth or fifth query, but the processor doesn’t care if the code incessantly asks whether an event is ready. Polling adds processor overhead, even when there are no events to process. However, if you are going to be in a while loop anyway (i.e., an idle loop), there is no reason not to check for events.

Polling is straightforward to code. There’s just one subtlety worth mentioning: if you are polling and waiting for the hardware to complete something, you might want a time-out just in case.

In the yellow light example, all we need to do is wait for a certain amount of time to pass. Even though embedded systems have a reputation for being fast, many systems spend an inordinate amount of their clock cycles waiting for time to pass.

System Tick

Like the sound you hear when the clock’s second hand moves, many systems have a tick that indicates time is passing. The amount of time in that tick varies, but one millisecond tends to be a popular choice.

Tip

Ticks don’t have to be one millisecond. If you have a time that is important to your system for other reasons (e.g., you have an audio recording system that is running at 44,100 Hz), you might want to use that instead.

The tick is implemented with a timer interrupt that counts time passing. Yes, if we implemented the yellow light time-out this way, we are still basing the solution on an interrupt, but it is a much less specific interrupt. The system tick solves a much broader range of problems. In particular, it lets us define this function:

void DelayMs(tTime delay);

This will wait for the amount of time indicated—well, for approximately the amount of time indicated (see the following sidebar, ). Note that because of fenceposting and jitter, DelayMs isn’t a good measure of a single millisecond. However, if you want to delay 10 or 100 milliseconds, the error becomes small enough not to matter. If your system depends on one-millisecond accuracy, you could use a shorter tick, though you’ll need to balance the overhead of the timer interrupt with the processing needs of the rest of the system.

Fenceposts and Jitter

A fencepost error is an example of an off-by-one error, one often illustrated with building materials:

If you build a straight fence 100 m long with posts 10 m apart, how many posts do you need?

The quick and wrong answer is that you need 10 posts, but you actually need 11 posts to enclose the 100 meters, as shown in Figure 5-10. The same is true of a system tick. To cover at least the number of milliseconds in question, you need to add one to the delay.

Figure 5-10. Fencepost example

However, this calculation is complicated by jitter. Your call to DelayMs is very unlikely to happen on a tick boundary. Instead, the delay will always start after a tick, so that DelayMs will always be longer than the number of ticks indicated.

You can choose whether you want to wait no more than the delay indicated (in which case, with a one millisecond delay, you may wait less than a processor cycle) or no less than the delay indicated (with a one millisecond delay, you may wait two milliseconds minus a processor cycle). You can make whatever choice leaves your application most robust, as long as the code is clear.

In the stoplight’s yellow state, we could call DelayMs and move to red as soon as it finishes. However, then we couldn’t respond to any other commands. In this example it happens to be OK because stop and go don’t do anything in the yellow state, but still, what if one of them did?

If you want to keep track of time passing and do other things, add a few more functions to your system tick:

tTime TimeNow();
tTime TimePassed(tTime since);

The TimeNow function should return the tick counter. The code never looks at this directly, instead using the TimePassed function to determine whether enough time has passed. In fact, DelayMs can be implemented as a combination of these functions (getting the initial time and then waiting for the time passed to be greater than the delay). In between these functions, your system can do other things. In effect, this is a kind of polling.

Note that the TimeNow function gives the number of ticks since the system was booted. At some point this variable holding the number of ticks will run out of space and roll over to zero. If you used an unsigned 16-bit integer, a 1 ms tick would make the clock roll over every 65.5 seconds. If you need to use these functions to try to measure something that takes 70 seconds, you may never get there.

However, if you use an unsigned 32-bit integer, your system will roll over to zero in 4,294,967,296 ms, or about 49.7 days. If you use an unsigned 64-bit integer, your 1 ms tick won’t roll over for half an eon (0.58 billion years). I’m impressed by this long-term thinking, but are you sure there won’t be a power outage or system reboot before then?

So the size of your timekeeping variable determines the length of time you can measure. In many systems, instability can occur when the rollover happens. Protect against this by taking the rollover into account when you write the time-measuring function, to ensure the discontinuity does not cause a problem:

tTime TimePassed(tTime since) {
  tTime now = gSystemTicks;

  if (now >= since) {return (now - since);}

  // rollover has occurred
  return (now + (1 + TIME_MAX - since));
}

Time-Based Events

In our stoplight example, the yellow state can use the system tick to create its time-based event. When the code enters the yellow state, it sets a state variable:

yellowTimeStart = TimeNow();

Then, when doing housekeeping in the yellow state, it checks for the completion of the event:

if (TimeSince(yellowTimeStart) > YELLOW_STATE_TIMEOUT)
  // transition out of the yellow state

Between those two times, the system can do whatever it needs to: listen for commands, check the lights to make sure their bulbs are working, play Tetris, etc.

A Very Small Scheduler

For things that recur or need attention on a regular basis, you can use a timer as a mini-scheduler to fire off a callback function (a task).

Tip

At this point, you are starting to re-create the functions of an operating system. If your mini-scheduler becomes more and more complicated, consider investing in an operating system.

Let’s add a state to our stoplight that blinks the red light to indicate a four-way stop. This happens only when something goes wrong, but we’ll cover those possibilities in the next section. Although we could use the time-based event to turn the red light on and off, it might be a little simpler to make this a background task, something that the scheduler can accomplish.

So, before looking at the gory details, let’s consider what the main loop needs to do to make all this work for a scheduler that runs about once a second, appropriate for our red-light blinking state (but too sluggish for handling other events):

  Run the scheduler after initialization is complete
  LastScheduleTime = TimeNow()

  Loop forever:
    if TimePassed(LastScheduleTime) > ONE_SECOND
     Run the scheduler, it will call the functions (tasks) when they are due
     LastScheduleTime = TimeNow()

We already worked out the time-based functionality in the previous section; now let’s look at the scheduler in more detail.

When a task is allocated, it has some associated overhead, including the callback function that is the heart of the task. It also contains the next time the task should be run, the period (if it is a periodic task), and whether or not the task is currently enabled:

struct Task;                                 // forward declaration of the struct
typedef void (*TaskCallback)(struct Task *); // type of the callback function

typedef struct Task {
  tTime runNextAt;              // next timer tick at which to run this task
  tTime timeBetweenRuns;        // for periodic tasks
  TaskCallback  callback;
  int enabled;                  // current status
};

The calling code should not know about the internals of the task management, so each task has an interface to hide those details.

void TaskResetPeriodic(struct Task *t);
void TaskSetNextTime(struct Task *t, tTime timeFromNow, tTime now);
void TaskDisable(struct Task *t);

(Yes, these could be methods in a class instead of functions; both ways work just fine.)

The scheduler is straightforward and has only one main interface:

void SchedulerRun(tTime now);

When the scheduler runs, it looks through its list of timers. Upon finding an enabled timer, it looks to see whether the current time is later than runNextAt. If so, it runs the task by calling the callback function. The SchedulerRun function sits in the main loop, running whenever nothing else is.

These tasks are not interrupt-level, so they should not be used for activities with real-time constraints. Also, the tasks have to be polite and give up control relatively quickly, because they will not be preempted as they would be in typical operating systems.

There is one final piece to the scheduler: attaching the tasks to the scheduler so that they run. First, you’ll need to allocate a timer. Next, configure it with your callback function, the time at which the function should run, and the time between each subsequent run (for periodic functions). Finally, send it to the scheduler to put in its list:

void SchedulerAddTask(struct Task* t);

Publish/Subscribe Pattern

With the scheduler, we’ve built what is known as a publish/subscribe pattern (also called an observer pattern or pub/sub model). The scheduler publishes the amount of time that has passed, and several tasks subscribe to that information (at varying intervals). This pattern can be even more flexible, often publishing several different kinds of information.

The name of the pattern comes from newspapers, probably the easiest way to remember it. One part of your code publishes information, and other parts of your code subscribe to it. Sometimes the subscribers request only a subset of the information (like getting the Sunday edition only). The publisher is only loosely coupled to the subscribers. It doesn’t need to know about the individual subscribers; it just sends the information in a generic method.

Our scheduler has only one type of data (how much time has passed), but the publish/subscribe pattern is even more powerful when you have multiple types of data. This pattern is particularly useful for message passing, allowing parts of your system to receive messages they are interested in but not others. When you have one object with access to information that many others want to know about, consider the publish/subscribe pattern as a good solution.

Watchdog

We started the scheduler section mentioning the blink-red state. Its goal was to put the system in a safe mode when a system failure occurs. But how do we know a system failure has occurred?

Our software can monitor how long it has been since the last communication. If it doesn’t see a stop or go command in an unreasonably long time, it can respond accordingly. Metaphorically speaking, it acts as a watchdog to prevent catastrophic failure. Actually, the term watchdog means something far more specific in the embedded world.

Most processors or reset circuits have a watchdog timer capability that will reset the processor if the processor fails to perform an action (such as toggle an I/O line or write to a particular register). The watchdog system waits for the processor to send a signal that things are going well. If such a signal fails to occur in a reasonable (often configurable) amount of time, the watchdog will cause the processor to reset.

The goal is that when the system fails, it fails in a safe manner (failsafe). No one wants the system to fail, but we have to be realistic. Software crashes. Even safety-critical software crashes. As we design and develop our systems, we work to avoid crashes. But unless you are omniscient, your software will fail in an unexpected way. Cosmic rays and loose wires happen, and many embedded systems need to be self-reliant. They can’t wait for someone to reboot the system when the software hangs. They might not even be monitored by a human. If the system can’t recover from some kinds of error, it is generally better to restart and put the system in a good state.

Using a watchdog does not free you from handling normal errors; instead, it exists only for when the system is unrecoverable. There are ways to use a watchdog that make it more effective. But first, let’s look at some suboptimal techniques, based on models earlier in this chapter, that would make the watchdog less effective:

Setting up a timer interrupt that goes off slightly more often than the watchdog would take to expire

If you service the watchdog in the timer interrupt, your system will never reset, even when your system is stuck in an infinite loop. This defeats the purpose of the watchdog.

Setting the delay function (DelayMs) to service the watchdog, with the idea that the processor isn’t doing anything else then

You’ll have delay functions scattered around the code, so the watchdog will get serviced often. Even then, if the processor gets stuck in an area of code that happens to have a delay, the system won’t reset as it should.

Putting the watchdog servicing in places that take a while for the processor to perform, maybe the five or six longest-running functions

By scattering the watchdog code around, it waters down the power of the watchdog and offers the possibility that your code could crash in one of those areas and hang the system.

The goal of the watchdog is to provide a way to determine whether any part of the system has hung. Ideally, watchdog servicing is in only one place, some place that the code has to pass through that shows all of its subsystems are running as expected. Generally, this is the main loop. Even if it means that your watchdog needs to have a longer time-out, having it watch the whole system is better than giving it a shorter recovery time while trying to watch only part of the system.

Sadly, for some systems, the watchdog cannot be segregated so neatly. When the signal to the watchdog must be sent in some lower level of code, recognize that the code is dangerous, an area where an unrecoverable error might occur, causing the system to hang. That code will need extra attention to determine whether anything could go wrong (and hopefully prevent it).

Generally, you don’t want the watchdog active during board bring-up or while using a debugger. Otherwise, the system will reset after a breakpoint. A straightforward way to turn off the watchdog will facilitate debugging. If you have a logging method, be sure to print out a message on boot if the watchdog is on. It is one of those things you don’t want to forget to enable as you do production testing. Alternatively, you can toggle an LED when the watchdog is serviced to give your system a heartbeat that is easy to see from the outside, letting the user know that everything is working as expected.

Watchdog Terminology

Servicing the watchdog so it doesn’t hang has traditionally been called “kicking the dog.”

This caused consternation when I worked at a small pet-friendly company. It was great to see the dogs play at lunch, and kind of calming. However, one day as we were preparing for a big client to review our code, our CEO went off the rails when he saw the function KickTheDog() in the main loop. He adamantly explained that no dog, real or virtual, would be kicked at our company. Ever.

We refactored the function name to PetTheDog(). On the day of the big meeting, the clients snickered when they saw our politically correct safety net. I’m not sure when the terminology changed, but in the years since then, I’ve seen less kicking and more petting, feeding, or walking the watchdog. What you choose to do is up to you, but you’d never actually kick a dog, right?

There are three takeaways here: 1) terminology for standard things changes, 2) language matters, and 3) never let your CEO see your code.

Further Reading

Running an embedded system without an operating system (running on bare metal) doesn’t mean you can be ignorant of operating systems principles. If anything, because you are doing the work yourself, you need to know more about how OSs function so you can re-create the parts you need. There are many good OS books, but my favorite is the classic text book: Operating Systems: Design and Implementation by Andrew Tannenbaum (Pearson).

I also recommend finding an old book about programming a small processor, something published in 1980 or before. For example, I have Programming the 6502, Third Edition, by Rodnay Zaks (Sybex). You can find one in a library or used bookstore. This sort of book takes out the modern fluff surrounding processors (the fluff that makes them easier to use) and provides insight into how the processors worked when they were simpler. Also, they make for pretty entertaining reading because the assumed knowledge is much different from current user manuals (mine starts with a section called “What is programming”).

Finally, the Arduino is a nifty and popular embedded platform, particularly for home projects. This blog post on setting up Arduino timer interrupts gives a great walk-through of interrupts.

Interview Question: Stoplight Control of an Intersection

A small city has decided their intersection is too busy for a stop sign, and they’ve decided to upgrade to a light. They’ve asked you to write the code for the light. There are four lights, each with a red, yellow, and green bulb. There are also four car sensors that can tell when a vehicle is stopped at the light. Where do you start? Tell me about your design, and then write some pseudocode. (During this time, I’ve drawn an intersection like the one shown in Figure 5-11. I tend to draw this intersection on their piece of paper if I can.)

Figure 5-11. Intersection in a small city

This is a problem that lets the interviewee drive the interaction. If she wants to talk about design patterns, there are plenty. If she wants to skip design and talk about timers or the hardware of the sensing, that works too.

An interviewee should clarify the problem if it isn’t clear. In this question, she should ask whether there is a left turn light (no, the intersection is small, and the city doesn’t need that yet). Some people also ask about a crosswalk (also not to be handled in the initial development).

Very good interviewees notice that the problem is only half of what it appears on the surface. The goal is not to control four stoplights, since two lights are always in sync.

As with all of my interview questions, naming is very important. The reason I draw on their paper is to encourage the interviewee to add her own information to the diagram. Some good names include identifying the intersection by compass directions (north/south and east/west), and some moniker that makes sense for the situation (First and Main) or place on the page (right/left and up/down). Until she’s put names on the diagram, any pseudocode will be gibberish.

Once she starts digging into the problem, I like to hear about the state machine (or automata, or flow chart). I want to see diagrams or flow charts. Some people get stuck on the initial state because we all tend to want to start with “what happens first.” However, the initial state is relatively uninteresting in this case (though, if asked, I’ll suggest she start with an all-red state).

Once she’s laid out the basics, I tend to add a few curveballs (though great interviewees tend to handle these before I ask).

First, what if a sensor is broken? Or what if the city wants to be friendly to bikes (which don’t activate the car sensor)? This adds a timer to the state machine so that the intersection doesn’t get trapped in any state forever.

Second, the intersection has been getting a lot of accidents with folks running the yellow light. Can she do something to improve the safety of the system? (Hint: Add a short all-red state to allow traffic to clear the intersection before going to the next green.)

Because this interview question is simply a logic problem, I often spring it on other engineers, particularly those who work in quality departments (“How would you write a test plan for this controller?”).

Chapter 6. Communicating with Peripherals

We’ve investigated what goes on inside your processor during the course of this book. Now, before you can build a system, we need to consider what goes on in other components. Ultimately, the information you need is in datasheets, so if you’re comfortable extracting information from them, you might be able to skip the first two-thirds of this chapter (which looks at an overview of some common peripherals’ communication methods). The last third is more software-oriented, offering strategies for making peripherals and communication methods work well together. Don’t skip that part.

The Wide Reach of Peripherals

A peripheral is anything outside your processor that the processor communicates with. Peripherals come in all shapes and flavors. Because your processor has direct access to memory, it barely counts as a peripheral (though it can be outside your chip). Sensors tell your system about its environment, actuators act upon the environment, and displays interact with users.

I’m going to mention the most obvious of each of these so you get some ideas for your options. However, don’t let my lack of creativity limit your ideas. And if you can’t find what you want, wait 6 or 18 months and someone will come along with it (yay for I²C tricolor LEDs!).

External Memory

We’ve already looked a bit at memory and how it is classified into volatile memory (lost when the system turns off) and nonvolatile memory (retained through power cycles). RAM is the most predominant form of volatile memory. It comes in different flavors, but the software usually doesn’t need to know the specifics.

On the other hand, most nonvolatile memory requires a software driver that recognizes the features of the memory style. As noted in Chapter 3, two common subtypes of this memory type are EEPROM and flash. EEPROMs are generally smaller, and their bytes can be written individually. Flash is made up of sectors that must be erased all at one time (the larger the flash, the larger the sector). Once a sector is erased, flash can be written one byte at a time.

Tip

The volatile keyword in C/C++ refers to something different from the idea of volatile memory. The keyword tells the compiler, “this variable may change outside the code that is currently running,” whereas volatile memory changes only when power is lost.

Once you’ve classified your memory as volatile or nonvolatile, you’ll need a few more pieces of information about your external memory:

How large is it? Don’t be fooled by memory sizes given in megabits (Mb) instead of megabytes (MB)!

How long does it take to access the memory? This depends both on the communication bandwidth and the memory characteristics.

How much do you need to erase at a time?

How many times can it be rewritten?

Your product team may have a few other questions to help determine what kind of external memory you need:

What is the price per bit?

How large is the memory physically (or the density of the memory, the physical size per bit)?

How much power does it consume? (RAM tends to be the worst, then flash, then EEPROM).

Once you choose what is right for you, memory is probably the easiest peripheral you will have to work with.

Buttons and Key Matrices

You’ve already seen buttons with a simple I/O interface (covered in Chapter 4). However, if you have a bunch of buttons (e.g., a keyboard), you don’t need one I/O line per button. You can matrix the inputs to get a lot more out of your I/O than you expect. There are two ways to implement a key matrix. A row/column scan is useful when you need the peripheral to be cheap, whereas Charlieplexing is more expensive but minimizes I/O.

With a row/column scan, you can implement an M×N matrix with M+N lines. So if you want to implement a 12-digit number pad, you could do a 3 ×4 matrix and use 7 lines (far fewer than the 12 lines it would take to do direct I/O). Of course, matrix input requires more complicated software.

In the electronics, each button is connected to one row and one column (and not an input pin to ground, as would be the normal I/O interface). On initialization, make all of the rows outputs and set them to be high. Then, make all of the columns inputs with internal pull-up resistors. As you are reading for button presses, set a row low, read the column’s inputs, set the row high, and move on to the next row. A button that is pressed will be low when you read its column. You will need to map the row/column value to the button’s actual meaning. Figure 6-1 shows a snippet of a schematic with the row/column scan. Each circle is a button.

Figure 6-1. Row/column-scan-style key matrix

Note that matrixing the buttons means that they need to be polled. Although you can set up a timer interrupt to do that, you can’t use a processor interrupt to tell you when an input has changed. The software needs to keep cycling through the rows.

This is also true of Charlieplexing, which is normally used with LEDs to make complex displays from only a few I/O pins. The same process can be used on inputs, but it requires more electronics (diodes) than the row/column matrix method. Instead of using N pins to receive N inputs in direct drive, or M+N pins to receive M×N inputs in row/column, Charlieplexing lets you use N pins to get (N²–N) inputs. So for the 12 buttons in a number pad, you can use a mere four I/O lines (and 12 diodes, or fewer if you don’t care about detecting multiple buttons pressed). Although the software is more complicated than the row/column matrix, it is just a matter of taking it step by step. A website about Charlieplexing uses excellent animations to walk through how it works. (They also walk through the row/column method with good animations.)

Warning

Both of these methods may cause problems when multiple buttons are pressed simultaneously.

Row/column matrix and Charlieplexing can be used on outputs (LEDs) as well as inputs (buttons). The methodology is the same in each case, so once you wrap your head around matrices, you’ll find plenty of places to implement them.

Sensors

A button could be called a type of sensor. The methods used to talk to buttons are similar to those used with sensors, and the choices are similar (e.g., interrupts versus polling). However, buttons are either pressed down or not, whereas sensors can provide all sorts of data—high, low, 1.45, 10,342.81, 12, 43, or out of range. What you read depends on your sensor.

Tip

Your system may have its own input sensors while serving as a sensor to another system.

Analog sensors

A microphone, like your ears, picks up audio in analog form. You can use an ADC to move from real-world analog to software-friendly digital (ADC means analog-to-digital converter, sometimes called an A2D). Your software can do any number of things with the digital data: watch for events in the analog stream, filter it, or change it back into analog with a DAC (digital-to-analog converter, aka D2A). In analog form, the signal can be played through a speaker. Figure 6-2 shows a simple system.

Figure 6-2. Analog-to-digital-to-analog system

The signal is the underlying thing you are listening to, whether it is currently digital or analog. Noise is what makes it hard for your software to see the signal properly. A goal of the system is to make sure there is plenty of signal compared to the noise, giving you a high SNR (signal-to-noise ratio).

Signal processing is a software technique for changing data from analog to digital, and often doing something to make sense of the data. Sometimes the data gets transformed to look like something different. Often this change converts the data to a combination of sine waves because these are easy to compute with. The sine waves are frequencies (think of radio station frequencies or musical pitches). So this conversion is called conversion to the frequency domain.

In Figure 6-2, the signal is reduced by dividing each integer sample by two. The resulting analog sound should be quieter (attenuated), but integer division isn’t a good idea with such small numbers, so the signal morphs more than it should. (Receiving a higher signal input at the ADC or sampling more bits in the ADC would reduce this problem with integer math.)

This is a relatively silly example because you could get a better attenuation effect without digitizing the input. More often, the data goes through multiple stages of processing. Some of these might include filtering (to reduce noise or enhance a particular signal characteristic), companding (attenuating loud sections and/or boosting quiet ones), and convolution (finding one signal hidden inside another).

ADCs and DACs are characterized by their sample frequencies (how fast they work) and number of bits. An 8-bit ADC has 256 levels in its digital signal; a 16-bit ADC has 65,536 levels. What you need depends on your application, but those two numbers can drive how your whole software system is organized by constraining the speed of your processor and the amount of RAM you need. How fast your system can process data is called its throughput or bandwidth.

Some processors are built with signal processing in mind (digital signal processors, aka DSPs). They have special processor instructions that work with the signal much faster than a general-purpose processor does.

Of all of the embedded systems areas, signal processing is my favorite. However, it is a complete book unto itself. Well, two books, because you need to understand the math (Fourier is fun) and how to apply that to real-world problems (fast Fourier transforms!).

I don’t want you to think you are ready to go off and make a great analog system with this tiny section. For instance, I haven’t talked about sampling and the Nyquist frequency. Either you already know about it or you need far more than this brief orientation. I recommend my favorite signal processing books in Further Reading.

I started this section with a microphone, but analog sensors come in all sorts of types: motion sensors (accelerometers, gyroscopes, magnetometers), weather sensors (wind, humidity, temperature), vision sensors (cameras, infrared detectors…), the list goes on and on. You may have only five senses, but your system can have many more.

Microelectromechanical systems (MEMS) sensors are a special type of sensor that uses a tiny (nanoscale) sensing element. Although MEMS have given us more robust sensors and a wider variety of them to play with, from the software point of view, MEMS sensors are the same as any other sensor.

Note

I once heard an electrical engineer say, “All sensors are temperature sensors, but some are better than others.” That’s true. Sensors respond to ambient temperature even when they are supposed to be measuring something else. Expect to have to calibrate your system for temperature and give a sigh of relief if you don’t need to.

Digital sensors

If I have made you a little nervous with analog sensors and the arcane world of signal processing, I do apologize. However, you’ll like digital sensors a lot more. Sometimes called a smart sensor, a digital sensor will do the analog-to-digital conversion for you, ideally giving you exactly the data you need. As with analog sensors, the range of digital sensors is staggering. Although digital sensors are usually a bit more expensive than their analog counterparts, the price is often worth it because they produce meaningful samples and are less susceptible to external noise (see ).

Your primary concern in reading the datasheet for the digital sensor is how to communicate with it. Does your processor have the type of communication method needed by the sensor? The next questions pertain to throughput: can your software keep up with the amount of data the sensor is going to send? What is it going to do with the data? How long will it take?

EMI: Electromagnetic Interference

As signals travel along wires, they pick up noise from all the things around that have clocks. Wait a minute, you say—my system has a clock? Probably more than one, actually.

So you can’t expect your analog system to be free of noise. The wires that the analog signal travels along are like antennas picking up static from everything around them. Your electrical engineer can shield the signal and reduce its susceptibility to noise (usually by putting a shiny metal box connected to ground, called a Faraday cage, around the signal). But you still might have to worry about noise.

Tip

Although digital signals emit more noise to the environment, they are more immune to outside sources of noise.

Sometimes, before you can fix the issue, you need to find the culprit causing the noise. That can be difficult. If it is not external to your system, it could be any of your communication paths, any of your peripherals, or your processor itself. Even worse, it may be some combination of these interfering in tandem, giving you hard-to-reproduce errors. Radiating noise internally is not the only problem.

Sometimes, after bring-up but before the end of your project, your electrical engineer will take your system off for electromagnetic compatibility (EMC) testing. Different countries have different regulations. In the US, if your system has a clock that is over 9 kHz, it needs to go through EMC testing to ensure that it doesn’t radiate signals that will interfere with communication (to comply with FCC part 15).

This test will put the unit into an anechoic chamber and monitor it at many radio frequencies to see whether it emits any noise above the tolerated level. As you prepare the software used in hardware bring-up, you may need to put together an EMC test version that will transmit data through all communication pathways as fast as possible, to generate chatter and verify that nothing much radiates from your device.

The same EMC test lab usually can blast your unit with different frequencies to see whether it is susceptible to noise in any particular range.

Actuators

Actuators are what your software uses to make a mechanical impact on its environment. The simplest actuator is a solenoid, which you can think of as a button in reverse. If you set it high, the solenoid is in one position. If you set it low, the solenoid is in another. These are useful for locking things or turning on and off valves. However, moving things is a lot more fun if you use a motor.

Motors

Whether you are making a bipedal robot or moving a widget along a conveyor belt, motors are where the systems really get to play in the real world. Unhappily, while motors are nifty and you should definitely try them out, really great motor control requires its own book. I’ll list some things to look for, but I can’t give you anything more than a brief orientation to motors.

Tip

Just as you can buy an easier interface using a digital sensor, some motor assemblies include a motor controller chip that will implement a control algorithm compatible with the style of motor. The controller will provide a simple API to your processor as well as increase the precision and life of the motor.

First, what kind of motor are you thinking of using? There are many types of motors, and your choice will be affected by such characteristics as the life span of the device, torque (turning power), energy efficiency, and power consumption.

A stepper motor is a popular choice, though it can be relatively expensive. A stepper motor can be positioned precisely and will stay there (high holding torque). These are the simplest motors to use because you can run them in open loop, without any feedback mechanism. Because they have a number of magnets to turn the motor shaft, stepper motors require multiple I/O lines. Each I/O line gets energized to cause the shaft to step forward to the next position (this can cause a bit of a jerk). When you want the motor to go smoothly in one direction, you need to make the I/O lines dance in a complicated pattern dictated by its datasheet.

At the opposite end of the motor spectrum is a (brushed) DC motor. This is very cheap and usually requires only one I/O line (two if you want it to be able to go backward). The speed of motor depends on the voltage applied. You can use a PWM to give you more control than the binary choice between “off” and “running full speed.” Motors usually don’t like to be jerked on and off in a digital manner, so using PWM (and some very simple electronics) to give a smoother velocity profile will increase your motor’s lifespan (we saw PWM control in Chapter 4). A difficulty with DC motors can be stopping them at the precise position you want; they often require a feedback control system.

Warning

The torque of a brushed DC motor depends on the current. But your processor probably sources only enough current to drive a very wimpy DC motor, and current surges can damage your I/O lines. So these I/O lines will go through some additional circuitry to get more oomph.

A brushless motor usually has a longer life span because it doesn’t have the jerkiness of the stepper or any parts (brushes) that touch. As with a stepper motor, it needs a few I/O lines working together to achieve rotation. As with a brushed DC motor, controlling the speed is easier than controlling the position. Brushless motors are power efficient and usually priced between stepper and brushed DC motors with similar characteristics. Finally, brushless motors can be DC motors or can use alternating current.

Position encoding

The second most important thing to understand about your motor system is: how will you know when it gets to wherever it is going? With a stepper motor, you can count the steps from where you are to where you want to be. However, how do you know where you are?

If your position feedback method gives you an absolute position, you can just read that. However, many systems use a cheaper position sensor that will give you only a relative position, so you need to know where the start of the measuring stick is located.

Most motors (stepper or not) have a home sensor that indicates the absolute position in one particular location along the motor’s travel. Usually home is at an extreme (e.g., all the way to the left or at the bottom), although sometimes home is the center of travel instead of one edge. Finding home is a matter of moving the motor slowly in that direction until the home position is sensed. If the home is at one extreme, it serves as the zero position of the system, and other positions are counted from there. Once your motor is in its home position, you can use relative positioning to start moving to a location.

In a stepper motor, the position is calculated based on motor counts. In any other type of motor, you’ll need another type of sensor called an encoder. Often optical, it is usually a series of black and white (or reflective) areas that let your software treat the signal as a digital input. A rotary encoder measures the angle of the motor shaft. Three bits will let you know which of eight positions your motor is in, measuring off eighths of a circle, which is not a lot of accuracy. But if your motor moves 1 mm per full turn, it may be enough. A linear encoder measures the position of the motor along a straight line and is usually incremental, so the software must keep track of where it is in reference to a home position.

Figure 6-3 shows a simple motor system with some PWM control lines going to a motor. As the motor rotates, the encoder moves back and forth (the dots indicate the mechanical black box that includes gears and whatnot). A sensor sees the black and white areas on the encoder. The sensor connects to a counter on the processor and measures the relative position. Initially, the motor goes through its homing routine. Once there, the software can count how many lines it has on the encoder. Your processor probably has a feature that will do this sort of counting; check the “Timer/Counter” section of your user manual.

Figure 6-3. Motor and encoder system

PID control

Controlling the motor is not as simple as telling it to go until you get to a position and then telling it to stop. By then, the motor has overshot the position. You could tell it to go back, but then you will overshoot again. Unless you are trying to rock a baby, you don’t want to keep going back and forth like that.

The most common way to handle this problem is to use PID control. PID stands for proportional, integral, and derivative. Each term handles a part of the problem, and together they can tell you how much power to send to the motor (usually in terms of PWM level). The procedure starts with an error measurement that is simply the difference in position between where the motor currently is (the process value, or PV) and where you want it to be (the set point, or SP).

The proportional term is easy to understand. It is just a constant multiplied by that position error:

          error = goalPosition – currentPosition;
          PID.proportionalTerm = PID.proportionalConstant * error;

So when the motor is far from the goal, your software gives it a lot of juice, whereas when you are close to the goal, it powers down. Sometimes this is all you need. However, starting the motor full-on when you are far away from the target will cause it to jerk, which is bad for it. Further, you may still overshoot the target, leading to oscillations around the goal position. Or if your error is small, the proportional term will never give the motor enough power to actually move, so you end up stuck in not quite the right position (a steady state error).

The integral term helps these problems by adding up the error over time:

        PID.integralSum += error;
        PID.integralTerm = PID.integralConstant * PID.integralSum;

Not only is the integral term proportional to the amount of error (as the proportional term is), but it also takes into account how long there has been error. After the motor has been commanded to move, the error has been around for only a short time. But after a few cycles of the PID control loop, if the motor hasn’t gotten close to its destination, the integral term lends oomph, accelerating toward the goal. However, it is kind of like momentum. Once the integral term gets going, it tends to cause an overshoot, one even worse than the proportional term would alone. Although the controller will find its way back to the goal position—sans oscillation or constant error, thanks to the integral term—it would be better not to overshoot the target. This is where the derivative term comes in:

PID.derivativeTerm = PID.derivativeConstant * (error – previousError);

This makes the controller decrease the output if the error is decreasing. This adds some friction to balance the proportional and integral terms, usually counteracting the PI terms as the error term gets smaller.

The output of the PID controller is just the addition of these three terms. Figure 6-4 shows a reasonably well-tuned interaction between the terms and their PWM output as the system is commanded to go to a position (or temperature). Note that with the integral and derivative terms, the output of the system feeds back in to the controller, so this is a feedback system. As such, it can be unstable. Too much proportional control can make the motor oscillate (or ring). Too much integral control, and the motor may smash itself to bits as it overshoots the position. Too much noise in the error channel, and the derivative term can lead to random results. Feedback systems that get out of control can do dangerous things.

Figure 6-4. PID response over time

However, there are many, many engineering problems where a PID controller provides a good solution. I’ve used PIDs with motors, heaters, and modeling spring systems. This control method is widely used and well understood; consider it an engineering design pattern. And you can see from the earlier code snippets that implementing one in software is simple.

However, those constants? The ones that describe how much weight each PID term brings to the output? Figuring those out is a painful process called tuning. The field does provide some easy guidelines: start with proportional control until you get it working pretty well, then add integral until it works better, and then add a little derivative to prevent too much overshoot. But three weeks after the project is supposed to end, you may still be cursing the person who gave you this advice, telling you to just tweak the parameters until the motor works.

There are also some mathematical tools to help you with tuning (e.g., the Ziegler-Nichols method). Despite the mathematical tedium and having to estimate some inputs (the real world doesn’t match the mathematical ideal), I recommend using these in addition to trial and error. But these methods often require a formal and deep understanding of the problem.

Note that the PID concept is pretty standard, but implementations vary. For example, as the derivative term is sensitive to noise in the error signal, many implementations will use an average of error over several cycles. This hardens the derivative term against noise-induced insanity but makes it less responsive to the system. There are many dozens of these little tweaks to ensure better performance and deal with system-specific issues.

Warning

At this point, I have given you enough information to be dangerous but not enough to do a good job. Further Reading suggests some books to help you implement and tune a PID controller (and some other control methodologies).

Displays

Your system output may go beyond what actuators give you. Some embedded systems have displays for dealing with those pesky humans. As with sensors, your display may be analog or digital, as simple as an LED, or as complex as an LCD with touchscreen. The range is huge.

Segmented displays

We’ve already spent a lot of time on individual LEDs in Chapter 4, even making them dim using PWM control. A seven-segment display is probably the next most basic display that you’ll see. Most seven-segment displays are ordered as shown in Figure 6-5.

Figure 6-5. Seven-segment display

Each LED in the segment can be represented as a bit in a byte (the extra one bit is sometimes used for the decimal-place segment found on some displays). The segments are accessed in order: abcdefg (or sometimes gfedcba). So if you want to put a 3 on the display, you need to turn on the LEDs at abcdg and turn off the LEDs at fe. In hex, using abcdefg, you’ll want to write 0x79 to the display to represent the number 3.

The actual implementation here is not that important. The critical idea here is the decoupling between human-readable data on the display and the representation in the code. Get used to having to map between the display’s context and other parts of the software.

Taking a step back, do you remember solar-powered calculators? The LCD display with eight digits (each one a seven-segment display)? The ones that were amazing technology in the 1960s but were handed out for free by the 1980s? They also had at least 17 buttons. They could total up to 81 different settings (17 (buttons) + 8 (places) * 7 (segments in each character) + 8 (decimal points)), but the processors that ran inside those calculators didn’t have 81 I/O lines.

As noted in Buttons and Key Matrices, you can matrix the button inputs: 17 buttons can use 9 lines (4×5), and you can get a three more buttons for “free.” The same method can be used to matrix the display outputs. Using row/column scanning, for eight characters with decimals (64 segments), we can use an 8×8 matrix, so only 16 I/O lines are needed. With buttons, the response time depends on the number of rows because you cut the time spent looking at each column into that many slices. With outputs, the time slice determines how long each segment can be on. So, if you use an 8×8 LED array, the LEDs will be on only 1/8 of the time. Most LEDs are so bright that this doesn’t matter. With LCDs (such as those found in our solar calculator), the limited amount of on-time can make segments look washed out and difficult to see, particularly when the power is low.

Tip

LCD segments don’t run with a constant “on” setting like LEDs do. Although LCDs can be driven through a matrix, it is more difficult because they need an AC signal. Usually you’ll use an LCD controller, but if you want to try matrixing it (or just want to understand it better), check out this ST Microelectronics AN1447 Application Note.

The 8×8 matrix is very convenient because it lines up with our character mapping very well. We can either power all of one character at one time or power all of the a-segments, then all of the b-segments, and so on. Either method works. As long as you refresh the display fast enough (get through all of the segments faster than 30 Hz), the user will never know.

Pixel displays

Unfortunately, seven segments per character isn’t enough to represent much. You can choose a 14-segment display to get decent looking letters for English applications (or a 16-segment display to incorporate Latin letters and Arabic digits). At some point, you might as well use a dot matrix display and build your digits, characters, and graphics out of dots. Well, dots sound so plebeian. Let’s call them “picture elements.” Naw, that takes too long, let’s call them pixels.

Now your mapping between the number 3 and what appears on your display may be even further apart than 3 and 0×79. Each thing you want to put on the screen needs a bitmap to describe what bits are on and what bits are off. Anything that goes on the screen is a glyph, whether it comes from a bitmap or is generated by the code (such as a graph).

All of your bitmaps together are called your graphic assets. Graphic assets are usually split into fonts and other graphics (pictures, logos, etc.). A font is a set of glyphs representing a set of characters (typically the ASCII character set). As before, the goal of designing a display subsystem is to keep images of the data loosely coupled to the data itself. This allows you much greater flexibility to change screens, give your system a new look, and reuse your display subsystem in a different product.

Figure 6-6 shows one way to set up graphic assets. First, there is a table of data, usually with a header file that tells you where the logo is, or perhaps the offset to the animations. It is important to version the file (and the asset list) because if they change without your software knowing it, you could display garbage to the screen.

Figure 6-6. Graphic subsystem organization

To put the number 3 up on the screen, first you need to know what representation of 3 to display: is the bitmap going to be small or fill the whole screen? Is it going to be bold or italicized? Each of these options will lead to a different font set in your assets. Once you know the font, you’ll need to map from the character to the bitmap, usually by searching through the character map to get an offset into the bitmap glyphs.

Bitmaps can be stored in monochrome (1 bit per pixel), as shown in Figure 6-6. Look at all the wasted data, though, that can be compressed. Compressing it may speed up retrieval from wherever the asset is stored (often off chip flash). However, uncompressing the data becomes one more thing for the processor to do.

One form of compression that might not slow down the processor at all is run length encoding (RLE). Sections of data that are the same are stored as a single data value and a count, rather than each pixel being replicated. This is great when you have a lot of the same color on the screen. For example, you might get a stream of white pixels (W) and black pixels (B) that look like WWWWBBWWW. This would encode to W4B2W4. If the data is stored as one bit per pixel (so the whole image is only black and white), that would give you no savings. However, if the image is one byte per pixel (or two or three bytes per pixel), RLE provides significant compression without much processor overhead.

Display subsystems end up pushing a lot of data from one place to another. A monochrome bitmap is fairly small. In Figure 6-6, the number 3 glyph will be 8 pixels wide and 8 pixels high, so it can be represented in 8 bytes. However, it will probably look terrible with those blocky edges. To make it look better, you’ll want to add antialiasing, which softens the edges by putting grayscale pixels in. Figure 6-7 shows a little bit of smoothing on the number 3 bitmap. It uses five levels, so it would need three bits per pixel.

Figure 6-7. Antialiasing of a character

With antialiasing, the bitmap is no longer monochrome. You could use two, four, or even 8 bits per pixel to describe the antialiasing. An 8-bit-per-pixel encoding means the number 3 glyph takes up 64 bytes—still a really small amount, but you’re on a slippery slope. Before, your whole A–Z, a–z, 0–9 monochrome font could fit into 500 bytes, but now it is almost 4 KB, and that is for 8-pixel-high letters. Once you have a 2.4-inch LCD display (24-bit color, 240×320 pixels), each screen can take up to 225 KB.

Coding a display subsystem often means optimizing your code so the parts handling this data throughput don’t cause problems with the screen. Most LCDs automatically refresh, which means the display hardware redraws the data on the screen. The LCD refreshes the screen with the same image over and over again until your data changes. Tearing is when the data changes partway through a refresh, showing half of the new image and half of the old image. To completely eliminate tearing, you need to synchronize the image update with the screen refresh and get all of the pixels to their intended value before the next refresh (it can be difficult to transfer all the data in such a short time).

Note that how you pack the assets into the storage mechanism is important. In addition to compression concerns, you need to store them in a way that makes sense for the space available, the interface between the graphic assets storage (usually flash) and your processor, and the interface between the LCD and the processor. Another issue might intrude as well: what if your display has to go into the product enclosure upside-down due to electro-mechanical requirements? It may require more frontend processing to put the assets in a different orientation, but it is better than having the processor turn them upside down and backward each time the asset is read.

Tip

Being an embedded systems specialist does not excuse you from writing programs to pack data assets. Since you’ll be the one using the information, you’ll find your life is much easier if you control how the graphics are stored.

Finally, once you have the bitmap ready to send to the screen, you’ll need to know where to put it. Figure 6-8 shows the number 3 glyph on the screen.

Figure 6-8. Number 3 on a display

To sum up the steps:

Determine what is going on the screen and where.

Look in the assets to find the correct font.

Find the character in the font.

Read the bitmap, possibly uncompressing it.

Send it to the display, ideally right after the last refresh.

To put up a logo or other fixed graphic, combine steps 2 and 3 into one: looking up the image glyph.

Hosts and Other Processors

From your processor’s point of view, a host computer is just another object to talk to, the same as any other peripheral. Sure, the host might meddle with your device, such as changing your processor’s code or instructing it to move the motor to a different location. But all of those things are just digital communications. In fact, if you’ve been sending bytes to your display or your motor, it has a processor. And if your sensor is a digital one, it has a processor. Each processor is the center of its world, so to the digital sensor, your processor is a host.

Just as each of your peripherals has a documented interface, your system’s interface to its host should be reasonably well documented (even if you control both sides). Putting a documented interface in place will let the two systems change independently. As for implementation, the command interpreter in Chapter 3 is a good place to start.

As more of our everyday objects enter the networked world, we build up detailed pictures of the environment. Whether it is a fridge that sends a shopping list to your phone when you’ve entered a grocery store or a scattering of small sensors monitoring a forest for fires, we are building distributed sensor networks. Increasing the intelligence of the sensors makes each one better able to deal with irregularities in its particular location, making the whole network more robust. Note that the information can return to a host for integration and processing (e.g., forest fire sensors), or the information can be generated with each piece contributing its knowledge in a host-less system (e.g., groceries).

The strength of the sensor network is that each element works together to construct information that is greater as a whole. However, that is also their weakness; if the system cannot communicate, it is much less useful. Further, if enough sensors go offline, the system can become degraded to such a degree as to be useless.

So Many Ways of Communicating

The Open Systems Interconnection (OSI) model is a way to talk about communication protocols as a series of layers (see Table 6-1). Each layer provides some feature to the one above it to build up a communication pathway.

This model is far more complex than you’ll need for most embedded systems, but by having an idea of how a big, complicated communication pathway works, you’ll get some ideas about how little ones work. (Hint: most of them combine or skip layers.)

Table 6-1. OSI model

Layer | Function | Questions for this layer | On a PC’s Ethernet
1. Physical | Provides the electrical and physical specification | How many wires connect your processor to a peripheral? At what voltage? At what speed? | Ethernet cable
2. Data link | Describes how bytes flow over the physical wires | Do the bytes have parity checking? How many bits are sent and received in each frame? | Ethernet (802.xx)
3. Network | Gets a variable length of information (packets) from one place to another | How is each system addressed? How does the layer break up (and re-form) big blocks of data into amounts that can go over the communication pathway? | IP
4. Transport | Moves blocks of data in a reliable manner, even if those blocks are larger than the lower levels can handle | How do you count on data being received even when there is a glitch in the wires? How are errors recovered from? | TCP
5. Session | Manages a connection between the local and remote application | How to send this data from here to there? | Sockets
6. Presentation | Provides structure to the data, possibly encryption | How is the data organized when it is sent? | TLS and SSL
7. Application | Takes user interaction with the software and formulates a communication request | What command to send when a button is pressed? | HTTP

I’ve put Table 6-1 upside down because the first few layers are the most important for embedded systems. Although you may need to know how the data is sent to a flash chip or received from a digital sensor, you won’t get many options for implementing the presentation layer; that information will be specified in the datasheet.

Tip

Here are some mnemonics for learning the OSI layers, from the top down:

All People Seem To Need Data Processing

All Penguins Stand Too Near Deep Pools

Or from the bottom up:

People Design Networks To Send Packets Accurately

Please Do Not Touch Steve’s Pet Alligator

For an embedded system, most of your attention will be spent on the data link layer and how to move bytes from one place to another.

Serial

At its broadest definition, serial communication is the process of sending data one bit at a time over a communication pathway. Under this definition, even Ethernet is a serial form of communication. Let’s narrow the definition down a bit and say a serial port is something that can be implemented with a universal synchronous/asynchronous receiver/transmitter (USART). That probably doesn’t help you much, does it? Let me define some terms, and then we’ll look at the most common serial communication methods.

A UART (pronounced “you-art”) implements protocols such as RS-232, the default protocol used when someone wants to hook a serial cable to your computer.

Tip

Even high-end Internet routers often have a serial port for configuration or debugging. Not only is it cheap to implement, a serial port is also like a torx screwdriver: most people don’t know what they are. Using one may convince other people that you think you know what you’re doing.

UART is missing the S in USART (“use-art”). The S is for synchronous, which means the clock is a separate signal from the data. An asynchronous pathway has an implicit clock, so it must use start and stop bits to indicate when communication is occurring (thereby decreasing its bandwidth). However, in a synchronous pathway, to maintain the synchronization, both ends send bytes at the same time and are always trading data, even when one of them doesn’t have anything important to say. Serial Peripheral Interface (SPI) is a popular synchronous bus.

Another important factor when considering serial ports is whether you have two wires to communicate on (transmit and receive) or only one wire (which switches direction depending on whose turn it is to talk). Having two wires is called full duplex; sharing a single wire is called half duplex. 1-wire bus and I²C are two of the most popular half-duplex protocols. Of course, half-duplex serial ports are a little more complicated to debug because you can’t be sure on your oscilloscope (or logic analyzer) which chip is the source of the bytes.

Tip

The timing diagrams from the peripheral’s datasheet are often critical for getting a communications driver working. As you bring up a driver for the first time, expect to spend some time setting up an oscilloscope and trying to re-create those diagrams (in order to debug your driver). Look in Chapter 3 for tips on datasheets and O-scopes.

When encountering a new peripheral bus, the things to pay attention to include:

How is the communication clock generated? Is it implicit (all parties agree upon it ahead of time) or explicit? If it is explicit (carried on a wire on the bus), does the clock generator (usually the bus master) have any other special responsibilities?

How many hardware lines does it need?

Is it synchronous or asynchronous?

Is it full or half duplex?

Is it point-to-point, or is there point-to-multipoint? Is addressing in the protocol or done via chip select?

What is the maximum throughput of the system? How many data bits can be transferred every second?

How far can the signals travel?

The real goal here is to figure out how long it will take you to implement a driver using the communication method in question.

Warning

Many processors come with example code, particularly for their communication drivers. Sometimes this code is excellent, but sometimes it is only an example to get it running (but not working robustly). Be sure to review this code if you incorporate it into your project.

Moving on from the higher-level terminology and general questions, let’s look at how some popular buses behave. Remember that this is still just an introduction. In the end, you’ll need to look at your datasheet to see what the peripheral needs and at your processor manual to see how to provide it.

RS-232 and TTL

As we’ve seen, the word “serial” is overloaded to mean many protocols. Even the most common serial interface (the one between your system and your computer) has several names. I already mentioned UART, which is the part of your processor that implements this interface. However, it sends out TTL-level serial signals,[4] and another chip converts them to RS-232-level signals (+/– 12 V).

Figure 6-9 shows a common use of serial cabling in embedded development. It lays out a possible debug pathway from your computer to your embedded processor. Although RS-232 defines eight signals (and ground), the most important signals are transmit (TX) and receive (RX). You can interface your embedded system with your computer using those alone. Note that both systems have a TX and RX, so you’ll need to swap them to connect each TX to the RX on the other side. This may be done in your cable (using a null modem cable) or on your board (in which case you need a standard or straight-through serial cable). Serial USB-to-TTL cables, available from vendors such as Sparkfun, can be used to take out the intermediate steps.

Figure 6-9. Serial connection from the embedded system to a PC

Warning

There are many different vendors of USB-to-serial cables. Some cables quietly drop or mangle bytes when the transfers get too large or go too fast. Make sure your tools can keep up with your system.

If you are hooking up to a cellular data modem or serial-to-Ethernet converter, you may also need some of the other RS-232 lines to indicate when the modem is ready to receive data (RTS/CTS handshaking). At this point, even though the clock is implicit, you’ll need to designate which side is the bus master, because the signals are not symmetric. In RS-232, the bus master is the data terminal equipment (DTE) and the slave is the data communication equipment (DCE). Yes, pretty much every protocol has different names for similar things.

This form of serial communication resides at the physical and data link layers of the OSI model. There is no built-in addressing scheme, so it can be established only between two parties. Both parties must agree upon the baud rate (bits per second), parity, number of start and stop bits, and flow control. Flow control can be hardware using RTS/CTS, software using Xon/Xoff, or no flow control. Further Reading provides resources that explain these conventions.

Tip

To talk to a processor’s debug serial port, usually you should choose 8 data bit, no parity, one stop bit, and no flow control. This is often abbreviated as 8N1, no flow.

The baud rates are usually between 2,400 and 115,200 (though they can go as high as 921,600). Certain baud rates are very popular: 9,600, 19,200, 38,400, and 115,200. Due to the implicit clock, you will need to configure the baud rate on both devices (i.e., your processor and your computer).

Because the data link layer transmits extra bits per byte sent (those start, stop, and parity bits), most serial connections have a throughput in bytes about equal to their baud rate divided by 10. So if you have a baud rate of 19,200, your system is going to get about 1,920 bytes per second. Compare this with standard 100-megabit Ethernet (though 1-gigabit is pretty common too), which gives a throughput of around 11 megabytes per second, depending on the protocol. Serial is slow.

The relatively high voltage of RS-232 (12 V!) means it can travel pretty far before the signals deteriorate, up to 50 feet (15 meters) with a normal serial (or null modem) cable. A special cable (low capacitance) can increase the distance by about a factor of 20.

RS-232 has been around seemingly forever and continues to be widely used. Even though its demise is often heralded as new protocols take its place (Ethernet, USB, etc.), RS-232 keeps coming back because it is easy to implement in software and cheap to implement in hardware.

SPI

RS-232 works pretty well, but it has some deficiencies: about 20% of the bitstream is overhead; both sides have to know the setup (e.g., baud rate and parity); the RX/TX crossing requirement is a pain because which wire is RX (or TX) depends on your point of view; and other difficulties crop up.

So you have an alternative: SPI (pronounced “spy” or S-P-I). Four kinds of wires connect your processor to a SPI peripheral:

Master-In Slave-Out (MISO), also known as Serial Data In (SDI)

Receive for the master.

Master-Out Slave-In (MOSI), sometimes known as Serial Data Out (SDO)

Transmit for the master.

Clock (SCK, CLK, or SCLK)

Generated by the master, this is the clock for both sides of communication.

Chip Select (CS) or Slave Select (SS)

One line per peripheral. This is generated by the master and is usually active low.

SPI is a synchronous protocol, so the master and slave both have to send data when the clock is going. Sometimes the master will not have a command that it needs to send, but it will need to transmit a byte in order to receive one. The value 0xFF is the traditional byte to transmit when you want to force the slave to send data. This is called “clocking out” the data on the slave.

With RS-232’s implicit clock, both sides of the communication need to have a relatively precise oscillator; otherwise, they may lose synchronization. Since the master provides the clock, SPI doesn’t require either side to have a precision oscillator. The master probably still has an oscillator of some sort, but it doesn’t need to be as precise, so it can be cheaper.

The clock speed can be slow (tens of kHz) or fast (up to 100 MHz). The clock speed is the baud rate, and there isn’t any overhead, so a clock of 8 MHz leads to 1 MB/second, much faster than RS-232, and at high clock speeds, even comparable to Ethernet. Finally, the clock speed can be whatever your processor can clock and the particular peripheral can receive. There is no need to try to run at precisely 8 MHz; it is acceptable to run at 8.1234 MHz or 7.9876 MHz. The flip side of running a clock so fast is that it doesn’t travel very far. SPI signals usually don’t leave the board, and in a cable they would travel less than a meter before deteriorating.

The protocol defines a chip select, so multiple peripherals can often share a single SPI bus as long as there is one CS line per peripheral.

Warning

Check your peripheral datasheets; not all peripherals play nicely on a shared bus.

A SPI driver is even easier to implement than an RS-232 driver. The only tricky part is to figure out whether you want the data on the line to be valid when the clock edge rises or falls (clock polarity and phase). Your peripheral datasheet will tell you what it wants, and the processor manual will give you some registers to configure it.[5]

The SPI interface is so easy that it is often implemented as a bit-bang interface. A bit-bang driver uses I/O lines to do the work of the communication interface. You can create a bit-bang driver for any of the serial interfaces, but it consumes far more processing power than a built-in processor interface. However, sometimes your processor doesn’t have the hardware interface to implement the communication method you need, so you make your own.

For example, to make your own SPI interface, start with four unused I/O lines and a built-in timer. The timer drives the clock I/O line. At each timer tick, either an output bit is written to the MOSI line or an input bit is read from the MISO line. (The chip select line works the same as it does in a built-in interface.) Because this is synchronous communication, you need to write a bit and read a bit for each full clock cycle. However, the peripheral datasheet will probably show you that the safe-to-read time is half a clock cycle away from the safe-to-write time, so set up the timer to interrupt at twice the intended communication clock frequency.

I²C

I²C stands for inter-integrated circuit, and is pronounced “eye-squared-see” or “eye-two-see.” I²C is sometimes called TWI, for “two-wire interface.” Like SPI, the I²C bus has a master that provides the clock and starts the interaction. However, unlike SPI, the I²C bus can have multiple masters. A master can even switch sides, allowing a peripheral to control the interaction.

²C is what happens when folks start wondering how few wires they can use to connect a whole bunch of things together. Apparently, you need three connections: SCL for the clock, SDA for the data (going both ways on the same wire, making this a half-duplex protocol), and ground.

However, the simplicity of hardware design is paid for in software complexity. Not only that, but because this is a multipoint protocol, allowing multiple peripherals (and multiple masters), it specifies an address scheme, which moves it beyond OSI layers 1 and 2 to layer 3 (network). On the other hand, I²C is fairly widely used, so you should be able to find some example code to help you implement your driver.

I²C drivers invariably include a state machine to deal with the complexity of switching the direction of the bus between the master transmitting and the slave transmitting. The master starts communication by sending a 7-bit address and indicating whether it wants to read from or write to the slave. The slave with that address then sends an acknowledgment (ACK). Next, the master sends a command to (or reads from) the slave, and the interaction proceeds. When the communication is complete, the master sends a stop bit.

The number of components attached to the bus is limited by the address space (and by the capacitance of the bus). The 7-bit addresses are allocated by NXP (the company that invented I²C, although lots of processors now use it). Many components let you set the last few bits of the address using pull-ups, so you can put several instances of the same part on the bus. Alternatively, some components have slightly different part numbers that result in different I²C addresses.

Common I²C bus speeds are 100 Kb/second (standard mode) and 10 Kb/second (low-speed mode). However, like SPI, because the master sends the clock, these frequencies don’t need to be precise. Some peripherals implement faster speeds: 400 Kb/s, 1 Mb/s, and even 3.4 Mb/s.[6]

Although some overhead is built in to the I²C bus—the bus arbitration and start/stop bits—if you are moving large amounts of data around (e.g., reading from an ADC or an EEPROM), the overhead becomes a negligible percentage of the traffic. So standard mode can achieve 12.5 KB/s throughput on the I²C bus.

I²C generally isn’t used to communicate to distant peripherals, but it can go a few meters before the cable degrades the signal too much (though you can get transceivers and special cables to increase the distance). As the protocol needs only two wires to go to many peripherals, your cable can be only four wires in total (the peripherals usually also need power and ground).

1-wire

I²C doesn’t actually win the award for having as few serial wires as possible; there is still the aptly named 1-wire bus. It provides low-speed data communication and power over a single wire. (Well, you need ground, too.) It is similar in concept to I²C (master, slave, arbitration, a software driver with a state machine), but it has an implicit clock of 100 Kb/s. Although this is a fairly low data rate, it can be used for longer ranges, up to 10 m (100 m with special cables).

Like I²C, the bus can be used with a wide variety of peripherals, including memory, sensors, ADCs, and DACs. However, the most common 1-wire implementations are in authentication chips. These are used to authenticate the origin of pieces of the system that can be replaced. For example, if you are making a printer cartridge (or any form of consumable) and you want to deter competitors from making a drop-in replacement, a 1-wire secure authentication chip will force your consumable to send the correct password to your firmware before it can be used.

USB

All of the serial protocols previously mentioned can be bit-banged if you don’t have the hardware (and you have enough processing power). I don’t know that I’d try that with USB, because it is pretty complicated. It is an asynchronous system, with an implicit clock and up to 127 devices. Depending on the version, it can be full or half duplex. And like most of the other protocols discussed here, it is asymmetric (it has a master that controls communication). It is also fast (1.5, 12, 480, or 4,000 Mb/s, depending on the version). The cable can be of medium length (five meters) and still maintain signal integrity. USB manages high throughput over a longish cable by having differential signals (which requires twice as many wires so it can travel farther).

USB provides not only communication, but also power (5 V, 500–900 mA, depending on version). That’s why consumers can power up their cell phones or cameras by plugging them into their computers (assuming the computers are also plugged in to a power source).

Like Ethernet, the USB interaction usually is complicated enough to warrant an operating system (or a USB protocol stack that goes far beyond a simple driver). Further, USB applications usually implement all or most of the OSI layers.

Miscellaneous other protocols

By now, I hope you are seeing the commonality of the different serial protocols. There is no need to memorize what is what. When you get a datasheet for a peripheral, it will tell you what it needs, and your processor will tell you how to implement that (or if it doesn’t, Wikipedia will probably give you pseudocode for a bit-bang driver). The goal here is to think about what makes a protocol easy or hard to implement. Some take-aways:

The more OSI layers you need, the more complicated the implementation is. Many protocols don’t fit neatly in the OSI model, but it is still useful to know the framework.

Point-to-point is easier than a network, which requires addresses.

Half duplex is harder than full duplex because you have to switch the wire around.

Synchronous is easier than asynchronous because you know exactly when you’ll get a byte.

Having a master on the bus (asymmetric) is generally easier than trying to figure out who gets to talk when no one has control.

Explicit clocks are easier than implicit.

So, say you need to implement a contact interface to a smart card. Smart cards use ISO-7816, which is a half-duplex, point-to-point, asymmetric protocol where the master provides the clock (in the 1–5 MHz range). How hard will it be to implement? Given that information, it looks more complex than SPI but simpler than I²C. Add a little to your planning estimate because the protocol is relatively rare and you probably won’t be able to find good example code. Now what if I tell you that it implements four or more of the OSI layers, though somewhat simplified? That should raise your implementation estimation by a factor of three (at least).

How about something more industrial, like RS-485? Designed to travel long distances, it can be full or half duplex. When it is full duplex, it is asynchronous. The clock is implicit (100 kHz–10 MHz), and only the OSI physical and data link layers are specified. Usually there is a bus master, but that isn’t required. With only that information, the difficulty of implementing the protocol sounds like a cross between RS-232 and I²C.

If you can find the commonalities between different protocols, then once you’ve written something similar, you’ll know what to expect when implementing something new.

ASCII Characters

If your test code for a new serial driver sends “Hello” as its first word, you can’t expect to see an electronic hand waving on the oscilloscope during debugging. The letters get translated into numbers, usually 8-bit numbers according to the ASCII character encoding scheme.

Letter: | H | e | l | l | o
ASCII: | 0x48 | 0x65 | 0x6C | 0x6C | 0x6F

So when you see “Hello” starting on your transmit line, it will start with H, which is 0x48, which looks like 01001000. Because ASCII uses only seven bits (it has 128 characters), the first bit is always 0, which makes it easy to find ASCII characters on an oscilloscope. In Figure 6-10, I’ve shown how to make it a little easier to know where you are in the data stream by starting the transmission high, to show the falling edge of the data. You might be able to see the start of the signal by triggering on chip select (if your protocol has one).

Figure 6-10. Compare ASCII “Hello” with UU3

Or you can select some letters that will be easier to pick out on the O-scope. I like “UU3.” Each U is 0x55, which means every other bit is set so it looks like a clock signal. And 3 is serendipitously 0x33, or binary 00110011, so it looks like a clock at half the speed.

Learning the ASCII chart is a lot like learning Morse code. It is very handy if you use it, but if you are just learning it for fun, don’t expect long-term retention. On the other hand, if you end up writing about one serial driver a year, you may start remembering the highlights. The ASCII chart is set up in a logical way, so it is easier to remember only the important stuff:

'0' -> 0x30
'A' -> 0x41
'a' -> 0x61

The numbers and letters increment from those starting points (hence 0x33 for the digit 3). My shorthand doesn’t help you know where the punctuation and the control characters are, but it summarizes the table well enough to talk to another embedded engineer in case you get trapped somewhere with binary communication.

Warning

Although ASCII is the most prevalent encoding scheme in embedded systems, it can’t fit characters from other languages into 8-bit words. Unicode uses 16 bits (sometimes more). If your product will be internationalized, figure out whether Unicode is the right choice for you.

Parallel

Parallel communication just means having several I/O lines so that you can send multiple bits to and from a peripheral at the same time. By putting 8 or 16 bits on the bus at once, you can communicate very quickly. The data bits of the parallel bus are almost always combined on a single bank of I/O pins. This lets you set all of the bits by writing the data byte to the I/O register. Instead of setting a single bit, as shown in Chapter 4:

LPC_GPIO1->DATA |= (1<<2); // IO1_2 high

you can modify several I/O lines at once:

LPC_GPIO1->DATA |= data & 0xFF;   // put 8 bits on the parallel bus: IO1_0 to IO_7

A parallel interface tends to make a chip more expensive. I/O lines take up space in the silicon, and the size of the silicon is proportional to the cost of making the chip (though for many chips, RAM is a larger consumer of silicon than the I/O lines). Figure 6-11 compares the number of lines needed for some of the most popular buses and peripherals.

Figure 6-11. Comparing peripheral communication methods

Parallel interfaces tend to be reserved for things that have high data throughput requirements: LCDs and external memory. As noted earlier in this chapter, a cell phone-sized LCD (240×320 pixel display) needs 225 KB per screen (if you update the whole screen). In a serial bus, that would mean 1.8 Mb per screen (225 KB * 8 bits per byte). The update rate is at least 30 Hz, so you’d need to move 54 Mb/s, faster than SPI could manage. However, with an 8-bit-wide parallel bus, the bit rate per line goes down to 6.75 Mb. The data gets spread over the whole bus, so the more lines in parallel, the higher the throughput.

Parallel buses tend to be half duplex, with control lines to indicate which direction the data is going (read and write lines) and whether the bus is connected to a chip (chip select line). The control lines also tend to be the clocks for the system, so a write interaction would look like the following:

De-assert the write control line.

Select the chip.

Set the parallel bus to the data intended.

Assert the write control line.

Go to step 2 until all data has been transferred.

Check the timing of your peripheral datasheet to figure out whether you need some delay between these steps. All in all, implementing a parallel bus is pretty easy, on the order of a SPI driver, although debugging is a bit harder because there are so many lines to look at.

Tip

If you have spare I/O lines available during design, bring them out to a header. You can write error or event codes to the I/O lines to make a parallel bus for debugging (and profiling!). Hook the lines up to a logic analyzer to get a peek into what your system is doing (without the hefty timing changes that a serial output would give you).

Ethernet and WiFi

Because it is so complex, using Ethernet usually implies an operating system. Since this book doesn’t make any assumptions about you having an embedded OS, I’m not going to spend many words on this. However, as systems become more intelligent, Ethernet becomes more important. Due to the standard infrastructure around Ethernet, it is easily integrated into a large system (such as those distributed sensor networks mentioned in Hosts and Other Processors). Finally, while Ethernet is the data link layer, it implies that there will be a network and transport layer as well, so your software will interface to a networking protocol, usually TCP or UDP. (TCP provides a reliable communication path so your data always gets there, but it is more complicated than UDP.)

Communication reliability isn’t that important when you are communicating with an LCD controller that shares board space with your processor. However, if your system is in the middle of the ocean monitoring for tsunamis, you may want something more reliable than serial communication.

If you are using a wireless network, you definitely want something reasonably reliable. Radio networks are hard. I mean, they are really hard. Murphy (of Murphy’s Law) loves radio networks.

Warning

One of the most difficult parts of dealing with a radio network is its dependence on the environment. I know of one system that worked when it was installed in November but gradually stopped working as spring came. Tree leaves absorb 2.4 GHz radio waves.

There are amazing and fascinating applications of networked systems. However, when faced with the cost of a good radio, most companies flinch. At that point, someone will say, “Let’s just implement our own. The components aren’t that expensive.” I know you want to agree, as your product is amazing and the radio cost is a big hurdle to make it truly compelling in the market. I’ve been there, thinking, “How hard could it be?” Take how hard you think it could possibly be and multiply that by at least one order of magnitude.

So don’t reinvent this wheel, as expensive as it seems. Spend the time making your system work. Once that’s done, you can cost-reduce the product to implement your own Ethernet controller or wireless network. In the meantime, buy the radio module off the shelf.

[4] By definition, TTL means 0–5 V. However, in practice it means whatever voltage level your processor’s digital I/O is at: 0–5 V, 0–3 V, 0–1.8 V, etc.

[5] If your datasheet isn’t sufficient, look at the Wikipedia page on SPI. Many protocols are best described on Wikipedia, often in more detail than from terse datasheets.

[6] Remember, “b” is for bit and "B” is for byte (almost always), so to convert from Mb to MB, divide by 8.

Putting Peripherals and Communication Together

So far, we’ve seen the data generators and the data consumers. And we’ve seen how to send and receive data. However, there are some pieces missing between these two. Understanding how to move the data around in your software is critical to making a great system. Let’s start with the big picture of handling the data at a system level, and then work down into the mechanics.

Data Handling

In Chapter 5, we looked at systems that were event-driven: things happen because sensors were activated, causing events that in turn caused changes to the state of the system. However, not all embedded systems can (or should) be set up as event loops and state machines.

There is a class of problems where the goal is to get data, process it, do something with the results, and repeat. In such data-driven systems, there are no events, just an ever-increasing mountain of data for your software to process. Ideally, the system can wade through the data just a tiny bit faster than it is generated. Some examples of data-driven systems include:

An airplane’s black box continuously records audio and telemetry.

A gunshot location sensor listens to its environment. Upon identifying an impulsive sound, it generates an event to send to a host.

A reconnaissance satellite records image data, compresses it, and (when in range of a link point) sends it to Earth.

An MP3 player reads data from its audio store, uncompresses it, and sends bits out of a DAC to generate a signal that sounds like music to your ears.

An autopilot system reads position and adjusts vehicle velocity.

A data-driven system can be understood by looking at the flow of data. The rate of the data and how quickly it needs to be processed are the primary features of the system.

Reboots and errors can cause the system to fall behind. This is one type of system where failing gracefully is important. If the system had an error that caused it to fall behind, should it skip some of the data and catch up? Or should it do some sort of reduced processing until the system returns to normal? What are the consequences of missing data?

Happily, implementing a data-driven system is pretty straightforward because the processing on the data is repetitive. Consider the analog-to-digital system described in Figure 6-2, where digital data comes from the ADC, gets attenuated, and goes out the DAC. The system can usually be divided into a producer of data (the ADC) and a consumer of data (the DAC). These must remain in Zen-like balance. As long as the processor can keep up with the ADC by running the algorithm and sending the data to the DAC, the system can run forever.

Most systems have some elements of both an event-driven system and a data-driven system. As you consider your system, try to figure out what aspects belong to each. This will unravel some of the complexity of your software by allowing you to implement them separately.

Circular buffers

The circular buffer is a key implementation tactic among data-driven systems. Unlike the stacks mentioned in Chapter 5, where the last datum in is the first one out (LIFO), circular buffers are first in, first out (FIFO).

A producer puts data in the circular buffer at some rate. The consumer takes it out of the buffer at the same average rate. However, the consumer can take it out in chunks while the producer puts it in in dribbles. This lets the processor do other things while the data accumulates to the size needed for the algorithm. (Or it could go the other way, with the producer putting a large chunk of data into the circular buffer to be read out at a steady rate.)

A circular buffer needs to keep track of a chunk of memory, its length, where to put the next element that comes in (write or start), and where to get the next element (read or end). See Figure 6-12. Note that understanding a circular buffer is easier when the read pointer is before the write pointer. This is true for everyone; most of the common circular buffer bugs happen when the read and write pointers cross, even though the software will spend half of its existence like that.

When the buffer is empty, the read pointer is equal to the write pointer. This is easy enough to check. However, the problem is that when the buffer is full, the read pointer is also equal to the write pointer.

Figure 6-12. Example of pointers in a circular buffer

A common workaround is to call the buffer full when the write pointer is one away from the read pointer. This is easy to understand and implement, though it wastes an element’s worth of memory. Another possible solution is to add a variable to keep track of the length by having an enqueue function increment the length and a dequeue function decrement it. Now you’ve traded a variable for a position in your circular buffer. Depending on the size of the elements in the buffer, that may be worthwhile.

Usually one user of the circular buffer is an interrupt (either the producer or the consumer) while the other is not. We saw in Chapter 5 that all sorts of interesting things occur when you have interrupt code and noninterrupt code trying to modify the same variable. If you use the blank element to know when your buffer is full, you can isolate the producer’s variables from the consumer’s, making the circular buffer interrupt safe as long as the read and write pointers update atomically. (Otherwise, a request for the length of available data will give an invalid response.) However, since the pointers wrap around when they get to the end of the buffer, an atomic operation may be unfeasible.

Most common implementations in embedded systems constrain the circular buffer length to be a power of two to avoid this issue. So let’s start with an implementation using a structure like this:

struct sCircularBuffer {
  tElement *buf;    // block of memory
  uint16_t size;    // must be a power of two
  uint16_t read;    // holds current read position: 0 to (size-1)
  uint16_t write;   // holds current write position: 0 to (size-1)
};

In the initialization function, we’ll set the buffer pointer to a block of memory (one that holds a power-of-two number of elements). We will also need to set the size to the length of memory and the read and write offsets to zero. From there, we need to implement only a few other functions. The first is probably the most difficult to follow. It does a wrapped subtraction to determine the length of the data available in the circular buffer:

uint16_t CBLengthData(struct sCircularBuffer *cb){

  /*        **********
     |-----|----------|-------|
          write     read
  */

  int32_t length = cb->write - cb->read;
  if (length >= 0) {return length;}
  /*
     bbbbbb           aaaaaaaa
     |-----|----------|-------|
         read       write
  */
  return (cb->size – cb->write) +    /* aaaa */
            (cb->read);              /* bbbbb */
}

Actually, that doesn’t use the constraint that says the buffer size has to be a power of two. A more efficient implementation looks like this:

uint16_t CBLengthData(struct sCircularBuffer *cb){

   return ((cb->write – cb->read) & (cb->size - 1));
}

Even though the read and write variables are unsigned, the length is correct thanks to that bitwise AND. explains why.

Representing Signed Numbers

You know about unsigned binary numbers (take a look at An Introduction to Binary and Hexadecimal for a refresher). How Many Bits in That Number? showed the highest number you could fit in an 8-bit or 16-bit variable, where signed numbers were always half as much as unsigned numbers because the sign takes up a bit. There are several ways to encode the sign in one bit; consider how we do it in decimal with a sign to the left of the value. One way to do it in binary is to have the sign be the leftmost bit (most significant bit) while the other bits encode the number:

snnn nnnn = <1-bit sign> <7-bit number>
0000 0000 = +0
0000 0001 = +1
…
0111 1111 = +127
1000 0000 = -0
1000 0001 = -1
1111 1111 = -127

Note that there is both a 0 and –0. Many encodings have that. It is a little odd and a little inefficient.

However, that sign and value encoding isn’t how your computer normally stores numbers, because there is a way to make processing faster using a different encoding. The ingenious method is called two’s complement. In this representation, the leftmost bit still holds the sign of the number, and a positive number is encoded just as you’d expect. However, a negative is encoded by inverting all the bits and then adding one to the result:

snnn nnnn = <1-bit sign> <7-bit number>
0000 0000 = 0
0000 0001 = +1
…
0111 1111 = +127
1000 0000 = -128
1000 0001 = -127
…
1111 1110 = -2
1111 1111 = -1

I find the “invert and add one” instructions difficult to remember. However, the neat part of two’s complement is that when you add a number and its opposite in sign, the numbers obliterate each other:

  0000 0001 = 1
  1111 1111 = -1
  --------------
1 0000 0000 = 0 (and a carry bit)

Being able to always add numbers (and not perform a different operation for subtraction) makes this number encoding very powerful.

Remember that –1 is equivalent to the highest unsigned value. If you write code:

uint8_t value = -1; // set to highest value this unsigned type can hold

your value will be 255 (0xFF). To figure out other unsigned numbers, subtract normally from –1:

-1 = 0xFF = 1111 1111
-2 = 0xFE = 1111 1110
-3 = 0xFD = 1111 1101
-4 = 0xFC = 1111 1100
…

Let’s see how this encoding works on the modulo math from this chapter. For example, here are the steps to do the wrapped subtraction of cb->write (5), cb->read (6), and cb->size (8):

  (cb->write – cb->read) & (cb->size - 1)
= (    5     –     6   ) & (    8    - 1)
= (          -1        ) & (      7     )
= (      1111 1111     ) & (  0000 0111 )
=  0000 0111 = 7

The power-of-two length makes the bitwise AND eliminate the sign information, leaving us with the correct result.

An empty buffer would return a length of zero. A full buffer would have a length of (cb->size-1). Keeping that in mind, writing the enqueue function is pretty simple:

enum eError CBWrite(struct sCircularBuffer *cb, tElement data){

  if (CBLengthData(cb) == (cb->size-1)) { return eErrorBufferFull;}
  cb->buf[cb->write] = data;
  cb->write = (cb->write + 1) & (cb->size - 1); // must be atomic
}

The modification of the write variable in the last line of code needs to be a single instruction. But setting only the write variable needs to be atomic; the preparation ((write+1) and (size-1)) can be multiple instructions. These variables are declared as uint16_t, so if this code runs on a 16-bit processor, the setting of the write variable will be atomic (but for an 8-bit processor, it won’t be; in a 32-bit processor, usually the uint16_t write instruction is a single operation, but not always).

Also note that the circular buffer rejects data that would overflow the buffer. This goes back to the question of how you want your system to fail. Do you want to reject new data, as this code does? Or do you want to drop some old data to make room for the new data? What is your strategy for falling behind? I’ll stick with the error for now, but you do have options to consider.

Taking things out of the buffer is similar, but deals with the read offset instead of the write offset:

enum eError CBRead(struct sCircularBuffer *cb, tElement *data){

  if (CBLengthData(cb) == 0) { return eErrorBufferEmpty;}
  *data = cb->buf[cb->read];
  cb->read = (cb->read + 1) & ( cb->size - 1);
}

Warning

If you don’t constrain the buffer size to a power of two, you need to use modulo arithmetic to wrap the pointers. Modulo arithmetic (like division) is a relatively slow operation, and not the sort of thing you want to do in an interrupt. Chapter 9 talks more about what forms of math are fast and slow on an embedded processor.

Note that the data element is copied out of the buffer to another location. In general, copying memory is not a good use of your embedded system. The act of copying uses up limited processing cycles, and having two copies of the same information uses up limited memory. Why don’t you just leave the data where it is?

You can add another pointer to your circular buffer to indicate when data is free for use by the write pointer. However, this is where buffers start to get complicated. This is also where you might want to pull out a paper and pencil or get to a whiteboard. For many circular buffer problems, the solution is far easier to see when you can draw out the options (see Figure 6-13).

Figure 6-13. Circular buffer with multiple pointers

Once you get it straight in your head, the code will be pretty simple. First, add a free variable to the structure and initialize it to zero. Next, you’ll probably need to know the lengths of the different sections of data:

How much is free and ready to be written

Wrapped subtract of write from free

How much data can be read

Wrapped subtract of read from write

How much data is currently checked out for reading

Wrapped subtract of free from read

Handling the free pointer is straightforward. When the code is done with the element it took, simply increment it past the element so the write pointer can use it again:

enum eError CBFree(struct sCircularBuffer *cb){

  if (CBLengthReadData(cb) == 0) { return eErrorBufferEmpty;}
  cb->free = (cb->free + 1) & (cb->size-1);
}

Note that the element wasn’t passed in to this function, because you always free elements in order. Also, this looks an awful lot like both the CBRead and CBWrite functions; they all follow the same principles.

You may want to do more than read parts of the buffer. Many data processing steps lend themselves to in-place modification. Once you get the hang of handling multiple pointers into the circular buffer, you may want to keep using them to avoid copies. Such streamlining will make your system faster.

Figure 6-14 shows the example from the analog sensors section. The analog data is sampled by the ADC. The digital data is written to a circular buffer at a constant rate. The data may be obtained from the buffer at a different rate because the processing is done in chunks (you do something to several samples at once) or because the processor does something else while the data queues up. The processing module reads and modifies the data. It could put the data in another circular buffer for output to the DAC or return it to the same circular buffer (or modify it in place). Once the DAC outputs the processed data, the circular buffer element is free to be used again.

Figure 6-14. ADC to DAC data-driven system with multiple pointers into a single circular buffer

To implement a system like this, you’d need to add one more pointer to the system, called processed. As the pointers move independently of one another, adding another is fairly straightforward. The code is nearly the same as we added for CBRead or CBWrite. Don’t forget to add a new length function, too.

Hardware FIFOs

You don’t always have to implement a circular buffer. Sometimes the hardware will do it for you with a FIFO buffer, especially with USARTs.

Using a FIFO is pretty simple. To send data, write to the transmit holding register (THR). Writing to this address sends data to the transmit FIFO. The data will get sent when it reaches the end of the FIFO queue and the transmitter is available. Unlike the circular buffer, there is no dealing with pointers; you write to the THR to fill the FIFO, and the data eventually reaches the pins. While the peripheral is streaming out the data stored in its FIFO, the processor can do other things.

These systems usually have both transmit and receive FIFOs. To get data from the receive FIFO, check to make sure something is available (using a status register), and then read the receive buffer register (RBR) to get the oldest data in the FIFO.

Note

The send and receive registers (THR and RBR) can be the same register, changing roles depending on whether you write to it or read from it.

Processor FIFOs use a set of flags to signal the software. The status flags generally include full, empty, half full, and half empty. (If you are thinking that the FIFO has optimist/pessimist issues, usually a receive buffer signals half full and a transmit buffer signals half empty.) These levels can be used to interrupt the processor.

Some processors will let you set specific levels at which to interrupt. For maximum throughput, determine how long it will take your processor to get to the interrupt (its latency) and how much data can be sent in that amount of time. For example, suppose you want to keep a steady stream of data flowing on the SPI bus. If you are running SPI at 10 MHz and it takes you a maximum of 3 microseconds to respond to an interrupt, you need to get an interrupt when the FIFO has 4 bytes remaining (10 MHz clock→1.25 MB/s→0.8 μs/byte, 3 microsecond latency / 0.8 μs/byte→3.75 bytes).

A transmit interrupt usually fills the FIFO with available data. A receive FIFO interrupt drains all of the data, putting it where the software can use it (often in a circular buffer). The goal is to balance the trigger levels so that you maintain a constant stream of data with the fewest interrupts.

Your processor manual will describe how to configure your FIFOs in more detail. FIFOs are usually 8 or 16 bytes deep. But what if your FIFO depth wasn’t so limited? What if your FIFO was more like your circular buffer, using as much RAM as you need?

Direct memory access

A processor that supports direct memory access (DMA) can transfer far more data than one that implements only a FIFO. To use DMA, you give the processor a pointer and a number of bytes to read (or write). When receiving, the processor reads data from the peripheral into the specified memory until it reaches the byte count, at which point it interrupts the software. Similarly, when transmitting, the processor moves data from the buffer to the peripheral, interrupting when the count is complete.

DMA is a lot like having another thread do the data handling for you. The configuration is relatively straightforward. Look in your manual for the DMA registers: transmit pointer register (the buffer to write to on the device), transmit count, receive pointer register, and receive count. If your data comes in chunks instead of a constant stream, you can use a DMA controller with one channel. If your data is a constant stream, you may need to copy the data from the DMA controller to another location so your software has time to work with it.

To avoid the copy (which, as you recall, is an inefficient use of your processing cycles and RAM resources), many DMA implementations can alternate between two buffers. This allows the software to use one buffer while the processor transfers peripheral data into (or out of) the other. This two-buffer back-and-forth movement is called a ping-pong buffer.

DMA is used with peripherals that have very high throughput, such as disk drive controllers and network cards. It is also used in multicore processors to communicate between cores. If you know how many bytes you’ll be receiving (or you are transmitting a block of data and not just a dribble), DMA is a great way to reduce processor overhead, making your processor seem faster than it is.

Comparison of buffering schemes

How much does good data handling really matter? Well, let’s compare how much processor time would be needed to implement a SPI interface running at 1 MHz with 8-bit (byte) transfers.

If your processor doesn’t support a SPI peripheral, you can implement a bus master via bit-banging. Set up a timer to interrupt at twice the clock speed (2 MHz). On every other interrupt, put the clock line down and set the outgoing data line (MOSI) value of the next bit in the transmit data byte (and shift the data byte). On the alternate interrupts, toggle the clock line up and read data on the incoming data line (MISO), setting the next bit in the receive data byte, shifting it to the next place. Every eight bits, you’ll need to start a new byte. Overall, you don’t have to do all that much for each interrupt, but you have a lot of interrupts, which means tons of context switching.

The next-fastest implementation would be a hardware-supported interface. When your processor datasheet says something like:

One Master/Slave Serial Peripheral Interface (SPI) – 8- to 16-bit Programmable Data Length, Four External Peripheral Chip Selects

then you have built-in SPI. You can configure it to run at 1 MHz (sometimes not precisely, as this will depend on your processor clock and how you can use dividers, but usually you can get close enough). Then you’ll need to configure it to interrupt when a byte has been exchanged, every 125 kHz (1 MHz/8). In that interrupt, you’ll need to get the byte from the receive register and put it somewhere (e.g., a circular buffer). You’ll also need to put the next byte in the transmit register. Not only is the interrupt shorter than the previous bit-bang solution, but there are also many fewer of them.

Of course, if your FIFO is 16 bytes deep, you only have to interrupt at 7.8 kHz (125 kHz/16). Well, you could if you interrupt when the FIFO is completely full. But that means your communication will fall behind if your latency is greater than 128 us (1/7.8 kHz). To give you more latitude, interrupt when it is half full. That will still give you an interrupt rate of only 15.6 kHz. At every interrupt, you need to move the bytes in the receive FIFO into another piece of RAM and move the transmit bytes into that FIFO.

Now for DMA. If your goal is to maximize throughput, divide your RAM into a small piece for local variables and a large piece for a ping-pong buffer. If you have 32 KB of RAM, you could save 2 KB for variables and have ping-pong buffers that are 15 KB each. That means you’ll get an interrupt eight times per second, leaving you with plenty of time to actually do the processing.

Table 6-2 compares the processing difference of the interrupt overhead alone (assuming each interrupt has only 10 cycles of overhead, a fairly small amount). Instead of making a massive DMA buffer, I gave it a reasonable 512 bytes to work with. Note that to implement a bit-bang solution, you would need a processor that is more than 8,000 times faster than a comparable DMA solution. That’s just for the overhead! A bit-bang driver is more complex than the DMA interrupt, which just needs to switch pointers and set a flag for the processing to occur on the already full buffer.

Table 6-2. How much faster is DMA?

Buffering methodology | Bit-bang interface | Hardware peripheral interface | 16-byte FIFO (interrupt on half) | DMA with 512-byte buffer
Clocks per second taken up with interrupt context switching (10-cycle interrupt overhead) | 20 MHz | 1.25 MHz | 156 kHz | 2.44 kHz
Times faster than the bit-bang interface | 1 | 16 | 128 | 8,197

Adding Robustness to the Communication

No matter your throughput, you need to know that the data you received is the data that was transmitted. If you don’t control both sides of the communication pathway, this can be tricky. Your SPI ADC might not have any facilities for ensuring data integrity. On the other hand, there is plenty that you can do when you have some control.

Version everything and then checksum it

In Version Your Code, I recommended that you assign versions to your code and also make it easy to get to version information from outside your system. Don’t stop with versions there. Even though your product may be a self-contained widget on the outside, it probably has several components inside that can change independently. It is critical to version and checksum pieces of the system that can be attached and detached. It is less important to do it for components that are on the same board as the processor (though still pretty important if they get programmed separately). A version byte (or two) tells you when two parts of your system are incompatible. A checksum will tell you when components have been tampered with or have experienced hardware failures.

So put a version in your flash memory so you know what assets are there. If you don’t have enough code space to be backward-compatible, at least you can give an appropriate error instead of putting random designs on the screen until the software crashes. The version can also act as a key to verify that external memories are programmed.

Running a checksum over a large flash is often prohibitively slow. However, if you checksum each asset as you obtain it from the flash, you can verify that the asset went into memory correctly and that the communication pathway didn’t lose or mangle any bytes.

In the same spirit, add a version to your EEPROM in case the layout (or size) changes in the future. Right now, a larger EEPROM might be too expensive or unnecessary, but things change. Give yourself the means to be flexible. Even if your EEPROM is small, adding another byte as a checksum will pay dividends when cosmic rays damage a chip.

Warning

Although cosmic rays jokingly get blamed for many irreproducible errors in hardware and software, they really do happen. And they really do cause errors, such as corrupted data in memory devices. Radiation hardening your system creates more work for each engineering discipline: mechanical (shielding), electrical (choosing components), and software (redundant pathways and error checking).

If you have control over both sides of a communication protocol (for example, if your company makes both an embedded system and a host for it to talk to), adding a version to the communication protocol will allow the pieces to grow independently.

I suspect you get the idea by now about versions and checksums, but there is one more to discuss: version your hardware. How? Take three of your I/O lines and send them to a set of pull-down resistors. As you configure the lines, make them inputs with weak pull-ups (see Chapter 3 if you don’t remember how). Next, read the value to get a version. This lets your hardware describe itself to the software, possibly letting you reconfigure for different I/O configurations (as described in Chapter 4), as long as you don’t change the location of the hardware version information. Finally, if you are concerned about power consumption, you may want to change the pull-ups on the input lines to match the values seen on the hardware. No need to suck even that much power.

Types of Checksums

The simplest form of checksum is just to sum all of the bytes in question, ignoring any overflow. If your buffer is {10, 20, 40, 60, 80, 90}, your checksum should be 300. If you are using only an 8-bit checksum, that becomes 44 (300 mod 256). There are many other combinations that could sum to the same value (e.g., {44} or {11, 11, 11, 11, 0, 0}), but those aren’t likely to happen by chance (statistics says there is a 1 in 256 possibility if all of the bytes are random). Even this paltry 8-bit checksum is likely to save you if a byte or two gets corrupted, as long as two bytes don’t get corrupted in ways that cancel.

If you have lots of data, 1 in 256 aren’t very good odds. You could sum everything as 16-bit words, which gives you a much lower probability of good checksums with bad data. Note that you need to sum the data as 16-bit words, not just keep the overflow from an 8-bit sum.

The types of errors you get depend on the memory or communication pathway. In an EEPROM, it tends to be a sticky bit that doesn’t change upon command. A simple checksum will always catch a single-byte error, but it cannot detect when two bytes in the stream are swapped. And errors can cancel each other out in the checksum if multiple bytes are modified. In many communication methods, the errors tend to be bursty, so that many bytes will get corrupted at once.

So when people say “checksum” they often mean “CRC value.” CRC stands for cyclic redundancy check, which you don’t need to remember. CRCs give you more protection than a checksum against multiple-byte and swapped-byte errors. However, CRCs require more computational power than the simple sum. There are many versions available online. I like the one published by Netrino because the site walks through several code examples, showing you how it works (and how to make it faster).

Remember, the main goal of a checksum is to detect that an error has occurred. A more complex scheme might be able to tell you where the error occurred or even correct small errors.

Authentication and encryption

Checksums and CRCs are not preventions against hacking. That is, they don’t protect against intentional modification of data. There are two main forms of protection.

Encryption ensures that no one can read (or modify) your data without having the correct keys and method of decryption.

Tip

Some processors come with cryptographic hardware acceleration. Check your processor manual before implementing it in software.

Authentication lets you know that your software is talking to something you know. Printer manufacturers want to make sure the printer cartridges come from a known source (partly because the wrong ink could ruin a printer and partly because consumables are profitable). The authentication may be an encrypted signature, so the two are often related.

Tip

When a consumable is cloned, the authentication information is copied to make a hacked consumable look good to your software. A common precaution against this is to use a database to indicate that your system has seen the same serial number more frequently than it should expect.

Authentication and encryption are difficult. For the sake of comparison, the next chapter about updating code is hard to understand, but once you get it, you’ll be able to implement it successfully. But authentication and encryption are things you can never do successfully. Sure, you might implement the algorithm correctly, but that doesn’t mean the data is safe.

The more valuable your consumable or data is, the more likely someone will take the time to reverse engineer it. A determined hacker will eventually succeed, though sometimes only by a brute-force attack (it is probably easier to get into the manufacturing building and access the source code than figure out a 128-bit AES key).

You can’t be in charge of locking the door; the best you can hope for is that your code is robust enough to fend off the casual attacker and create enough headaches that they leave your product alone. You probably don’t have a lot of processing power for your system. Trying to implement AES or some other well-understood encryption when you are running an 8-bit processor at a few MHz may lead you to throw up your hands and put out a welcome mat for the hackers.

However, not all encryption methods have symmetric processing burdens. Some algorithms make it easier to decrypt than encrypt (or the other way). Let the host do the hard math. Alternatively, run encryption only on the critical data, leaving the rest in clear text (or with a reduced encryption level). It may make your code more complex, but if the choice is between good encryption for the important part or mediocre encryption for all, I know which I’d take.

Even if you have to use an encryption algorithm that isn’t good enough to stand up to the NSA (what is?), use a well-understood algorithm. For all that you may have a tricky mind, your 8-bit encryption solution will not be better than a well-understood algorithm. Hiding things through obscurity instead of security doesn’t protect against someone who wants your information. Use one of the most common algorithms: RSA, DES, or AES. You may have to use a smaller key for an embedded system, but then you can estimate the time it takes to crack the key and give your management a reasonable effort level, something you may not be able to do if you design your own algorithm.

As you design your system, put on a black hat. What would you do to attack the system? Assume the hackers know the algorithm but not the keys.[7] Chapter 7 describes ways to protect your code from being read via JTAG. Given that, is the easiest way to obtain the keys the relatively difficult matter of putting the chip under an electron microscope to read out the code? Or can intruders read it from your email because another developer sent it to you in clear text, or log in to your source code repository with the guest account? Your authentication and encryption algorithms are only as good as the weakest link.

Mild paranoia aside, protecting your system is hard. Your management team will need to determine how much time (and money) they are willing to spend on developing protocols to keep your data (or consumables) safe. It is a business decision as well as a technical decision. It is also a long-term decision; staying ahead of the hackers is an ongoing concern, not a one-time algorithm choice.

Changing Data

In the LCD section, I described a way to store character and image glyphs, retrieving them as needed to put on the screen. There is a design pattern that matches that description: the flyweight pattern. The way I described it mashes it together with a factory pattern, so it is more correctly called a flyweight factory pattern. Let me untangle the two so you can see them separately and be able to use the concepts outside the LCD.

The typical example of the flyweight pattern is a document with many characters. Each character is an object describing the character’s size and encapsulating a bitmap. In a nonembedded world, there is some temptation to instantiate each character object on its own. Eventually, when there are enough characters, this leads to sluggish behavior. In an embedded device, we wouldn’t have that problem, as there is never enough RAM to let us get into trouble this way. The solution is to point to a single instance of the character object (which for us is in flash memory).

That is the pattern: when you have lots of things used repeatedly, create a pool of instances (one for each object) and point to the pool instead of instantiating each directly. Each of the shared objects is called a flyweight (like the lightest weight in boxing, not like the weight used to increase inertia in flywheels). Each flyweight must be relatively interchangeable with the others, though they may describe their state (e.g., a character may describe its width because that differs unpredictably between characters).

The factory pattern is a bit more complicated, centering on the factory method. Remember the Unix driver model from Chapter 2? Almost all drivers implement the same interface (open, close, read, write, ioctl). If you are writing a program to use a driver, you open it as though it were a file (/dev/tty0). The driver gets instantiated and returned to you as a generic handler on the surface, but underneath it represents a serial port. The factory method (open) knows how to create a specific widget (subclass) that is of a generic type (class). All of the specific widgets implement the same interface (the class interface).

If you found the previous paragraph a bit daunting, Figure 6-15 may help. It shows the classic factory pattern diagram, with the Unix driver model example underneath, followed by the LCD graphics example. The factory pattern often is implemented in a very object-oriented way, with specific subclasses being instantiations of a class. The creator class (the code that calls the factory method) controls the whole thing.

Figure 6-15. Factory pattern and flyweight factory pattern examples

Tip

The diagram is in UML, a useful visual language for describing software, particularly object-oriented software. It can be simple (like this) or far more complex, going so far as to become an interpretable language.

The factory pattern is all around you, decoupling instances from a generic ideal. It is a fairly physical pattern, so its name makes sense. Imagine you have a real live factory. It is flexible enough to make any one of several relatively similar products (the product class). The factory contains a machine (the creator). Today, you configure the machine to make sprockets (concrete product) by putting a template (factory method) into the machine. When you push the “on” button, the factory will generate sprockets. Tomorrow, you may switch out the template to make gadgets (a different concrete product) ensured by a different template (factory method).

When we do this in graphics software and with flyweights, pressing the “on” button is more like the software requesting a glyph. With a flyweight factory pattern, the factory part manages the flyweights, making sure they are shared properly and decoupling characters from the way they are displayed on the screen.

Changing Algorithms

Sometimes it isn’t the data that needs to be selected, depending on the situation. Instead, sometimes the path of your code needs to change based on the environment.

If you are thinking that we saw a way to change what the code is doing based on commands from the host in the command pattern in Chapter 3, you are correct. The command pattern is used to make short-term command/response changes. The strategy pattern is used to make longer-term changes, usually to data processing. According to the official definition from Design Patterns, Elements of Reusable Object-Oriented Software, the strategy pattern is used to “define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.”

Let’s go back to the data-driven system in Figure 6-2, where analog data is digitized by an ADC, attenuated by the processor, and then sent back to analog via the DAC. What if you weren’t sure you wanted to attenuate the signal? What if you wanted to invert it? Or amplify the signal? Or add another signal to it?

You could use a state machine, but it would be a little clunky. Once on every processing pass, the code would have to check which algorithm to use based on the state variable.

Another way to implement this is to have a pointer to a function (or object) that processes data. You’ll need to define the function interface so every data-processing function takes the same arguments and returns the same data type. But if you can do that, you can change the pointer on command, thereby changing how your whole system works (or possibly how a small part of your system modifies the data).

Tip

Some embedded systems are too constrained to be able to change the algorithm on the fly. However, you probably still want to use the strategy pattern concepts to switch algorithms during development. The strategy pattern helps you separate data from code and enforces a relatively strict interface to different algorithms.

A related pattern is the template pattern. A template provides a skeleton of an algorithm but allows the steps to change (without changing the algorithm’s structure). Usually these aren’t function pointers; the steps are part of the organization of the algorithm. In our data-driven system, we could make a template that looked like the following:

class Template {
private:
  struct sCircularBuffer *cb;

public:
  enum eErrorCode sample();
  enum eErrorCode processData();
  enum eErrorCode output();
}

Even though each of these functions has the same prototype, they aren’t interchangeable, unlike the strategy pattern. Instead, they provide an outline of how the system works. An instance of this template for the system described would have the ADC sampling the data, the data being amplified, and the DAC outputting the result. The instantiation of a template may override certain (or all) parts of the default implementation. For instance, if the default implementation sets up the order “ADC-process-DAC,” a test version of the class may override the sample function to read data from a file while leaving the other two functions in place.

You can, of course, combine patterns. Here is a strategy pattern inside the template’s skeleton:

class Template {
private:
  struct sCircularBuffer *cb;
public:
  enum eErrorCode sample();
  enum eErrorCode (*processData)();
  enum eErrorCode output();
}

Object-oriented software has the concept of inheritance, where an instance is-a concrete version of an object. Template patterns are like that (the template is a series of these steps). An alternative approach is composition, where the software has-a concrete version of an object. Strategy patterns are more like that (they offer a function to call). Composition is more flexible than inheritance because it is easier to switch what things have than what they are. On the other hand, building (composing) a system at runtime might not be a good use of limited resources. Balance the trade-offs for your system.

[7] In cryptography, this is called Kerckhoffs’s principle: a cryptosystem should be secure even if everything about the system, except the key, is public knowledge.

Further Reading

This chapter covered many things, so I have lots of places for you to look if you want to dig deeper into a particular area. As mentioned elsewhere in this book, these two references are great for understanding design patterns:

Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley. The canonical text.

Freeman, Eric T., Elisabeth Robson, Bert Bates, and Kathy Sierra. 2004. Head First Design Patterns. Cambridge, MA: O’Reilly.

On the topics of encryption and authentication, there are many resources. For a relatively fluffy introduction, see Leo Marks’s history of cryptography in WWII (it’s almost a spy novel), Between Silk and Cyanide: A Codemaker’s War, 1941–1945 (Free Press).

For a more comprehensive look at security and some of the chips used to keep information secure, look at The Smart Card Handbook, Fourth Edition, by Wolfgang Rankl and Wolfgang Effing (Wiley). This is a weighty tome, but it is surprisingly easy to read, even in the security chapter.

For an example implementation of FIFO versus DMA, look for James Lynch’s write-up on Atmel’s AT91SAM7 serial communications. If you are using the AT91SAM7, look for his excellent tutorial on setting up open source tools.

When coming across any new serial interface, I tend to hit Wikipedia first. There are so many variants of different protocols, and the datasheet for a device may be filled with acronyms and complex timing diagrams. Wikipedia gives me an overview that puts it all in perspective.

For motor control and other control schemes, I like The Art of Control Engineering by Ken Dutton, Steve Thompson, and Bill Barraclough (Addison-Wesley). It is a little more math-filled than it needs to be, but still pretty useful for actual implementation.

Signal processing and motor control have a lot in common in their underlying math (Laplace and Fourier transforms). If you haven’t had those in school (or cannot dredge up the memories), you might consider taking a class. It isn’t the sort of thing to learn from a book. But if you are going to anyway, the seminal textbook is Signals and Systems by Alan V. Oppenheim, et al. (Prentice-Hall).

Or you can skip to practical applications with a really fantastic book, The Scientist and Engineer’s Guide to Digital Signal Processing by Steve Smith (California Technical Publishing). It is also available online for free.

Interview Question: Choosing a Processor

How would you go about choosing a processor for our next-generation platform?

This question is multifaceted, starting out with a basic check of the interviewee’s listening skills. Does he know what we build? Is he interested enough in the company to spend some time doing research? Has he been awake while I blathered on about the current system?

From there, it moves into checking his design skills and experience level. I want the candidate to know which questions to ask (what are the bottlenecks on the current platform, and how have we identified them?). He should have specific questions about the types of processing and the bandwidth requirements of the system to help him understand the problem better.

Most companies don’t want to go backward in their feature set, so it is relatively safe to start with the assumption of more processing power, more RAM, and more code space than the previous design. However, I want the candidate to check these assumptions. Ideally, as we talk about these, the candidate should describe ways to unburden the processor (peripherals often take up an unreasonable amount of processor overhead).

A good candidate will also consider the underlying goals of a new platform (i.e., why are we looking for a new platform?). A candidate with a product view is often a better fit than someone whose scope is more limited. To that end, I hope he asks about the goals for volume and cost. An excellent candidate will also ask whether the requirements are expected to change significantly over the development cycle, helping him determine whether he wants a general-purpose processor or can choose something very specific.

I hope the candidate discusses the advantages of using a popular part (one that is widely supported with compilers, debuggers, tool chains, and even RTOSs). It is also good to hear him consider lead times and end of life expectations (hint: don’t start a new platform on a dying chip). Good candidates also mention the goal of having a new processor be in the middle of a family of processors, allowing for future platforms (and possible cost reductions using less fully featured parts).

Processor selection is difficult because many factors enter into a decision. I don’t expect someone to come in for a day’s worth of interviews and produce a suggestion for a new processor. Most people don’t keep the processor lines from the various microprocessor vendors in their head. If he immediately suggests something concrete, I take it as an indication of what he’s been working on. At that point, I tend to ask why he thinks that is a good path for our particular product. The goal is to get him to tell me how he would choose, not the actual choice. In interviewing, the methods people use to confront problems are far more interesting than the solutions they generate.

Chapter 7. Updating Code

When your system has problems after it goes out into the wild, you’ll need to be able to update the code on a device in the field. To understand the complexities, let’s start by looking at how uploading works in the comfort of your development lab, and how that differs from uploading to a device you’ve released.

During development, your system lets you peer into the guts of the code, maybe with a JTAG that can stop the processor. As part of the process, you also get to modify the code (aka flashing the chip). The method for doing that is chip-dependent, but it usually requires some special debugging widget. If you want to update the code after the system leaves your desk, you’ll need to plan ahead. Although we’d like to arm the world with development hardware, it isn’t cost effective (sadly).

The lowest level of updating code is very chip-dependent, but the general procedure follows certain common patterns.

Tip

There are many different names for updating code on a device: bootstrap loading, bootloading (even when it isn’t related to booting), updating, uploading, burning the code, and over-the-air programming. Whatever you call it, uploading code is one of the most difficult topics in embedded systems. It is right up there with storing code on cartridges, keeping consumables secure, and faking floating-point math via binary scaling. This chapter may be tough the first time you read it. Don’t lose hope.

Three different loaders are described in this chapter in increasing order of complexity. Some issues you should know about before we start include:

The code storage mechanism and communication method

We’ll need something that currently has the new code image. The communication method used to transfer this new code depends on what you use to store the data, for example: a thumbdrive when updating over USB, an EEPROM for SPI, or a hard drive for a network.

Code space memory (old code location)

This is often called ROM (read-only memory). However, since we are about to change its contents, it isn’t read-only. Instead, the code space is some sort of nonvolatile memory (memory that is not erased when the power is lost) that often requires special voodoo to rewrite. Once, this really was read-only memory, and firmware updates simply weren’t possible at all. Now this type of memory is usually flash and able to be rewritten so that runtime code can be updated in the field (as well as on your desk or in manufacturing). Whether it is flash or some other form of nonvolatile memory, writing to it is more complicated than writing bits to RAM, so it is often called programming the memory. The particulars of how you program the nonvolatile memory depend on your processor.

Scratch space RAM

Ideally, this RAM is the same size (or larger) as your runtime image. It should be local to the system. Although not strictly necessary, the scratch space prevents serious complications if communication with the new code is lost before programming is complete. The idea is to write the code to the scratch space and make sure the upload is complete without corruption before actually copying it over to where it counts—the permanent memory of the device.

Run space RAM

This is RAM from which the processor can execute. If you don’t have this, it limits your loading options.

Onboard Bootloader

Some processors have an internal boot sequence that will load code from an external source if the right I/O pins are set. In an ideal world, you’d just set the I/O pin while you connected the new code to the system, generally as the system powers up. The bootloader would automatically load the data into the code space.

Warning

Some onboard bootloaders will load the code only to RAM, not to nonvolatile code space. That means you’ll have to do some element of building your own updater, as described in the next sections.

Part a) of Figure 7-1 shows the new code connected to the processor using one of the communication methods mentioned earlier. The bootloader code inside the chip reads in the data and writes it to the code space.

Figure 7-1. Three architectures for loading code

If that figure describes your system, excellent. If you can make that be your system, do so. Everything else is a lot more complicated when it comes to uploading in the field.

Build Your Own Updater

If you have plenty of code space, the next easiest option is to build your own updater: a resident program in the code space that could reprogram the rest of the memory. Since most code space is flash memory, you will have to erase sectors of the old runtime code before you can write the new code.

The naive updater would erase a sector, read the new code into some scratch memory, and use chip-specific functions to write the code to that sector, repeating until all runtime sectors are complete. What could possibly go wrong?

First, the new code could come corrupted from the source. Second, the communication method could drop part of the message. Third, the memory holding the new code could be removed. A checksum could detect these problems, but that won’t help if the updater has already written the data to the code space before it can detect the error. Programming a bad image will lead to a nonfunctional unit at best. The scratch space RAM is optional, but it will help you avoid those pitfalls. See part b) of Figure 7-1 for a high-level view of this system.

Your updater is a resident of your code space. This gives you the opportunity to recover from failures. Even if there isn’t enough RAM to validate the whole image, so that you have to take the risk of installing corrupted image on the device, it will be OK as long as you allow part of the updater to run during the power-on sequence. The updater can verify that the runtime code image is valid by running the checksum that was uploaded with it. If the code isn’t valid, the updater can wait as long as it takes for you to connect new code. Yes, the device is nonfunctional, but that’s probably better than having it display garbage or short out some part of a factory system. This solution does mean that your updater has to be the first code to run at boot time, as shown in Figure 7-2.

Figure 7-2. Build your own updater flowchart

Since the updater will be erasing the rest of the code, it must contain everything it needs to run (and be unable to erase itself). Instead of just a function to be called, it is a mini-program embodying code to communicate with the new code storage mechanism, processor-specific functions to write to the code space, and any debugging code you need to make it all work.

If the code space can be erased only in sectors, the updater must take up a whole sector (or a whole number of sectors). If you are a little short on code space, you might consider putting other things in those sectors with the updater (for instance, fixed lookup tables). These sectors become untouchable, never to be updated, so you have to hope any problems that arise don’t reside there. Test your updater thoroughly.

Modifying the Resident Updater

If you really need to, you can modify the updater using a two-pass process that resembles a shell game. See Table 7-1 to identify where the updater and runtime codes go. The asterisk (*) indicates which code is running at each stage.

Table 7-1. Procedure for modifying the resident updater

Stage | Storage mechanism | Updater area of memory | Runtime code area of memory
1 | Up2 | Old updater* | Old runtime
2 | (Nothing) | Old updater | Up2*
3 | New updater | (Erased) | Up2*
4 | (Nothing) | New updater | Up2*
5 | New runtime | New updater* | (Erased)
6 | (Nothing) | New updater | New runtime*

In your new code storage mechanism, put an image that contains code similar to the updater (we’ll call it Up2). The old resident updater will load this and then run it. Up2 erases the old resident updater and writes a new resident updater. Once the new resident updater runs (this might require a reset), it can load a new runtime image. See the flow of control in Figure 7-3, and note that the stage numbers from Table 7-1 are also in the figure.

Figure 7-3. Modifying the resident updater

If you can leave the updater resident in the code space, building your own can be a good solution, one that is workable for many systems. However, this method doesn’t work if you don’t have enough code space to devote to the updater, if the updater functionality changes regularly, or if the code space doesn’t allow sector erases (a security feature).

Brick Loader

The next loader solves all of the problems mentioned at the end of the previous section but is riskier and more complicated. The danger comes from the possibility of creating a useless system that is unable to ever load valid code (aka “turning the system into a brick”).

There is a period between erasing the flash and having the new code fully loaded on the system. If the power is lost during this time, the system is not recoverable in the field. Depending on the processor, the system might never be recoverable. One of the goals of good loader design is to make this period as short as possible.

Let’s get back to our processor and its new code. We’ve determined that the loader can’t run from the normal code space, because we are going to erase that and put in new code. That leaves the processor RAM as the only place left to run the code from (though some architectures disallow this, so not all processors can have this type of loader). See part c) of Figure 7-1.

Tip

Loading code isn’t the only time you may want to run from RAM. Sometimes it is faster! See Memory timing for more information.

In Build Your Own Updater, the updater was separate from the rest of the program because it had to contain everything it needed to operate while the runtime code was erased. For the loader in this section, you’ll need a completely separate image of the loader code. You will also need to edit the linker script to make the loader run from RAM. Linker scripts are not for the faint of heart, but they are reasonably standard across platforms. Once you learn the basics of one linker script language, others will be easier to use. And you don’t need to learn all the features, just enough to modify an address and segment length here and there.

To get the new runtime image running using our loader, we’ll need to implement five steps:

With the runtime code, copy the loader from the storage to RAM.

Run the loader code (in RAM).

Copy the new runtime code from storage to a scratch area.

Erase the old runtime code and program the new.

Reset the processor to run the new code.

Whereas the methods in the previous sections are part of the system and can be part of boot-time checking, the loader in this section is often run in response to a command from a debug terminal or a host processor.

Copy Loader to RAM

The process just described sounds pretty simple, but a few details make it difficult. First, you can’t just copy the loader into the RAM that your runtime program is using. If you put the loader into the same RAM used by the runtime (including interrupts), your program will exhibit random behavior and crash. You’ll need to allocate some RAM specifically for the loader code—enough for the whole loader program—and make sure the runtime program doesn’t use that RAM for the stack or heap.

Next, you have the problem that your loader program is built to run at a certain address (its base address), calling functions at certain other addresses. The RAM you allocate has to be at that particular base address, which is not something that you can do in standard C or C++. Compilers for embedded systems will give you a way to do this, sometimes using the @ symbol, using a #pragma, or through the use of linker variables. If the solution involves linker variables, you will need to modify your runtime linker script to set the address of the RAM buffer and create a gap the size of your loader program.

This is pretty compiler- and processor-specific stuff. Linker Scripts shows some sample linker code. Yours won’t look exactly like that, but it will give you a place to start.

Warning

Loading the RAM-based executable at runtime takes a bit of arrangement. Before getting all gung ho about writing the loader, start small and create a program that blinks an LED (or something similarly hello-world-esque). Get it running from RAM, ideally via a debugger (though some debuggers do not support this well, which is one of the reasons I suggest you start small, in easily debugged chunks). Then get your runtime code to copy the blinky test program from the new code storage mechanism to RAM and run it.

If your loader program is greater than the size of your available RAM, start by divesting the loader program of all unnecessary functionality. If it still doesn’t fit, don’t lose hope yet. Is there a large buffer or set of medium-sized buffers in your system? (For instance, you may be able to turn up some display or data acquisition buffers.) If the code is about to have its mind wiped, these buffers might not need to stay valid until the brain transplant is complete. In fact, if your program stops executing all interesting functions, you might be able to free up enough space for the loader. Make a list of the buffers that aren’t needed during the firmware update. If the loader code can fit in the sum of those buffers, you can make this work.

In the linker script, put your large buffer at the top of RAM (the lowest address). If the loader program area contains multiple buffers, you’ll need to allocate them and specify their sizes. At the end, you’ll also need to figure out exactly how big your loader can be.

For example, let’s say our system has flash memory at 0x00000 where the code runs. And we have RAM at 0x010000, which is where we’ll want the loader to go. This system communicates over RS-232 to get new code from a host processor.

Finally, the system has a buffer for data processing and a circular buffer for communication with a peripheral on the SPI bus. At the end of updating the code, the system will reset and lose the sensor data anyway. So during the loading process, we can halt the subsystem(s) that use those buffers and reuse them. The loader will run from the space that once held the data processing and SPI data. The entire loader program must fit in that space. For example, the memory map of the system may look like the following table:

Address | Buffer | Buffer size | Loader ramifications
0x010000 | Data processing | 0x2000 | Loader base address should be 0x010000 in its linker file.
0x012000 | SPI circular buffer | 0x300 |
0x012300 | (No more big buffers) |  | Loader program can be no more than 0x2300 in length.

Warning

Don’t forget to disable all interrupts that use any of this memory before loading your bootloader there.

Run the Loader

Once the loader is in RAM, you’ll need to run it. Essentially, you want to blindly execute the code put in RAM.

This is kind of scary. One way to make it less scary is to be sure that the loader is correct and complete. As you build the loader, calculate a checksum. Then calculate the loader checksum as you load the code into RAM. If they don’t match, log an error and reset the system. The old code will still be around, and a reset will clean up the RAM so that the runtime code can safely use it.

Once you get down to actually loading the code, you’ll need to use a function pointer (see Function Pointers Aren’t So Scary). The function pointer should be set to the address in RAM that holds the loader program. Once the function pointer executes, it will never return:

  int loaderStartAddress = 0x10000; // should come from the linker file
  typedef void (*tFunctionPointer)(void);
  tFunctionPointer fp = (tFunctionPointer) (*(uint32_t*)(loaderStartAddress));
  (*fp)(); // jump to loader, never to return

If you are walking through the code, this jump will make your debugger lose all context and it will show you only assembly code. Some debuggers allow you to reload symbols without reloading code, which will fix the context. In others, you’ll have to test the process first with a small, easily debugged test loader (e.g., blinking LED).

Copy New Code to Scratch

Once the loader code is running, the next step is to get the new runtime code on the system. You could erase and program the new code directly, but if something happens, you could end up with a system whose only program is a RAM-based loader. Once the power is cycled, you end up with a brick.

Unlike the procedure for building your own bootloader, here you don’t have a back-up way to check and load code at power-on. These failures are much worse than having a unit that doesn’t currently work; they lead to units that can never work again. The optional scratch RAM becomes more critical for this type of loader due to the direr consequences.

Once the image is good in its entirety and the image cannot be removed due to user impatience or network recalcitrance, the old code can be erased and the new code programmed.

Dangerous Time: Erase and Program

Up to this point, no damage has been done to the system. If the system resets, the worst that can happen is a break in normal operation. However, once you erase the old code, that’s no longer true. Nearly anything that goes wrong is a catastrophic system failure, including a power loss.

So before erasing the old code, disable all interrupts (really this time, no excuses). Then write the new code. A reset will make the new code run. You can implement a software reset or stop feeding the watchdog (see Watchdog for more information about using your watchdog timer to improve system robustness).

Tip

Chip vendors often supply the erase and write functions, so look around before writing them.

Reset to New Code

The flow chart in Figure 7-4 shows the process we’ve gone through to get here, with the dangerous area highlighted.

Figure 7-4. Loader flowchart

There are a few additional points of interest when thinking about loaders. First, where does the loader come from? The loader could be stored in the current image already on the system. This implies that the old code needs to know how to load new code. However, it isn’t always possible to get users to update for every revision. You’d have to architect your bootloader in a way that allows users to leapfrog versions. Or you can get your loader from the same place as the new application code, as shown in part C of Figure 7-1. Then, as described there, your runtime needs to know only how to copy the loader to a spot in RAM and jump to it. The loader retains control of the rest of the process, adding flexibility. As long as the loader fits in the memory allocated by any of the released runtimes, users can upgrade to any revision from any other revision.

Second, this process assumes that getting the new image is straightforward. However, if it is not so simple, such as when you need to send the image over a network, one way to keep your loader code small enough to fit in your RAM is to let the runtime handle the difficult processing and put the loader’s image someplace local before handing control over to the loader. This decreases the flexibility for future, unforeseen needs, but you might need to do it.

Finally, loading code is dangerous. Often the loader is the last piece of code written and therefore one of the least tested parts of the system. As you draw a flow chart for what goes on in your loader, look at the amount of time it takes to erase the old image and get the new image running. Minimizing that time will save your units.

Security

Adding security into the mix tends to make bootloaders exponentially more complex. In general, winning against a sea of hackers is impossible, but you can make casual attacks more difficult. The first step is to identify what you are protecting: a secret algorithm in the code? The ability to create or verify consumables? The integrity of the hardware? What you choose to implement depends on your priorities.

You might get a little bit of help from your processor. Many chips offer code-read protection. Once it is turned on, on-chip memory becomes unreadable. The processor can still execute the code, but a debugger (or a loader) cannot read the code space.

Often times, the code-read protection will limit the processor’s ability to erase sector by sector. Instead, you have to erase all of it before updating code. (Some levels of protection won’t even let you write new code at all.)

Once you’ve done that, the weak point is in the way new code is handled. Ideally, the new code is seen only by trusted associates who will be updating the code (or the code will travel only over secure networks). However, if you allow the user to update the system, you need to augment your loader with some security features.

If you build your own (resident) bootloader, you should encrypt the new application code. The bootloader will need a decryption method (which will increase the number of sectors devoted to the bootloader).

Because the loader must fit into a limited amount of space, it usually doesn’t have a lot of space to authenticate your new code. If you don’t need to encrypt the whole code (perhaps just secret keys for consumables), consider encrypting the loader with the keys. The runtime can decrypt the loader as it puts it in RAM. Then the loader can program the clear-text new code, adding the secret keys only the loader knows.

What you do depends on your system and the goals you have. It will require some twisty thinking to determine an authentication scheme that works for you. Remember that loading new code is likely to be a weak point in the overall security system.

Linker Scripts

Although you may have seen a linker script as you got the code working, chances are it was a file that came with your compiler and you haven’t needed to open it. When you write a loader (especially the build-your-own variety), you will need to become far more familiar with what a linker does and how to modify the linker script.

Tip

You may need to modify the linker script to do some optimization as well. That is described in Chapter 8.

After your source code gets compiled and assembled, the object files (and libraries) are combined by the linker into an executable file. The resulting code has three sections:

bss segment

Contains uninitialized global variables. This will go in RAM. The odd name is due to historical reasons that don’t concern us here.

Data segment

Contains global variables that are initialized. This will go in RAM. The data segment may include bss as a subsegment. It may also include the heap and stack.

Text segment

Contains code and constant data. This may be put in read-only memory or in RAM.

Vector segment

A special part of the text segment that contains the exception vector table to handle interrupts.

The linker reads a text script to determine the memory layout of the output. To move the code or place buffers at certain addresses, you’ll need to modify the linker script. Don’t write one from scratch. When you build your executable, it already has a linker script, usually ending in .ld. Find the existing one and modify that. (Or look for a loader example specifically for your processor and start with that one.)

For example, if our system has flash for storing and running code, some internal RAM, and some external RAM, the memory map might look like the following table:

Address | Size | Memory type | Segments that can be placed here
0x000000 | 0x10000 | Read-only memory (flash) | Text
0x010000 | 0x08000 | Internal processor RAM | Text, data, and bss
0x110000 | 0x20000 | Off-chip RAM | Data and bss

A simple linker script representing that memory map would look like the following:

SECTIONS
{
   /* Memory location is in Flash; place next commands at this location */
  . = 0x000000;
  Code : {
    *(.vectors)} /* Put interrupt table at very first location in memory */
    *(.text)
  }
   /* Now put everything in off-board RAM */
  . = 0x110000;
  Data : { *(.data) }
  UninitGlobals : { *(.bss) }
}

The script is very order-dependent. The line . = 0x000000 indicates that the cursor (where the next section will be placed) is at 0x000000. The next line (.text : { *(.text) }) says to put that part of the text segment in a section called Code.

A more complex linker file would define the size and addresses of the memory so that the linker could give an error if the segments don’t fit in their allotted spaces. Also note that the type of memory (readable, writable, or executable) is specified to enable more error-checking:

MEMORY
{
  /* Define each memory region */
  Flash (rx)  : ORIGIN = 0x000000, LENGTH = 0x10000  /* 64k */
  InRam (rwx) : ORIGIN = 0x010000, LENGTH = 0x08000  /* 32k */
  ExRam (rw)  : ORIGIN = 0x110000, LENGTH = 0x20000  /* 128k */
}
SECTIONS
{
  Code : {
    *(.vectors)}
    *(.text)
  } > Flash
  Data : { *(.

Linker scripts can get very complicated. Don’t get bogged down in the details; focus on the addresses and how they match the table. Look up the language, start out by making small changes, and look in the program’s output map file to see the effects. (Reading a map file is discussed in Chapter 8.)

As with many scripting languages, it is pretty easy to build up a linker script that is so complicated no one can decipher it. Be careful. If you have a gnarly linker script to start with, search the Internet for “linker script” to find tutorials and manuals for more detailed information.

When creating a RAM-based loader (or, indeed, any code that will run out of RAM), you’ll need to move the code from the Flash section to the InRam section (or in the first example, set the initial memory location to the internal RAM address, .= 0x010000, instead of .=0x000000).

Creating buffers at hardcoded addresses is only a little bit trickier. You can add variables before any sections (before SECTIONS or MEMORY). Then use those variables in the script (and in your code):

_linkDataProcBufferLen = 0x2000;
_linkSpiBufferLen = 0x300;
_linkMaxLoaderSize = linkDataProcBufferLen + linkSpiBufferLen;

MEMORY
{
  … /* same as before */
}
SECTIONS
{
  Code : {
     *(.vectors)}
     *(.text)
   } > Flash
  /* The special buffers allocated here; the NOLOAD indicates to the linker that
  ** it doesn't need to do anything with these gaps in the memory */
  Special (NOLOAD): {
    _linkDataProcBuffer = .;       /* address of data buffer */
    . = . + linkDataProcBufferLen; /* increment memory pointer to leave a gap */
   _ linkSpiBuffer = .;
    . = . + linkSpiBufferLen;
  } > InRam

  Data : { *(.data) } > OutRam
  UninitGlobals : { *(.bss) } > OutRam
}

As for using those linker variables in your code, it depends very much on your compiler. Here is one way I’ve seen it handled. The _linkDataProcBuffer variable comes from the linker script, and the program just creates an ad hoc struct called sDataProcessing in order to access the data at that point:

extern unsigned long _linkDataProcBuffer;
extern unsigned long _linkDataProcBufferLen;
struct sDataProcessing *gDP = (struct sDataProcessing *) &_linkDataProcBuffer;

Summary

Going back to updating the code, the three loaders described in this chapter are good places to start your design. There is a lot of room between the ones presented and your particular system requirements. For example, you may want to consider having a resident bootloader but copying it to RAM when code is updated to allow it to be reprogrammed. Or your scratch RAM may be only half the size it needs to be, so you update the firmware in two chunks. There are lots of options now that you understand the basics.

Questions to ask yourself about your design:

How often will new code be loaded and by whom? Are they trusted associates? Experienced technicians? Unknowledgeable end users?

Would adding a low-cost part make loading new code safer, and can you include it?

What other pieces of the system might change? Will you need to accommodate updating information on different chips? (Maybe reprogramming an external flash with new data?)

What if the new code is corrupted? What if the new code becomes unavailable because its media is removed or communication to it is lost?

Are there security concerns? What are the points of attack? (Authentication keys? Algorithm?) What are the vulnerabilities? (Reading code from the processor? Reading code from the new code storage mechanism? Hacking the loader and taking over the hardware?)

At each stage, what is the worst that can happen? For the unrecoverable stages, how can you make them take less time or decrease the probability of bad things happening?

Once you determine what is important about your loader and set some goals, you can design the methodology to update the code.

Interview Question: Getting a Goat Safely Across a River

A man is taking home a goat, his partially tamed wolf, and a cabbage. They reach a river, but the bridge is washed out. The boat conveniently tied on his side of the shore is very small and can hold only the man and one passenger at a time. If the goat and the wolf are left alone, the wolf will eat the goat. If the goat and the cabbage are left alone, the goat will eat the cabbage. The wolf, however, will not eat the cabbage. How can they cross the river safely? (See Figure 7-5.)

Figure 7-5. Taking everyone home safely

Although this style of question is pretty common, I am strongly opposed to interview questions that involve goats crossing water, so I wouldn’t ask this one. Basically, if it is necessary for me to herd goats, I don’t particularly want the job. However, loading code is a lot like this problem. There are dangers and resource limitations at multiple points. You have to know what to look for and then use some little mental twist you learned along the way to make the leap to the solution. I generally prefer to see how people think instead of whether they are in the club that already knows the answer, so I wouldn’t ask this question. Since I don’t know what I’d look for, I’ll just provide the solution.

In this case, the trick is to note that the goat is the dangerous one and must be kept away from the others. If you frame the problem that way, the solution falls out a little easier.

Start on side A with the wolf, goat, and cabbage. Cross to side B with the goat. Return to side A. Take the wolf (or the cabbage, doesn’t matter) to side B. Now, take the goat back to side A, leave it there while you ferry the cabbage (or wolf) back to side B. Cross back to A, get the goat, cross back to B, and move the parade along.

The trick behind the problem is that you can take extra trips to get the job done, as long as the job is done properly. Updating firmware is a lot like that.

Chapter 8. Doing More with Less

Engineering requires technical skills and a deep understanding of the relevant technology. Writing good embedded software goes a step further, also requiring a devious mind with an affinity for puzzles.

Implementing the requirements on a system that has everything you need is a matter of turning the metaphorical crank to get the answer. Some solutions are more elegant than others, but most will work well enough to get the product shipped. It all gets a lot more interesting when you have a system that seems as if it can’t possibly contain everything you need. You can compromise on the features, but where is the fun in that?

For me, the great part of embedded systems implementation is the thrill of finding just the right tweak that liberates a few more processor cycles, being excited about freeing eight bytes of RAM, persevering through the map file to find a whole section that you can reclaim for the use of your code, and realizing you can get your product that coveted green award if you can just squeak out a few more milliseconds of deep sleep.

The downside to all of this is that you’ll probably make the system more fragile. For instance, when you free the RAM by having two otherwise decoupled subsystems share it, those subsystems become linked. Maintainers of the system become confused and frustrated by hidden linkages. The code is no longer modular, and subsystems cannot be reused. Flexibility is lost.

This chapter looks at the ways to get more out of your system. We’ll need to start by characterizing the resources we have and those we need. Some of the resource optimization techniques allow you to trade one resource for another. Identifying the plentiful resources is almost as important as determining where the scarce ones disappear to.

One of the most important resources is development effort. Your time is valuable. You will need to balance value versus the time it takes to implement a solution. For example, moving to a larger, faster chip in the family is a cost to all units, which could be a win if you are building only a few; trading memory and processor cycles may require you to restructure some code; and going line by line to optimize assembly code is going to take serious effort (and skill).

Code Space

Implementing an application on a system without enough code space is like trying to write a term paper in a booklet that is too small. Even though you plan ahead and try to figure out how much space should be allocated to each point, in the end you will have to write in tiny print on that last page and wind up using the margins. This section will help you cope with the code space situation.

Reading a Map File (Part 1)

I get a sinking feeling when the linker gives an error message during a build. The compiler provides all sorts of friendly advice and critiques my typing. But the linker usually doesn’t talk back unless it is something important.

On the other hand, the linker provides a wealth of information if you know where to look. Linker Scripts describes the linker input. The output map file is easier to read, though still foreign to most developers.

Warning

You may need to configure your linker to output the map file. Look in the directory where your executable is located for a .map file. If it isn’t there, check the manual.

Map files are processor-specific. The examples in this section come from a GNU-based tool chain for the NXP LPC13xx. Most map files have the same information, though it may be in a different order or have different formatting. You’ll need to make some educated guesses as you look through yours. If you aren’t sure that your map file is giving you the information you are looking for, try this: make a copy, change the code, rebuild, and then diff the resulting map file with the original.

The GNU linker for the LPC13xx starts off with a list of the library modules that are included and which module is responsible for the inclusion. In this example, libcr_c.a is included in your build because your code module aes256.c calls memcpy(). Note that only part of the libcr_c.a library is included (specifically memcpy.o):

Archive member included because of file (symbol)
../lib/gcc/arm-none-eabi/4.3.3/../../../arm-none-eabi/lib/thumb2/libcr_c.a(memcpy.o)
                              ./src/aes256.o (memcpy)

If you find that a library is large, this section helps you figure out which part of your code is calling functions in that library. You can then decide whether the code module needs to make that call or if there is a way around it.

Next in the map file is a list of global variables and their sizes:

Allocating common symbols
Common symbol       size              file
gNewFirmwareVersion
                    0x6               ./src/firmwareVersion.o

Limiting the scope of the variable using the static keyword will cause the variables to be later in the map file (sometimes the linker won’t list them at all), so if a common symbol such as the one just shown isn’t in your file, you’ve done a good job of getting rid of global variables. If you have a lot of data here, review Object-Oriented Programming in C to remember the difference between global variables and file variables.

Next is a list of sections, addresses, and sizes of code that is not referenced by anything. This section, titled “Discarded input sections” in the LPC13xx map file, shows which functions and variables are cluttering your code but not taking up any code space. Often the unused code remains in the code base when it is part of a library, some vendor code you don’t want to modify, or test code that is used only in special circumstances.

A reflection of the linker script is shown in a memory map:

Memory Configuration
Name      Origin       Length      Attributes
Flash      0x00000000  0x00008000  xr
RAM        0x10000000  0x00002000  xrw
*default*  0x00000000  0xffffffff

This can be useful as you test out your linker-modification skills. Also, some map files give you the amount of each resource used, which is very helpful for determining how close you are to running out of resources.

The next section of the map is a tedious list of all the files included in your project. Skip over that.

Eventually you get to a section that lists each function, the address where it is located, the function size, and the file it came from. These are in the order they occur in the compiled image, generally grouped together by file (module). The function listings start with the first section in the linker file (you may want to pull up the linker input .ld file as you go through the map file so you know what to expect). The line at the top gives the section (.text, which contains all the code), the address, and the size of the section:

.text           0x00000000     0x7ccd

Other than experience and expectation, there is nothing to indicate that this line is more important than the others. In the “Memory Configuration” snippet shown earlier, the total flash size is 0x8000. Here we see that the code is taking up nearly all of that.

Next, the listing breaks down the contents of the code section:

.text.Initialize
                0x0000037c       0x7c ./src/main.o
                0x0000037c                Initialize
 .text.main     0x000003f8       0xb4 ./src/main.o
                0x000003f8                main

In the map output, most functions have two lines like this, giving slightly redundant information, though this is often configurable via command-line options to your linker:

.section.functionName   address    size        file
                        address                functionName

This part of the map file shows you the size of every function. The first step to reducing the footprint is to find out which pieces are particularly large. Many of the largest pieces will be libraries, especially if you are using floating-point, C’s standard I/O (scanf/printf), or C++’s iostream. Even operators, such as division, end up calling functions:

.text.__aeabi_ldiv0
  0x00006c6c  0x4   ../lib/gcc/arm-none-eabi/4.3.3/thumb2\libcr_eabihelpers.a(rtlib.o)
  0x00006c6c        __aeabi_ldiv0
.text.__bhs_ldivmod
  0x00006ec0  0x20c ../lib/gcc/arm-none-eabi/4.3.3/thumb2\libcr_eabihelpers.a(rtlib.o)
  0x00006ec0        __bhs_ldivmod

The aeabi_ldiv0 function is a wrapper because it is only four bytes, just enough to jump to another function. However, bhs_ldivmod is a real function. Doing signed long divides is costing the program 0x20c bytes, more than twice the size of the main function.

Warning

Some map files don’t break out the function sizes in the same way. Instead they list the address of each function in the image, and you need to calculate the differences to get the size.

Functions are not the only things that take up code space. Later in the example map file is the section for read-only data:

*(.rodata*)
 .rodata.str1.1
                0x000070cc       0x36 ./src/main.o
.rodata         0x0000715c       0x10 ./src/aes256.o
.rodata.str1.1
                0x0000716c       0x35 ./src/aes256.o

Here, main has 0x36 bytes of str1.1, not something I’d name a variable. Actually, this is the automatic name that this compiler gives to string variables. So wherever there are constant strings (such as “Hello world”), the compiler collects them into one area that goes into the read-only data section. In addition to the 0x35 bytes of string data in aes256.o, the previous snippet also shows unspecified data in the file (0x10 in length). In the source file’s test function, a variable is declared with static initializers:

uint8_t buf[16] = {0xe5, 0xaa, 0x6d, 0xcb, 0x29, 0xb2, 0x71,
       0xae, 0x0e, 0xbc, 0xfa, 0x7a, 0xb2, 0x2b, 0x57, 0x59};

Because this is inside a function, it isn’t visible by name to other files. Therefore the map file doesn’t show the variable name, but it lists the constants because they take up space in the executable image.

All constants take up space, whether they are declared in #define declarations or with the keyword const. Global constants are often visible in the map file, but variables declared with #define end up in the code, so they aren’t called out in a similar way. You can see the variables declared with const in the map file under .rodata:

.rodata        0x00007ab8      0x6A ./src/displayMap.o
               0x00007b8a            kBackgroundInfo
               0x00007bf4            kBorderInfo

There may be some sections that say fill. These indicate that the data between files got misaligned, generally because a file used a constant that was smaller than the native format of the processor. For example, a character string that is five characters long will consume 1.25 words on a 32-bit processor, causing the linker to fill three bytes (usually with zeros). This is wasted space but difficult to recover. Unless you really need every byte, it is better to focus on the larger users of memory.

The rest of the map file is filled with juicy RAM details (we’ll get to those later) and debug information (generally not useful, unless you are building a JTAG unit).

Tip

If the map file doesn’t present the data in a way that is useful to you, write a script to parse it yourself! Read the map file, and create an output table where the height of the cell is proportional to the size of the function. This is pretty easy to do in Python, especially if you use HTML as the output.

Process of Elimination

Now that you know what is taking up your code space, you can start to reduce the size. Start with your tools. In addition to the different optimization levels you normally see to make your code go faster (e.g., the -O3 compiler option), there are special optimization flags to make your code smaller (for instance, in GCC, -Os tries to optimize for code size instead of speed of execution). This may be enough to solve your code size issues.

Warning

If you get a different runtime outcome when you turn on optimizations, check that all of your variables are initialized and that volatile variables are marked as such. Occasionally a compiler will disallow rare but legal features, so if you’ve eliminated the obvious, check your compiler manual.

If compiler optimizations aren’t enough, as you look for memory, it is useful to keep score so you know which changes lead to the best improvements. Not only does this give you a feel for the types of improvements, but it will also help you express trade-offs to your colleagues (e.g., “Yes, that code is uglier now, but it saves 2 Kb in code space”).

As in Table 8-1, create a spreadsheet, starting with the baseline numbers. For every change, fill in a row so you can see the relative value of the changes. If your linker output gives you the numbers in hex, you might want to let the spreadsheet translate to decimal (or the other way around).

Table 8-1. Optimization scorecard

Action | Text (code) | Data | Total | Total (hex) | Freed | Total freed
Baseline | 31949 | 324 | 32273 | 7E11 |  |
Commented-out test code | 26629 | 324 | 26953 | 6949 | 5320 | (Reverted change)
Reimplemented abs() | 29845 | 324 | 30169 | 75D9 | 2104 | 2104
Calculated const table at init time | 29885 | 244 | 30129 | 75B1 | 40 | 2144
= comment from you | = size of .text section | = size of .data section | = total image size | = hex of total image size | = bytes freed with this change | = total bytes freed since start

The table documents how you tried multiple actions, including commenting out a large block of test code. As described in Chapter 3, it is often good to be able to run code tests in the field. However, these take a huge chunk of precious space. Although feature reduction is undesirable, when looking at code space reduction, the relative importance of features should be kept in mind.

Tip

“Reduced size of code by 40%” is a super line on your resume. Hard numbers are great, but be prepared to explain how you did it and the trade-offs you considered. (And the explanation shouldn’t consist solely of “turned on compiler optimizations”!)

Libraries

As you go through the memory map, look at the largest consumers first. You may find that some unexpected libraries are included. Trace through the functions to see where the calls to these libraries are coming from.

While some (monolithic) libraries are included if any function is used, other libraries are granular, loading only the required functions. Even the standard libraries can be monolithic so that using the built-in string copy function leads to a large footprint. Your map file will show you these space hogs.

Many times you can write a function to replace the library. Other times you’ll need to figure out how to work around a limitation. Here are some common examples to get the ideas flowing:

Replace floating-point numbers with fixed-point representations (see Chapter 9).

Replace printf with a few functions that don’t take variable arguments (e.g., Log, LogWithNum).

Replace strcpy with your own implementation to exclude the strings library.

Replace the abs function with a macro to remove floating-point math library dependencies.

Functions and Macros

Keeping your code modular is critical to readability. However, each function comes with a price, increasing code space, RAM, and processor time. The code space cost is easy to quantify.

For example, I needed a small implementation of the algorithm to find the minimum of three variables (Example 8-1).

Example 8-1. Minimum-of-three algorithm

if a < b,
  if a < c, return a
  else, return c
else,
  if b < c, return b
  else, return c

I put together some code to try out the different implementation options. First, I wrote out the code in the main function and compiled it to get my baseline code size in bytes (given the other cruft automatically compiled in, this was almost 3 KB). I changed the implementation to be a macro:

#define min3(x, y, z) (((x)<(y))?(((x)<(z))?(x):(z)):(((y)<(z))?(y):(z)))

Tip

Don’t remember the ternary conditional operation? It is a shorthand version of an if statement:

test ? value if test is true : value if test is false

Not only does this make your code more dense, but there are also times when it nudges the compiler into more optimal code.

Then I modified my code to run the macro a few times to get different code space sizes. I changed the macro into a function and ran the same tests. It was a bit odd: the functions were the same, regardless of whether I used an inline function, a regular local function, or an external function.

Warning

The inline keyword should have made the function behave as the macro did, replicating the code into each location. However, it is only a suggestion to the compiler, not a requirement. How the compiler takes the suggestion is very compiler-specific. There is often a compiler-specific pragma to disallow inlines (e.g., __attribute__((noinline)) in GCC).

After I recorded the size, I turned optimizations on and reran the tests, recording the difference in bytes from the baseline for each implementation (see the following table). All along I had all my variables marked as volatile, so the compiler couldn’t remove code that appeared to have no side effects (e.g., the interim function or macro calls).

Implementation | 1 call (diff from baseline) | 2 calls | 3 calls
Macro | 0 | 76 | 152
Function (local or external) | 20 | 60 | 96
Macro with space optimization | –40 | 8 | 56
Function with space optimization | –40 | –20 | 0

The key thing to note from the table is where the crossover points are (two calls without opts, one call with). A macro uses the same amount of space as copying the line of code into your function (essentially the preprocessor does a search and replace). Once you’ve called the min3 macro a few times, it would take less code space to turn it into a function. Although the function call generally makes for smaller code, we’ll have to return to the issue when we look at functions in RAM and processor cycle optimization.

Macros do have the advantage (of sorts) that they don’t do type checking; the same macro could be used for integers, unsigned integers, or floating-point numbers. If you need to perform an operation on many different types, you’ll need to compare the space consumed by multiple functions against the equivalent macro.

Finally, a smaller snippet of code might never have a crossover point, so a macro may always be better. (Even the innocuous minimum-of-two-variables function has a crossover point of three calls, so a smaller snippet of code could be very small indeed.) Crossover points depend on many factors, including processor architecture, compiler implementation, and memory layout. You may need to do some experimentation on your system to determine the code space trade-off between macros and functions.

Constants and Strings

Going through the map file, you may find that your debug strings take up a lot of your code space. This is a really tough dilemma because those debug strings provide valuable information and useful comments.

If you’ve implemented a logging API (described in From Diagram to Architecture), you already have the ability to turn the debugging on and off per subsystem at runtime. To get rid of the debugging strings, you’ll need to go a step further and remove the functions at compile time.

You can still get some flexibility (and some documentation) using this common idiom that lets you turn off the logging in a particular subsystem (the motor subsystem in this example):

#define MOTOR_LOG 1 // set this to zero to turn off debugging
#if MOTOR_LOG
#define Log(level, str)             Log(eMotorSubSystem, (level), (str))
#define LogWithNum(level, str, num) LogWithNum(eMotorSubSystem, (level), (str), (num))
#else
#define Log(level, str)
#define LogWithNum(level, str)
#endif

When you change the value of MOTOR_LOG to 0 in the #define, all strings get compiled out (even though the code looks the same). This is part of motor.c on the principle that you want to touch only the code you are working on. However, some systems use the more global log.h to allow (and disallow) logging in each subsystem. Either way, it can be a little frustrating if you forget and try to turn debugging on during runtime, so you’ll need to find the balance between runtime flexibility and code space.

Tip

Adding a significant amount of complexity might give you even more compression. If you really need to know what is going on, consider a macro system that replaces strings with constants and uses a separate script to build a dictionary of all the strings from the code. The output looks useless, but a postprocessing script makes it readable, and you’ve potentially saved a ton of code space.

As for other constants, why are they there? Do you really need them? Are there ways to calculate the data at runtime? Or compress it? Can you move the information to another storage mechanism (an external device such as a flash or EEPROM)? The options depend on your system, but now that you know where the space is going, you can dig into the problem.

RAM

Unlike the angst produced when your linker says you are out of code space, a RAM resource error from the linker should give you a sense of relief at having dodged a bullet. The alternative symptom for a RAM shortage is a system that crashes randomly.

Some of the same techniques for finding more code space work for RAM as well. However, it’s more difficult to find out where the RAM is disappearing to. You can make it easier on yourself with some design choices.

Remove malloc

To really understand where your RAM goes, eliminate dynamic memory allocation. Local variables are hard to see, so if you make large arrays global, you can use the memory map output of your linker to determine where the RAM is used.

Warning

If your system uses a language with garbage collection, you might not have the luxury of knowing where your RAM is going. For embedded systems written in such languages, dealing with RAM constraints becomes much more difficult.

If I haven’t convinced you that transparency is a good enough reason to get rid of dynamic allocation, there are some other reasons. If you have an operating system, malloc makes an application nondeterministic. Even without an OS, malloc makes the same function run differently depending on the current state of the heap. Say you have a heap that is 30 bytes in size (this is a little small, but it makes my example easier), and in a particular function you allocate a 10-byte buffer and a 5-byte buffer. Then you free the 10-byte buffer so you can allocate a 20-byte buffer. What is wrong with this picture? More than you might expect:

Wasted RAM

The heap requires RAM to keep a data structure describing the memory that’s in use. Every dynamic allocation has some amount of metadata overhead.

Lost processor cycles

Keeping track of the heap is not free. Searching the heap data structure for available memory is usually a binary search, which is pretty fast, but it is still a search.

Fragmentation

As shown in Figure 8-1, after you’ve allocated the 10-byte and 5-byte buffers, the first half of the heap is used. After you free the 10-byte buffer, there are 25 bytes available. However, there isn’t a contiguous block for the 20-byte buffer. By mixing buffer sizes, the heap gets fragmented. With a heap that is much larger than the variables (and few or no small allocations), this is less of a problem. However, in an embedded system, you probably won’t have the luxury of a large heap.

Figure 8-1. An example of heap fragmentation

If you have a buffer that can be recycled after it is used, there are options besides using the built-in dynamic allocation system. If you need two buffers—say, one for an interrupt to write to and one for the normal code to read from—use a ping-pong buffer as described in Interrupts. If you have a queue of data, implement a circular buffer (Chapter 6). If all of your buffers are the same size, consider a chunk allocator (there are many examples on the Web and Wikipedia). However, avoid reinventing the wheel. If your memory system is so complex that you end up rebuilding malloc, use the built-in version. Its benefits will probably outweigh the costs on your system.

Reading a Map File (Part 2)

Going back to the map file, we may find some RAM information in the .data and .bss sections. These sections are not always obvious, and you may need to search for them; they often look like the text section filled with functions. For each section there is a summary that shows the total amount used:

.data       0x10000000     0x144
…
.bss        0x10000000     0x1b7c

Tip

In some map files, the .data section is called .cinit. Most linkers are pretty standard, but if you see things you don’t recognize, you may need to break out the linker manual or search online.

Looking back to the overview memory map (the memory configuration in Code Space), this program is taking up quite a lot of its 0x2000 bytes of RAM.

Note that this total doesn’t include the heap or the stack; see Stacks (and Heaps). There is no heap in this program, so all remaining RAM is allocated to the stack. Often allocation for the heap and the stack is done in the linker file (or as an argument passed into the linker). Without malloc, the heap can be allocated to zero (or only as large as your libraries require). Long function call chains can make for a large stack, particularly when the intermediate calls have many local variables.

To determine the size of your stack, in a debug build, fill a larger-than-expected stack with a pattern (such as 0xDEADC0DE). Then exercise as much of the system as possible. Finally, check how much of the stack is undisturbed to see how much you can eliminate. The stack allocation should be a bit larger than the amount you calculate (I’d say 25% larger). Stack overflow bugs are very difficult to solve, and a bit of buffering will save you from needing to do so. (If you have an operating system, each thread may have its own stack and heap.)

Tip

Recursion is seldom used in embedded systems, because the stack may be exhausted before the solution is obtained.

Returning to the map file, the .data section contains constants for your initialized variables, and .bss contains all of your uninitialized global and static variables. The .data section variables also require code space for their initial values, which is captured in the rodata section. The .bss section variables are RAM only. The layout is similar to the one for functions:

.section      address    size        file
              address                global variable name
              address                global variable name

If your variables are local to your file (or static variables in a function), only the size and filename are shown:

.bss           0x10001bb4       0x14 ./SharedSrc/i2c.o

As with functions, the goal here is to look for the larger variables, as those are where the best savings can be had. (Reducing a 12-byte array by 50% is not as exciting as doing the same for a 200-byte array.)

Registers and Local Variables

Unlike global variables, it is more difficult to estimate the RAM consumed by local variables (those within functions). Registers are the memory pieces of a CPU. Some processors can’t perform actions on RAM directly. Instead, they have to load the value from RAM into a register, perform the action on the register, and then store the information back to RAM. That’s a lot of instructions for a line of code that says:

i++;

Not every processor needs to go through such gymnastics, but registers are faster for any processor to use. The reason local variables are difficult to count when tallying up RAM usages is that many local variables spend their whole existence as registers.

Warning

C has a keyword called register that is supposed to give a hint to the compiler about which variables you think should go in registers. The keyword is generally ignored, if it is implemented at all.

Function parameters

A function’s input arguments are often in registers instead of on the stack (RAM). You can encourage the compiler to do this if your function has only a few parameters per function. A good rule of thumb is less than four parameters per function. If you have a 32-bit processor, that would be four 32-bit variables, not a dozen pieces of data crammed into four structures.

In fact, if you have an N-bit processor, try to stick to N-bit variables. Larger variables generally are not candidates for precious registers. (Smaller variables often add processor steps because the compiler tries to access only the part you find interesting.)

To use registers, you usually want to pass arguments by value. If you pass by reference (or by pointer), you are sending the address of the data. The address may be in a register, but the data will need to be accessed in RAM. For example:

int bar = 10;
foo(&bar);  /* this takes more RAM than passing by value
             because bar has to be in RAM to have an address */

There is an exception to the pointer guidelines if you pass a structure to a function. If you pass a structure by value, you are likely to have two copies of it in RAM: the original and the stack version of the copy. Pass pointers to structures to eliminate duplication.

Minimize scope

You are probably familiar with namespace (where a variable is valid according to the programming language). A variable may be global, in which case its namespace scope is the whole program. Or it may be declared in a file, in a function, or even in a designated area of a function (e.g., inside a loop). However, there is another form of scope: where the variable is used by the code. You might define 10 variables in your function in such a way that their namespace is the whole function, but if you use each one in a limited area, its scope is that area, not the whole function.

For the most efficient register use, each function should use only a small number of variables at any time. When optimization is on, your compiler will move around the code to limit the scope of the variables and free up registers. However, you can help it figure out what is going on. For a simple (and somewhat lame) example, take this code snippet that needs to make each array value equal to its place in the array. It does some processing and then needs the array to be reinitialized to its original values before going on:

for (i=0;  i < MAX_ARRAY_LENGTH; i++) { array[i] = i; }
… /* do stuff to array, need to set it up again */
/* i still equals max array so subtract one and run through the loop again*/
for (i--; i >= 0; i--) { array[i] = i; }

The programmer thought that avoiding the need to reinitialize i would save a few cycles. However, as the code is written, its value has to be remembered across other processing, even though it doesn’t really matter. The variable’s scope is increased without cause. We can rewrite the code to enable the compiler to forget about i when it isn’t in use (and the compiler can reuse a register for a different variable):

for (i=0;  i < MAX_ARRAY_LENGTH; i++) { array[i] = i; }
… /* do stuff to array, need to set it up again */
for (i=0;  i < MAX_ARRAY_LENGTH; i++) { array[i] = i; }

For this example, the solution is a no-brainer (and has cleaner code). However, when you look at your code, can you see places to decrease the scope of the variables? Maybe do all the processing on one variable before shifting to another?

Don’t worry about the total number of variables you have in a function. If you limit the scope of each variable and within each scope limit the total number of variables in play, the compiler can figure out the rest. It doesn’t always pay to reuse variables, particularly when your compiler can’t recognize an old variable used in a new scope.

Look at the assembly

How do you know if you’ve been successful in your attempts to mind meld with the compiler? Look at the assembly code. No, wait! Stop running away!

I’m not suggesting you learn assembly language for this. Well, maybe read it a little. I definitely am not suggesting you write in assembly language (that is in Coding in assembly language). However, you can learn a lot about your compiler (and your code) by looking at the list file (.lst) or walking through the assembly in your debugger. Make sure you can see the higher-level language at the same time as the assembly. Your code acts as sort of a comment for the assembly code so you know what it is trying to do. (You may need to add a compiler option to retain the list files; look on the Web or in your compiler manual.)

Warning

The first few times you look at assembly and walk through it, make sure optimization was off during compilation. The tricky things that occur during optimization are not a good introduction to learning how to read assembly code.

As you look at the code in a new light, note that when the compiler optimizes for speed, it will put the most often used variables into registers so that it doesn’t have to load and store them for each access. If you can set your compiler to optimize for RAM usage, it will instead put the largest number of variables in registers, possibly leaving an often-used variable in RAM if its scope is large.

Function Chains

We saw in Stacks (and Heaps) that each function call increases the size of the stack. Say we have three functions: main calls foo, and foo calls bar. At that point, our stack looks like the left part of Figure 8-2.

Figure 8-2. Function chains in the stack

Only a few registers are available on the processor, so everything else goes on the stack. You don’t get extra registers for making function calls; there is a static number through the whole system. So, if you have a chain of functions, the local variables and parameters from earlier functions will have to go on the stack to make way for the latest calls. Even when a parameter or a local variable starts out as a register, if it has a large enough scope, it may end up on the stack when you call another function.

As shown in the right side of Figure 8-2, if you can tweak your design to have a flatter function structure, your stack can be smaller (and each stack frame will be smaller as you increase the register usage).

One exception to the rule that functions calling functions incur RAM costs is a technique called tail recursion. It isn’t really recursion (remember that recursion is bad in an environment with limited RAM!). In tail recursion, you call the next function as the last statement of the current one (for instance, call bar at the very end of foo). This lets you retain encapsulation for your modules and keep the stack small. The compiler will remove foo’s local variables and parameters from the stack, allowing the bar function to return directly to main (without passing through foo). Even though foo called bar, the stack looks like the right side of Figure 8-2.

Also, whereas an earlier section discussed the trade-offs of macros in terms of code size, macros play a different role in RAM reduction. Macros take up no stack frame at all, so they are good for decreasing the RAM footprint (especially for little functions such as the one in Functions and Macros). A little more code space might lead to less RAM (and faster execution).

Pros and Cons of Globals

Global variables have a bad reputation. They’ve been party to all sorts of spaghetti code, they continually conspire with inexplicable outside forces to control the flow of code, and they don’t know the meaning of the word reentrant.[8]

Coders that use global variables are often accused of being lazy. But laziness and convenience are not the only reasons for using global variables. They do have a good side.

But a little more about their bad side first. Global variables can’t be stored in registers, which means they always take RAM. They are almost always slower to access than a local (register) variable. So if you have a global variable that could be replaced by a local one, make it local. Even if it ends up on the stack instead of in a register, once the program execution goes out of scope, that RAM is free for use elsewhere.

When can a global save on RAM? Say you have a function chain and need a particular variable in several functions (or worse, you need the data only at the end of your function chain). If you pass the data in via a parameter to each function in the chain, with intermediate processing, the compiler may be unable to keep the data in a register all the way through the chain. Because it is a parameter to each function, the data will go on the stack multiple times. A global variable is outside the stack and short-circuits the process.

Memory Overlays

Does your system have a few large buffers? Some of the particularly bulky ones I’ve found on some of my systems include display buffers, communications buffers, and sensor input buffers. Once you’ve identified the large buffers in the system, ask yourself whether they all need to be used at the same time.

For example, maybe your display buffer is used to build up the image and then sits idle until the next time you need to update the display, at which point you build it up again. Or the sensor input buffer isn’t needed while you’re waiting for an interesting event. Once you notice the event, you need the whole buffer to queue up the data, but until then, only a small area of it is needed. Or maybe you have a large communications buffer for when you are receiving new code to load, but in general you need only a little of it. Does any of that sound familiar?

If your system uses dynamic memory allocation, these subsystems might allocate and free their memory to avoid tying up the RAM resources. However, if you’ve banned dynamic memory allocation (and if a chunk allocator is overkill), your system might benefit from RAM overlays. This is where two (or more) subsystems share some or all of their allocated memory, but only one gets to use it at a time. By overloading the resource, you effectively get a lot more RAM. Figure 8-3 shows a 4 KB RAM buffer available on a processor. Without the overlay, they overflow the available space and the system won’t compile. With the overlay, the buffers fit. However, the subsystems depend on each other in a way that is not obvious to the casual observer (since you’ve smashed encapsulation to bits).

Figure 8-3. Sharing RAM between modules

There are several ways to implement memory overlays. The simplest is to treat the buffer as private to one module and allow access to it through a single function, as described with the modified singleton pattern in From Diagram to Architecture. Another method is to implement a union of several arrays, possibly with an owner tag to indicate the current user of the memory.

Alternatively, to make the subsystems less entwined, you can modify the linker script to overlay the RAM buffers on each other. This eliminates any direct interaction between the two subsystems, but requires that any future developer understand that the two subsystems cannot run at the same time (without catastrophic results).

[8] A subroutine is reentrant if it can be interrupted in the middle and then safely called again before its previous invocation has been completed. (More in Chapter 5.)

Speed

Of all the places to spend your valuable development time, the worst can be sunk in trying to squeeze out a few more cycles from the processor. Try to avoid that position by starting with the design of the system, maybe building in some overhead when selecting a processor. However, there are times when you have to make the code go faster to respond to a real-time event, to add another feature, or to reduce power consumption (see Chapter 10).

Before delving into serious system tuning, start by profiling your application to make sure you focus on the important parts. Profiling is not as simple as reading the map file, but it is still unwise to optimize the wrong thing (e.g., cutting half of your initialization time, which runs only once at system boot, instead of focusing on the problem in your main loop).

Profiling

To know where to spend your time optimizing the code, you need to know where the cycles are going. Many compilers (and operating systems) have some built-in analysis tools, including profilers. If you have those, learn them. If you don’t, you may want to build your own rudimentary profiler to give you some insight.

In physics, the Heisenberg uncertainty principle says that electron momentum and position cannot simultaneously be known to an arbitrary precision. In profiling, there is also uncertainty: the more precisely you need to know how long each piece of code takes to run, the less you can know about the whole of the execution. That is, your profiler will change the behavior (and timing) of the code. Understanding the impact your profiler has on your code is an important part of profiling.

Profiling usually starts off trying to answer questions about where the processor time is spent. By digging down into a particular function, you can apply the same method to determine which part of that function is longest (and so on). Each of the four profilers discussed here has a slightly different focus to help you get a broad and deep view of your system.

Tip

Similar to the code space and RAM score cards, use your profiler to track which modifications were most effective.

I/O lines and an O-scope

If you have a few open I/O lines, they can show you where to start on the path to profiling your code. As you enter a function of interest, set an output line to be high. When you leave, set it low. Watch these lines on an oscilloscope to see how long each function takes.

For example, we have a system that waits for data to be ready, reads the data, transforms it, and displays the result to an LCD:

Interrupt sets data ready variable when data is available to be read

Main loop:
  Loop, waiting for data ready to be set
  Read the data in the buffer
  Transform data into information
  Write the information to the LCD

If we had four I/O lines, setting and clearing one for each stage, the result might look like Figure 8-4. Each I/O switches exactly as its predecessor completes (often you don’t need to instrument everything; you can look at the gaps). In the beginning, it looks like everything is good; the bulk of the time is spent waiting for the data to be ready and writing information to the LCD. Eventually, the data to be ready goes to zero, but the other tasks take up the same amount of time.

Figure 8-4. Profiling using I/O lines on an oscilloscope

Two things in the graph are important. First, as time goes on, the data is always ready as soon as the LCD write is finished. Let’s assume the data is ready at a fixed interval (e.g., sampled from an ADC). The narrowing data-wait time means that you will start to miss data because the system can’t keep up. Second, the system time is dominated by the LCD write, which is the activity on the bottom. If you can fix what is taking so long in there, the system might be able to keep up with the input.

The great part about I/O line profiling is being able to set and clear an output line in an interrupt. It will slow down your system to add the I/O changes to interrupt service routines, but only by a little. The profiling information you gain about the system is often worth the temporary slowdown.

Timer profiler 1 (function timer)

For a system with few interrupts, another way to implement a profiler is to time how long a section of code takes to run. In System Tick, I talked about how to build a system clock to measure how long things will take. We built the TimeNow function to return the number of milliseconds since the processor started. Using that as a simple timer profiler might look like the following:

struct sProfile {
  uint32_t count;
  tTime    sum;
  tTime    start;
  tTime    end;
} ;
void main(){
  struct sProfile profile;
  ...

 profile.count = 0;
 profile.sum = 0;
 while (1) {
    profile.start = TimeNow();
    ImportantFunction();
    profile.end = TimeNow();
    profile.sum += profile.end - profile.start;
    profile.count++;
    if (profile.count == PROFILE_COUNT_PRINT) {
      LogWithNum(eProfiler, eDebug, "Important Function profile: ", profile.sum);
      profile.count = 0;
      profile.sum = 0;
     }
   ... // continue with other main loop functions
  }
}

The function profiled (ImportantFunction) should be longer than the timer tick—at a minimum, twice as long, and preferably at least 10 times as long. It is also critical that the profiling function (TimeNow) takes a negligible amount of processing compared to the profiled function.

Note that the summation and logging are outside the scope of the profiler. Try to sample only the things you care about and not all of the accoutrements of profiling. If you aren’t sure that you’ve eliminated the profiler overhead, try commenting out the function of interest so that you are profiling only the start and end time acquisition. The result should consistently be zero if the profiler is working properly.

Tip

If you have a few functions of interest and are trying to decide where to spend your development time, you might use three or four profiler variables to monitor different areas of code.

Finally, using many samples will average out any minor differences (for example, if you have a short, intermittent interrupt). By sampling multiple times, you also get a small increase in precision as the integers get larger. For example, with one sample, the function may take 10 ms. But with a thousand samples, the sum may give you 10,435 ms, indicating an average of 10.4 ms.

Figure 8-5 shows how this function timer profiler measures one area of interest at a time.

Timer profiler 2 (response timer)

If you want to profile a function that is much shorter than your timer tick, you can use a shorter timer tick in your profiler or change the way you sample. The following example, for instance, profiles the whole main loop because the profiler timer doesn’t stop until the counter has reached its goal, as shown in the middle of Figure 8-5. This may yield valuable information if you are trying to react to an event within a certain amount of time:

  profile.count = 0;
  profile.start = TimeNow();
  while (1) {
    ImportantFunction();
    profile.count++;
    if (profile.count == PROFILE_COUNT_PRINT) {
      profile.end = TimeNow();
      profile.sum = profile.end - profile.start;
      LogWithNum(eProfiler, eDebug, "Important Function profile: ", profile.sum);
      profile.count = 0;
      profile.start = TimeNow();
      }
    ... // other main loop functions are also part of the profile
  }

This timer profiler has a larger impact on the results because it increments the counter and compares it to a constant inside the profiled area. If your loop is very quick, these activities may be non-negligible. As before, you can check this assumption by commenting out everything in the main loop except the profiler. By taking this measurement, the baseline of the profiler can be subtracted out of your final analysis.

As shown in Figure 8-5, this profiler gives you a longer view of the system timing. Both of the timer profiling methods break up the flow of code. They tend to be temporary pieces of code that you remove after gathering the information you want. Instrumenting the code in this fashion is straightforward, so reproducing the code as needed is usually better than leaving it in to slow down the code and/or confuse future generations.

Sampling profiler

If you have an interrupt-driven system, profiling can be more difficult. However, if you can allocate a block of RAM to it, you can implement a sampling profiler with a timer-based interrupt.

First, create a timer interrupt that is asynchronous to everything else in the system. For example, if you have 10 Hz and 15 Hz interrupts, make sure your new timer is not 1, 2, 3, or 5 Hz. Instead, make it something like 1.7 Hz so it is not evenly divisible into any of your other time-based interrupts. This makes sure that your results are not biased by periodic functions. Figure 8-5 illustrates the relative timing of two functions and your custom timer (the little arrows at the bottom).

Figure 8-5. Comparing timer and interrupt profilers

Now, on every profiler timer interrupt, save the return pointer to the block of RAM (you’ll need to know the stack frame format of your processor and possibly some assembly to do this). The return pointer tells you what code was running when the timer interrupted. Once the RAM buffer is full, stop the timer and output the list of addresses. Armed with a list of return addresses, figure out where these are in the image using the map file. A scripting language will significantly help you parse the map file and count up the number of times the profiler sampled from each function. You can thus figure out what percent of the processor time each function is taking.

This method works best when your processor allows nested interrupts and the profiler timer is the only one allowed to interrupt other interrupts. If you have other nonmaskable interrupts, you won’t be able to see those in the results.

This sampling profiler doesn’t slow down any particular function, but it does place a tiny drain on the whole system. (Outputting the list of addresses may be a larger drain, depending on your implementation.) This sort of profiler is readily left in the code, with the timer (and output function) turned off when it is unneeded.

Optimizing

Armed with the knowledge of what is slowing down your system, you can now explore the options available to you. Definitely start by turning on optimizations in your compiler. The less tweaking you need to do, the better your code will be.

Next, try to get most of your variables into registers. Even if you have a long function call chain and variables end up going on the stack while another function is called, if they can at least be registers in a smaller scope, the code will be faster.

Having done those basics, consider the techniques in the following sections.

Memory timing

Wait states are a bane to efficient use of the processor resources. Many types of memory cannot be accessed as fast as the processor runs. To get information from such memory, the processor has to wait some number of processor cycles to offset the timing difference. Memory has a number of wait states. For example, if your code runs from four wait state flash, every time it needs a new instruction, the processor has to wait for four clock cycles.

I should point out that four wait state memory doesn’t (always) mean your code runs four times slower than in one wait state memory. Processors can pipeline instructions to reduce the impact of slow memory—in other words, look up the next instructions while executing the current one. If the number of pipeline stages is greater than the number of wait states, the memory will slow execution only when the pipeline stalls (which tends to happen at branches, such as if statements, and function calls because the processor can’t just assume that it will execute the next instruction in memory).

By knowing how many wait states each type of memory has, you have options for speeding up execution by moving code to faster memory. For example, you may want to copy a critical function to zero wait state RAM if that is faster than your normal program flash. If you need the function occasionally, you can overlay the memory with another buffer. When the function is needed, copy it out of code space into RAM (though you’ll need to do some profiling to make sure the overhead of copying it is balanced by its amazing speed in RAM). Many compilers support a keyword (ramfunc), pragma, or macro that lets you indicate that the function should be stored in code space but loaded into RAM on boot (by the startup code that runs before main). Often you’ll need to modify your linker file to put the section into RAM. Check your seldom-used compiler manual for this, not your handy processor manual.

Variable size

When you are working on an 8-bit processor, it is easy to understand that using a larger variable will incur a higher cost in terms of processor cycles. Although it might not seem as logical, using a smaller variable on a larger processor may have some unwanted overhead as well. For best results, local variables should match the size of the registers.

When the compiler needs to reduce the size of a local variable from a native 32-bit variable to an 8-bit char or a 16-bit short, it has to extend the sign, which means every manipulation takes two instructions (or one extra instruction to zero extend unsigned variables). Cutting out one instruction at a time is a good way to drive you crazy. However, if you generally implement variables using native types, you won’t have to sort through your code later.

Along the same lines, try to use the same type of variables for the bulk of your processing. Type conversions are a waste of processor time. Converting between signed and unsigned also should be avoided as much as possible.

Warning

Signed ints are upgraded to unsigned ints when the two are compared. So a small negative-signed variable is likely to be considered a numerically larger value when unwisely compared with an unsigned variable. Stick with unsigned variables unless you really need negative numbers.

One last look at function chains

Functions make your code run more slowly because the processor switches context by pushing data on the stack (and usually having to refill the pipeline). Of course, functions make your code maintainable (and usually make it more correct), so don’t eliminate them entirely. Try to avoid small functions where the cost of calling the function may outweigh its benefit.

For example, take an LCD display driver. LCD drivers are often optimized because the data needs to be on the screen before the screen refreshes. If there is no other form of control and only part of the data is available in the LCD’s buffer, the screen may tear (show part of the old image and part of the new) until the next refresh cycle. Our example driver is for an LCD that is 240×320 pixels, and each pixel is 16-bit (meaning it has 2¹⁶ colors). When putting an image on the screen (or part of the screen), the driver reads from a buffer of RAM and puts it on a parallel bus that is 8 bits wide. The code starts out looking like this:

// in Lcd.c
void LcdWriteBuffer(uint16_t* buffer, uint16_t bufLength){

  int i;
  while (bufLength) {
    LcdWriteBus(buffer[i] & 0xFF);       // write lower byte
    LcdWriteBus((buffer[i] >> 8) & 0xFF); // write upper byte
    i++;
    bufLength--;
  }
}
void LcdWriteBus(uint8_t data){

  IoClear(LCD_SELECT_N);          // select the chip
  IoWriteBusByte(LCD_BUS, data);  // write to the I/O lines
  IoSet(LCD_SELECT_N)             // deselect the chip
}

// in Io.c
IoWriteBusByte(uint32_t io, uint8_t data){

  // ioBus was configured during initialization
  ioBus[io] = data;
}

That is a lot of code, and when it’s lined up like that you may see some chances for optimization. When the code is scattered in multiple files, it can be harder to see.

Tip

When you find a spot that seems worth optimizing, look at its call chain as a whole.

Selecting and deselecting the LCD for every write is silly. The LcdWriteBus function has to do it because it doesn’t know what else is going on around it. However, the LcdWriteBuffer function knows it is sending a large amount of data. Although the LcdWriteBus function is probably used by other parts of the code, the LcdWriteBuffer code won’t call it, instead implementing its own version:

//in lcd.c
void LcdWriteBuffer(uint16_t* buffer, uint16_t bufLength){

  int i;
  IoClear(LCD_SELECT_N);                       // select the chip
  while (bufLength) {
    IoWriteBusByte(LCD_BUS, buffer[i]);        // write lower byte
    IoWriteBusByte(LCD_BUS, (buffer[i] >> 8)); // write upper byte
    i++;
    buffLength--;
  }
  IoSet(LCD_SELECT_N)                          // deselect the chip
}

So you’ve eliminated one link in the function chain. The function will run faster, and there is no way the compiler could have done that optimization for you. All you had to do was copy code from one function to another, not sell your soul or anything. However, if there was a bug in LcdWriteBus, now it is likely to be in LcdWriteBuffer. Maybe you should add a comment in each function cross-referencing the other, so a maintainer will know to change both locations.

The slope gets much more slippery from here. The next function in the chain is IoWriteBusByte. It is only one line of C code, but underneath, it does some indirect addressing to write to the I/O register. It does that every time the function is called, even though the byte is written to the same LCD I/O lines each time.

It seems like an easy change. However, the I/O function is in the I/O module, not the LCD module. If you change this, your code becomes faster but less portable, as it won’t have any abstraction between the LCD and the hardware. Is it worth it? Are you looking at a situation where you need 20% more cycles or one where you absolutely need 60% more cycles?

The cost of making a dramatic improvement is often high. Even if the change is fast to make now, future development time will need to be invested as the system becomes less flexible and more information-intensive. The next person is not going to recognize the trade-offs, so they will stumble over the code and ask why you made it so ugly.

There is another option. By making IoWriteBusByte a macro, you can preserve the modular boundaries and still eliminate the stack manipulation. The macro will take up a little more code space (especially if IoWriteBusByte is used in many other functions). Worse, you are still spending processor cycles with the pointer access. However, before we think about breaking encapsulation further, let’s think through some other options.

Consider the instructions

If you were the processor, what would go on with the instructions that appear in the two IoWriteBusByte calls that read buffer[i] twice and then increment i? Though the machine code is processor-dependent, we can estimate the steps for each line of code (the steps are in italics):

IoWriteBusByte(LCD_BUS, buffer[i] & 0xFF); // write lower byte
  (Variable i would already be in a register, already initialized)
  Copy the buffer pointer from the stack into a register
  Add the buffer pointer to i
  Read the contents of memory at that address
  Perform bitwise AND with contents
  Put i on stack
  Call IoWriteBusByte, passing data
  Pop i off the stack

 IoWriteBusByte(LCD_BUS, (buffer[i] >> 8) & 0xFF); // write upper byte
  Copy the buffer pointer from the stack in a register
  Add the buffer pointer to i
  Read the contents of memory at that address
  Perform shift with contents
  Perform bitwise AND with result
  Put i on stack
  Call IoWriteBusByte, passing data
  Pop i off the stack

i++;
  Increment register i

I broke the process down into pretty granular steps. Although some assembly languages could combine some of my instructions, most of them would need a few more. The goal is to think like a microprocessor. And if you do this, even if you are wrong sometimes, you will write better and more easily optimized code. Thinking along those lines, it would be more efficient if we didn’t have to add the index to the buffer pointer:

IoWriteBusByte(LCD_BUS, *buffer & 0xFF); // write lower byte
  (Variable buffer would already be in a register, already initialized)
  Copy the buffer pointer from the stack in a register
  Add the buffer pointer to i
  Read the contents of memory at that address
  Perform bitwise AND with contents
  Put buffer on stack
  Call IoWriteBusByte, passing data
  Pop buffer off the stack

IoWriteBusByte(LCD_BUS, (*buffer >> 8) & 0xFF); // write upper byte
  Copy the buffer pointer from the stack in a register
  Add the buffer pointer to i
  Read the contents of memory at that address
  Perform shift with contents
  Perform bitwise AND with result
  Put buffer on stack
  Call IoWriteBusByte, passing data
  Pop buffer off the stack

buffer++;
  Increment register buffer

i++;
  Increment register i

This doesn’t really change anything important; it only gives a hint to the compiler about what choices it could make to go faster. (And some compilers are intelligent enough to have parsed the code to get to the same point.)

The immediate lesson is to use pointer arithmetic (buffer++) instead of arrays and indexes where possible. Pointer arithmetic uses a little less RAM and gives the compiler a clearer path toward optimization. The larger lesson is to understand how your code is translated into machine language, and to make it easy for a compiler to take a faster route.

Reduce math in loops

What next? Ideally, we’d like to do less math during a loop. The shift and bitwise-AND are relatively cheap, but if you do them on every pass, they add up. It wouldn’t be necessary if the buffer were a byte buffer instead of a word buffer:

void LcdWriteBuffer(uint16_t* buffer, uint16_t bufLength)
{
  uint8_t *byteBuffer = (uint8_t *) buffer;
  // buffer is now a buffer of bytes.
  // This works only if your endian-ness matches what the hardware expects
  bufLength = bufLength*2;

  IoClear(LCD_SELECT_N);         // select the chip
  while (bufLength) {
    IoWriteBusByte(LCD_BUS, *byteBuffer);
    bufLength--; byteBuffer++;
  }
  IoSet(LCD_SELECT_N)              // deselect the chip
}

The code now relies on a feature of the processor (endian-ness) that will not be portable. However, the change will speed up the code—probably. Remember that memory size matters. This change would improve the speed of an 8-bit chip significantly. For a 32-bit chip? Well, it shouldn’t slow it down much, because we were already doing the bit manipulation previously shown. At least now we are letting the compiler do the best optimization it can. The potential improvement depends on your architecture.

Usually, making the loop smaller is a good idea, as is making it do exactly the same set of operations each time. Keep if statements out of loops, as they mess with your pipelining and slow it all down.

However, there is a conditional statement (while) in the previous loop. At this point, it may be taking a nontrivial number of cycles. If you were to walk through the assembly code, you might see that the loop is writing the byte, decrementing and incrementing some registers, and checking to make sure one register is not zero.

Tip

Checking against zero is cheaper than checking against a constant (much cheaper than checking against a variable). Make your loop indexes count down. It saves only an instruction or two, but it is good practice.

Making a PB&J

There is a neat experiment you can run if you have some 5–9-year-olds handy. Ask them to pretend that they have a robot (you) and want to make a peanut butter and jelly sandwich (PB&J). The ’bot is very dumb, so they have to give it complete and detailed instructions.

The robot already knows how to grasp and lift objects (bread, peanut butter, jelly, bread, and spreaders). But it doesn’t know how to put it all together to make a sandwich. What are the steps, and how do they go together? As the child writes (or says) the steps, the robot often creates a mess instead of a sandwich as it interprets each command literally.

Breaking down the process of making a PB&J is a fun way to get kids to understand a little bit about the process of writing software. Plus, you get to say “does not compute” like a Dalek until they manage a sandwich.

(Thanks to Walter and Emma Stockwell for letting me experiment upon their sons, Alasdair and Toby.)

Loop unrolling

By now, when you look at the assembly for the loop in this function, the check against zero may be one of about 10 instructions. The decrement of bufLength is another. You are spending 20% of your time dealing with loop overhead.

The compiler can’t get rid of that, but you know more about the situation. For example, you know that there are an even number of bytes (because you made it that way). There is no need to check the loop on every pass, only on every other pass:

void LcdWriteBuffer(uint16_t* buffer, uint16_t bufLength){

  // Use a buffer of bytes. This works only if your endian-ness is correct.
  uint8_t *byteBuffer = (uint8_t *) buffer;
  bufLength = bufLength*2;

  IoClear(LCD_SELECT_N);         // select the chip
  while (bufLength) {
    IoWriteBusByte(LCD_BUS, *byteBuffer);
    byteBuffer++;
    IoWriteBusByte(LCD_BUS, *byteBuffer);
    byteBuffer++;
    bufLength--;
  }
  IoSet(LCD_SELECT_N)              // deselect the chip
}

Now, if this loop previously took 10 instructions to write a 16-bit pixel, instead it will run in 8. We’ve eliminated an increment to bufLength and a check against zero for a 20% improvement (yay us!). Loop unrolling, then, means reducing the number of iterations by duplicating code inside a loop. Improvements that large are not usually so cheap. Now we look back to IoWriteBusByte as the largest consumer of cycles in the loop, even as a macro. We’ll stop here because, as noted earlier, it accesses variables in another file, so it is a more egregious modification with respect to maintaining modularity.

Why don’t I care about removing the bufLength multiplication at the top of the function? For the same reason I never mentioned breaking modularity to reduce IoClear and IoSet. Reducing the code outside the loop is much, much less important because it runs only once (or once per function call).

When you are optimizing, focus on the loops and the repeated actions.

When I introduced the LCD function, I mentioned the screen was 320×240 pixels. Could you have a single function write out a whole screen’s worth of data without any conditionals? Of course. And it would be faster than having conditionals. However, you’ll be sad when you’ve lost count after pasting the 12,243th instance of:

IoWriteBusByte(LCD_BUS, *byteBuffer); byteBuffer++;

Worse, when you compile, you may find that all those repeated instruction come with a cost: code space.

Explaining Optimization to a Business Major

Ultimately, optimization is an economics problem. You start out with a portfolio of assets associated with your system. The major ones are the RAM, processor cycles, and code space. You may also have some auxiliary assets in power consumption and peripheral support on your processor. Your most liquid asset is development time, so think of that as money. Once you invest (or allocate) these assets to parts of the system, you lose the opportunity to use them in other subsystems.

If the set of investments you initially selected does not deliver the expected returns, you might partially recoup the spent resources, but to do so you will need to sink more development time into the project. As you go further along the development path, switching the allocations becomes more and more costly (and less and less possible). Making the right call at the start of the design process can mean the difference between buying at the bottom of the market and losing your shirt.

In order to make strategic trades that are truly beneficial, you need to understand your resources in terms of the assets and debts. Unfortunately, the system portfolio is not yet a slick, prepared prospectus that comes in the mail. An accurate, precise understanding of it takes a lot of time (aka money). Estimation can suffice for certain areas; others require in-depth investigation (profiling).

Your system’s assets are somewhat fungible. For instance, you can trade RAM for processor cycles or code space (you will need some development time to facilitate the transaction). Sometimes you don’t have to trade anything and spending more development time will reduce your debt load. However, the total amount of any resource is fixed, and a resource can become so scarce that any amount of development time (and assets) will fail to produce any more of it.

As you consider different optimization strategies, consider as well the high opportunity cost that comes with investing your assets. Buying futures is almost always cheaper than buying stock. Similarly, selecting resources at the design phase is cheaper than waiting until the last minute and realizing you are seriously in debt with no easy way to recover.

(Thanks to Jane Muschenetz for helping with the business side of this analogy!)

Lookup tables

Nothing is ever free in optimization. Even turning on the compiler optimizations takes development time because the debugger no longer works as it did. When you step through line by line, the debugger won’t show you most of the variables (the ones the compiler removed), and it jumps around (because the compiler rearranged the assembly instructions).

Loop unrolling trades code space for speed. Using a macro instead of a function also trades code space for speed. These trades go either way, so if you need more code space, you may want to roll up any loops and eliminate macros.

There is another trade-off between cycles and code space (or RAM) that goes beyond macros: lookup tables. These store the values needed for operations on a particular parameter. When the running code needs the information, instead of performing calculations, it finds the result in the table, often with a simple index.

For a simple example, if you need to transform the 128 ASCII characters into their Unicode equivalents, you can put them in a table to look up the result. Lookup tables go far beyond this, though. Many complex algorithms (e.g., encryption’s AES) have two implementations: a fast, large one that uses lookup tables and a small, slow one that computes the values it needs. (Generating lookup tables to reduce math overhead is covered in gory detail in Chapter 9.)

Lookup tables can be code-based (where the values are calculated at or before compilation) or RAM-based. The latter are useful if your RAM access is fast (fewer wait states) or you have plenty of RAM but not enough code space (calculate the table on initialization). They can also be useful if your lookup values need to change when circumstances change (e.g., a networking router’s lookup tables change when a different protocol is needed).

Tip

The compiler may create a lookup table when you use a switch statement in place of an if-else. Looking at your assembly code will tell you whether it has (and whether it is a savings to you).

Coding in assembly language

Looking at your assembly code is an important part of optimizing. Even if you don’t start out knowing the meaning of the instructions in your assembly language, following along in the debugger for critical functions will help you figure out what the processor is really doing. Once you understand that, you may be able to tweak your high-level language code to help the compiler do something more efficiently.

That said, your time is valuable. Don’t program in assembly language. Really, it isn’t a good idea. The resulting code is usually a mess and almost always easier to redo from scratch than it is to maintain, even by the original programmer. That isn’t to say it is easy to rewrite, just that it can be impossible to read. Maintenance of assembly code tends to be in the form of major refactoring (aka rewriting it all and fixing the old bugs, but indubitably writing new ones).

However, if you have a function that really, really needs to be super-fast and the compiler is clearly not doing all it can, well, programming in assembly can be kind of fun, in a furtive playing-Tetris-at-work sort of way.

Start off with the high-level code. Understand the assembly generated by the compiler. Next, turn on optimizations and really understand the new assembly generated by the compiler. You are smarter than the compiler, and you have more system knowledge. But the compiler almost certainly knows the assembly language better than you do. Let it show you what it thinks is important before you start tweaking the assembly that the compiler output.

That is the big secret: don’t start with a blank slate. Start with the assembly your compiler gives you and go from there. Paste the equivalent high-level code into the assembly code as comments so that your future self can remember the goal of each section.

Summary

I can’t provide a comprehensive list of optimization techniques. They depend on your processor and your compiler. Even more importantly, they depend on your requirements and your available resources. I can say that if you’ve gotten to the end of the project and are looking for cheap ways to reduce your code by 40%, you have seriously misjudged the capacity of your hardware resources. But I’ve made that mistake, too.

Optimization starts with a good design, preferably one with a little extra overhead just in case the future features turn out to be larger, slower, and more RAM-intensive than you’d planned. Even at the end of the project, you need a few spare resources to fix bugs and respond to hardware changes.

Warning

In medical and safety-related products, that margin needs to be larger. Recompiling the code is a paperwork-intensive process, even for a minor one-line change. If you end up changing 50 lines because you need one more byte of RAM or just a few more processor cycles to implement the change, the paperwork and testing grows exponentially.

Although there is some fun to be had in reducing the resources as far as possible, you’ll get diminishing returns. The first 10% of improvement shouldn’t be too difficult, but the next 10% will probably take twice as long (and so on). Start with a goal, and don’t optimize much beyond there.

There are trade-offs in optimization. You can share resources between subsystems, but every time you do that, your product becomes a little less robust and a little more fragile for the next person to modify. There is something incredibly embarrassing about handing a project off to another team and explaining that they should never change subsystem X, because it will change the very precise timing of subsystem Y.

A better way of utilizing development time is to look for ways to optimize using well-understood algorithms. Keep reading books (and blogs) for ways to solve common (and not-so-common) problems. A proven algorithm is well worth the time invested. Instead of inventing the wheel all over again, you can refine someone else’s.

One last point: many times, the tuning of the system is left to the end of the development cycle, even after the first testing has completed. I’ve heard that this is done on the theory that the features should work before they get tuned. The process of keeping the system within its resources should be planned for and maintained during system development. Introducing complex changes right before you ship a product will lead to an unstable system. Balance the risk of making a premature optimization (e.g., reading all of your assembly list code looking for ways the compiler could have reduced a single instruction) against the benefit of making an optimization early so it can be tested thoroughly and have an impact on other design considerations.

Further Reading

Although I have attempted to give you some tools to take care of the largest problems, there are almost always ways to squeak out a little bit more. For more optimization techniques, look to your processor vendor’s application notes and your compiler manual. Atmel has a particularly good application note for one of their 8-bit processors: AVR035: Efficient C Coding for AVR.

Interview Question: Reverse Bits in a Byte

Start by reversing the bits in a byte using limited memory. Once you’ve finished, modify the code to be as fast as possible (but without the memory limitation).

In an interview setting, I don’t mind if the interviewee starts out slow with a relatively dumb solution. Optimization should take a backseat to correctness:

uint8_t SwapBitsInByte(uint8_t input) {
  uint8_t output=0;
  for (uint8_t i = 0; i < 8; i++) { // for every bit in the byte
    if (input & (1 << i)) { // need to set the bit in the output
      output |= 1 << (7-i);
    }
  }
}

Generally, I don’t look for syntax during interviews (that is what compilers are for). However, in this problem, parentheses are important because the bit-shift operator has lower precedence than addition or subtraction. If the interviewee has missed this, the reminder will probably come in the form of “walk through this test case and tell me the value of each variable at every step.”

Ideally, the programmer should check her own code, running through it with a couple of values to make sure that following the instructions works. Like mental unit tests, I like to see the interviewee do this as she writes the code (or even before).

When I look at the solution, naming is important, for both function names and variable names. Comments are nice as long as they are useful (“// set the bit in the output” might be useful on an if statement, but it probably isn’t on the line that actually sets the bit).

Using variables to implement the problem is a good start to the solution, although there is a straightforward way to do it with one variable (by unrolling the loop). If the candidate mentions that both variables (and input parameter) are likely to be in registers, I can point out that the use of an extra register probably meant some other variable got pushed on to the stack. Despite the correction, the interviewee gets a lot of points for the observation.

As for the second part of the problem, I don’t know many people who have successfully worked out the problem in an interview situation. It is one of those tricks that you just need to know: use a lookup table. If you can allocate 256 bytes (in RAM memory or in code space, whichever is faster), to quickly find the reverse of a byte, all you need to do is use the value of the byte as a lookup into the array of bytes:

uint8_t SwapBitInByte(uint8_t input) {
   const uint8_t lookup_table[256] = {0x00, 0x80, 0x40, 0xC0... 0x7F, 0xFF};
   return lookup_table[input];
}

This technique is often used in embedded situations when the system has more code space than time. I hope that by this point the interviewee has mentioned that she’d look up the algorithm to make sure she had a reasonably optimal solution for the design constraints.

There is a more optimal solution to the first part of the question. Instead of reversing the bits one after another using a temporary variable, the code can reverse the bits pairwise, then reverse the pairs, then reverse the nibbles, all without a temporary variable (and I suspect one fewer register can be used):

uint8_t SwapBitInByte(uint8_t val) {
  val = ( val & 0x55 ) << 1  | ( val & 0xAA ) >> 1;
  val = ( val & 0x33 ) << 2  | ( val & 0xCC ) >> 2;
  val = ( val & 0x0F ) << 4  | ( val & 0xF0 ) >> 4;
  return val;
}

I would not expect an interview candidate to come up with this on her feet. I had to adapt it from Hacker’s Delight by Henry S. Warren. On the other hand, now I’m tempted to make future interviewees explain what the code does (and how to make it faster, hoping they will come up with a lookup table).

Chapter 9. Math

When we looked at trading resources in the last chapter, you had to choose between RAM, code space, and processing cycles. Trading resources goes only so far. Sometimes you need to make your code go faster. Not knowing what you’ll need for your system, I can still guess that you’ll need to implement some math (because that is where processors excel).

The less your system does, the fewer resources it needs to do them. Sometimes we confuse accuracy, which is important, with precision, which can go too far (see ). If you can quantify the range of data you expect and your error budget, there are some useful methods to reduce unnecessary precision for all sorts of algorithms, thereby saving RAM and processing cycles.

Accuracy Versus Precision

Accuracy is a measure of how correct you are. Precision is how many digits you show in your answer. Both have degrees to them; one answer can be more accurate and/or more precise than another. A really accurate answer knows its limitations. For example, what is the mass of an electron?

Mass of an electron | Is it precise? | Is it accurate?
12.12345124 kg | Stupidly so | No
10–30 kg | No | Far more so than 12 kg, and accurate enough for most conversations
9.109 x 10–31 kg | Yes | Even more so
9.1090000001 x 10–31 kg | Uselessly so | Same accuracy as the previous answer
(9.1092 +/– 0.0002) x 10–31 kg | Yes | Yes, best answer yet

Precision can be noise. If the extra bits of precision don’t mean anything, they can needlessly complicate your system, use precious RAM, and waste processor cycles. As you implement complex algorithms and perform mathematics on your data, you need to determine just how much precision is required to maintain sufficient accuracy for your product.

Identifying Fast and Slow Operations

Optimizing your system to do its mathematical operations quickly requires you to understand a bit more about your compiler and processor. Once you understand which operations occur quickly (and which ones take up one line of code but compile to use two libraries and an absurd amount of processing), you’ll have a basis for optimizing your system.

So, addition and subtraction are fast. Shifting bits is usually fast. Division is very slow. Anything with floating-point is dead slow, unless you have a floating point unit (FPU).

What about multiplication? On a DSP, it is fast: multiply and add together form a single instruction (MAC, for multiply-accumulate). On a non-DSP (e.g., an ARM or your PC), multiplication falls between addition and division, but closer to addition.

Warning

For many processors, division isn’t built into the hardware, unlike the other arithmetic operators. On those processors, the divide operator calls a library function—usually a pretty large one, as you can tell by looking in your map file.

Just because an operation can be described in one line of code doesn’t mean it takes a short time to run on the processor. Chapter 6 mentioned that modulo math was useful for circular buffers. It is also useful if you want to do something on every Nth pass of a loop:

for (i=0; i<100; i++) {
  if ((i%10) == 0) {
    printf(“%d percent done.”, i);
  }
}

However, that %10 is actually a hidden division, a relatively costly instruction (though nowhere near the processing cost of printf—but that is another story). If you make the interval a power of two, you can replace the modulo math with a cheap (single-instruction) bitwise logic operation (take a look at Representing Signed Numbers for the reason why):

for (i=0; i<100; i++) {
  if (!(i & 0x07)) { // this will print out every 8th pass
    printf(“%d percent done.”, i);
  }
}

In general, shifting bits takes a single processor cycle when the number of bits to shift is a constant. It is a cheap way to do division and multiplication if you can keep your constants to a power of two so that an integer shift can replace the arithmetic operation. (We discuss this one later in a lot more detail.)

In many processors, addition and subtraction take a single processor cycle. However, if they don’t, addition of unsigned variables is often faster than addition or subtraction with signed variables, as the processor doesn’t have to keep track of the sign bit.

Using constants is faster than using variables. But they need to be real constants (#define in the C language), not constant variables (const), which most compilers load into registers instead of putting the number in the assembly file. Now you know why embedded system programmers are still addicted to ugly #define statements.

There are a ton of these rules, but you’ll learn them with time. Once you get a feel for the details of your processor and compiler, much of it is common sense, considering what your system really needs and how to implement it. Let’s take a look at a common system example to see how different choices can change an algorithm.

Taking an Average

For many signals, you’ll need to calculate the average (aka mean) and standard deviation. Sometimes that is your output. Sometimes it is just a sanity check to make sure your signal hasn’t gotten overly corrupted by noise.

With a rolling N-point average, you calculate the average of the last N points. You don’t even need to add them all up and divide each time; you can add the new point and subtract the oldest (like a FIFO):

newAverage = lastAverage + (newSample/length) - (oldestSample/length);

However, you have to perform division at every step. And if you are averaging over a large number of small-valued samples, this division may truncate the result and give you an inaccurate average (take a look at the sidebar The Downside of Integer Math).

The other downside to a rolling average is that you need to keep the sample buffer intact until the oldest sample leaves the average. If the average is calculated over a large number of samples, this could be a big RAM buffer.

Instead of jumping into optimization, take a step back and consider the needs of the system. Do you need a new average value at every time step? Or can you use the same value for a period of time? If the latter, you might be able to implement a block average instead of a rolling average. In a block average, you accumulate a number of samples until you need the average, then restart the calculation:

struct sAve {
  int32_t blockSum;
  uint16_t numSamples;
};
int16_t AddSampleToAverage(struct sAve *ave, int16_t newSample) {
  ave->blockSum += newSample;
  ave->numSamples++;
}
int16_t GetAverage(struct sAve *ave) {
  int16_t average = ave.blockSum/ave->numSamples;
  // get ready for the next block
  ave->blockSum = 0; ave->numSamples = 0;
  return average;
}

Figure 9-1 shows a set of samples with the rolling average (over five samples) and the block average (also over five samples). Note that each time the block average changes, it is the same as the rolling average result at that point. The rolling average changes faster, so it is a more accurate representation of the data during the other four time steps. If you need the average only at every fifth (or hundredth) time step, the block average is just as accurate as the rolling average, but at a much lower cost in terms of RAM and processor cycles.

Figure 9-1. Rolling average versus block average

The Downside of Integer Math

What is 4/4? 1. Excellent. What is 5/4? Um, according to the processor, that is still 1. As are 6/4 and 7/4. Integer division truncates the number, like running the floor() function on a floating-point number. It can lead to serious trouble.

For example, what if you optimized the rolling average to avoid an unnecessary division:

newAverage = lastAverage + ((newSample- oldestSample)/length);

If your samples are all about the same, your average will be horribly incorrect. Figure 9-2 shows three different forms of the rolling average. First is the floating-point version, which tracks the signal reasonably well (though it is slightly delayed, of course). Next is the sample implementation, but with integer truncation, as the oldest and newest sample are divided individually by the length. It tracks reasonably well, but you can see some inaccuracies. Finally comes the implementation I just showed with truncation. It saves a costly division step, but the result is junk.

You want to save resources but not generate junk.

Figure 9-2. Truncation failure in rolling average

The solution is easy to state: large values should be divided by much smaller ones; they shouldn’t be similar in magnitude.

Implementing this solution is more difficult than stating it. If you know that you are going to have the problem, you can choose to increase the magnitude of your numerator (that is, multiply every sample by some fixed constant). To take the average, multiplying by any constant greater than the number of samples in the average will suffice. That would be fine in our case. The resulting average will be off by that amount, so if you need to retrieve the actual value, you will need to divide by the multiplication factor later. If you’re just comparing values, you never need to divide, because their relative values are correct.

You can choose any constant, as long as you don’t multiply the samples to the point where they are too big to fit in their variables. While division can cause underflow, multiplication can cause overflow, which also results in odd behavior. That means you need to know the range you are expecting to receive. Using your knowledge of your system to optimize it is very useful, but it can lead to a brittle system where seemingly minor changes (e.g., the input range changing by 10%) lead to errors.

Thus, the downside to integer math is that you need to know and depend upon a range of input values so that you can make sure your accuracy is not degraded by the lack of precision.

Use an Existing Algorithm

Now that you have a bit of a feel for what is fast (addition, shifting, and maybe multiplication), I’m going to share the most important thing to do when you need to implement an algorithm using minimal resources: look it up. If it is a standard algorithm, search online or pull out a numerical recipes book. If someone has already put in the time and effort of explaining how to reduce the processing cycles or the RAM usage, use their work. Make sure you pay attention to copyrights, but most instructional materials exist so someone will use them. There is a lot to be found online about optimizing some algorithms, but some of the trusted resources described in Further Reading are worth having on your bookshelf when you need them.

There are probably several ways to implement an algorithm, but only one or two will save the resources you need to preserve. For example, let’s look at standard deviation.

The standard deviation (s) is how far the samples in a set vary from the average of the same set. The standard deviation is calculated using each sample in the group (xi), the mean or average of the group (x-bar), and the number of samples in the set (N), as shown in the equations in Figure 9-3 (though it is unintuitive, they are equivalent).

Figure 9-3. Two equivalent equations for calculation of standard deviation

How to make this friendly for your embedded system? Well, take the square root piece first. The simple answer is to not perform the square root. Although it changes the value of the result, it doesn’t change the information it gives you about how much the samples vary. Since many people do this, the un-square-rooted standard deviation is called the variance.[9]

(Think of how many processing cycles we’ve already saved by redefining the goal!)

We can start by implementing the variance pretty much as it is in the equation:

uint16 GetVariance(int16_t* samples, uint16_t numSamples, int16_t mean) {
  uint32_t sumSquares = 0;
  int32_t tmp;
  uint32_t i = numSamples;

  while (i--) {
    tmp = *samples - mean;
    samples++;
    sumSquares += tmp*tmp;
  }
  return (sumSquares/(numSamples-1));
}

Note that the mean was passed into the function. It has already been calculated for this block. Another piece of code has already looked at each of the samples, so this function is a second pass through the data. In addition to the inefficiency of running through the loop twice, you have to keep all of the data around in a buffer, which is probably a waste of RAM.

However, like the block average, you can calculate variance as you go along, which is shown more clearly by the second equation in Figure 9-3. Note that the block average happens for free in this variance calculation:

struct sVar {
  int32_t sum;
  uint64_t sumSquares;
  uint16_t numSamples;
};
void AddSampleToVariance (stuct sVar *var, int16_t newSample) {
  var->sum += newSample;
  var->sumSquares += newSample*newSample;
  var->numSamples++;
}
uint16_t GetVariance(struct sVar *var, int16_t *average) {
  uint16_t variance;
  // This method also outputs average
  *average = var->sum/var->numSamples;
  variance = (var->sumSquares - (var->sum * (*average)))
                       /(var->numSamples-1);
  // get ready for the next block
  var->sum = 0; var->numSamples = 0; var->sumSquares = 0;
  return variance;
}

There is one problem with that implementation: the variable sumSquares can get quite large (even sum can get pretty large when you have many samples). Instead of the signed 16-bit samples in the code, let’s say the function used signed 8-bit samples. If you have two samples, each with a value of 127, your sumSquares would be 32,258, almost 15 bits. By the time you had five samples at the maximum value, you’d need 17 bits to hold sumSquares, which means moving up to a 32-bit variable. If you are using 16-bit values, the sumSquares needs to be a 64-bit variable. Using large variables takes RAM and processing cycles.

That wouldn’t happen in the two-pass implementation, because the subtraction of mean from each sample keeps the sum of squares reasonably small. However, there is a way to do it without using a large intermediate variable and without taking two passes at the data:

struct sVar {
  int16_t mean;
  int32_t M2;
  uint16_t numSamples;
};
void AddSampleToVariance(struct sVar *var, int16_t newSample) {
  int16_t delta = newSample - var->mean;
  var->numSamples++;
  var->mean += delta/var->numSamples;
  var->M2 += delta * (newSample - var->mean); // uses the new mean
}
uint16_t GetVariance(struct sVar *var, int16_t *average) {
  uint16_t variance = var->M2/var->numSamples;
  *average = var->mean; // running average already calculated
  // get ready for the next block
  var->numSamples =0; var->mean = 0; var->M2 = 0;
  return variance;
}

I don’t think I would have come up with that on my own, especially when I was focused on building a way to see how much the samples in my system varied from the mean. Even Donald E. Knuth credits an article by another author (B.P. Welford).

Let’s look at the code in more detail. To store the intermediate variable for the variance, you only need a variable that is double the size of your sample (so if your sample is 8 bits, M2 can be 16 bits). This method lets you use a variable that is the same size as your data to store the mean (as opposed to the earlier sum variable, which depended on the number of samples). However, the code is less legible, and if you change the order of the lines in AddSampleToVariance, it will break. It also does a division operation on every pass, and worse, the division to calculate mean can cause problems because delta is likely to be small.

Yes, everything has consequences. Sometimes trying to save processor cycles is like trying to hold on tightly to a balloon; the more you grip it, the more some other area will poke out between your fingers. If you stick to a well-understood algorithm, there is a good chance that the failure analysis will be done for you already.

Tip

If you really need to know the standard deviation (instead of the variance), there are many methods of approximating the square root. More than 10 are listed on the relevant Wikipedia page. Choose one or two, then use your profiling tools to compare them with your compiler’s math library.

[9] If you aren’t familiar with the definition of variance (or any mathematical operation), look it up. Wikipedia is a great place to start. There is also a page covering different implementation techniques, including those described here.

Designing and Modifying Algorithms

Sometimes your algorithm goes beyond simple math and yet isn’t something you find as a recipe in a book. There are many tips for implementing different processor-intensive operations. If you learn some of the building blocks, you can optimize for yourself. As with cooking, the more recipes you know, the more likely your modifications will succeed.

Factor Polynomials

Say you have something that can be implemented as:

y = A*x + B*x + C*x;

That is three multiplications and two additions. If you factor your polynomial, you can do it in two additions and one multiplication:

y = (A + B + C)*x;

Much better. Let’s take a nonlinear example and use factoring to simplify the processing load. It is a matter of turning the polynomial inside out:

A*x³+B*x²+Cx  ==>  ((Ax+B)x+C)x

On the left, if you had cubed x and multiplied it by A, and so on, you would have done nine multiplications and two additions. If you go the other way, the same answer arrives in three multiplications and two additions. How you frame a polynomial is very important to minimizing processing cycles.[10]

Taylor Series

Polynomials are important because they constitute one way to model complex steps in your algorithm. A Taylor series represents a function as a sum of infinite terms. It works for most functions, including trigonometric (sine, cosine, tangent, arcsine, etc.), more difficult polynomials (x–1), square root, and logarithms. As more terms in the infinite series are added, the result becomes more precise (and more accurate). And you can turn that around: you don’t need infinitely many terms to generate an accurate answer for your function.

Creating a Taylor series for a generic polynomial is a fair bit of math, beyond the scope of this book (much more than the slight trickery of Horner’s scheme). However, you can look algorithms up easily. For example, the sine function can be replaced with its Taylor series expansion (see Figure 9-4).

Figure 9-4. Sine function in Taylor series, rearranged via Horner’s scheme

Note

All angles in this section are represented in radians. Remember that π radians = 180°.

For the Taylor series with four terms, the error for the argument values between π and –π is small (0.000003). If this error is still too large, you can add the next term in the Taylor series for more accuracy (+x⁹/9!). Or if you can deal with more error than that (particularly at the edges of +/–π), you can use fewer terms.

Tip

Physicists often stop at the first term, approximating sin(x)=x. The closer x is to 0, the better this works.

As before, to really optimize, you need to understand your input and the acceptable error of your system. If your input is in the range of –0.2π to 0.2π and you need an accuracy of 10%, you can use the physicists’ approximation. Or if you need an error of less than 1%, you can use two terms of the Taylor expansion. A Taylor series lets you balance the processing power with the accuracy required by your application (see Figure 9-5).

Figure 9-5. Graph showing accuracy of different numbers of terms for the sine Taylor series

Because they use just multiplications and additions, Taylor series are relatively light in processor intensity. Putting in constants and using Horner’s method, we can implement four terms of sine (in floating point) as:

xSq = x*x;
sinX = x * (1 - xSq * (INVERSE_THREE_FACTORIAL + xSq *
            (INVERSE_FIVE_FACTORIAL - INVERSE_SEVEN_FACTORIAL * xSq)));

// or taking it apart and writing multiple steps, stating with
// the end and moving toward the front:
tmp = - INVERSE_SEVEN_FACTORIAL * xSq;
tmp = xSq * (INVERSE_FIVE_FACTORIAL + tmp);
tmp = xSq * (-INVERSE_THREE_FACTORIAL + tmp);
sinX = x * (1 - tmp);

Why does this have to be floating point? Because INVERSE_THREE_FACT and the other constants need to be between zero and one. However, there are ways to avoid this problem.

Dividing by a Constant

Division is a relatively costly arithmetic step. But when you divide by a constant, there are ways of cheating. Using sine’s Taylor series for an example, let’s say you need to divide by 3!. That is 3*2*1 = 6. Well, that won’t be too difficult:

uint16_t DivideThreeFactorial(uint16_t input) {
  const uint16_t denominator = 6;
  return input/denominator;
}

For now, let’s not worry about x being between +/–π. Instead we’ll take a larger number to play with. If the input is 245, the result will be 40 in integer math (40.833 in floating point). I’m not yet concerned about the truncation here, just about the division operation. Is there a way to make it faster?

A shift is much faster. Sadly, six is not a power of two. However, there are other ways of formulating the number 1/6, such as 2/12, etc. What we want is a multiplier/power-of-two that is equivalent to 1/6 (0.166667), at least within some small amount of error. Then we can multiply by the numerator by the input and shift the result to finish the division. We’ll trade division for a multiplication and shift. Table 9-1 shows some multipliers with their power-of-two divisors and resultant errors.

Table 9-1. Approximating 1/6 with a power-of-two divisor

Multiplier | Divisor | Equivalent shift | Result | % Error
1 | 6 | None | 0.166666667 | 0
3 | 16 | 4 | 0.1875 | 12.5
5 | 23 | 5 | 0.15625 | 6.2
11 | 64 | 6 | 0.171875 | 3.1
21 | 128 | 7 | 0.164063 | 1.5
43 | 256 | 8 | 0.167969 | 0.78
85 | 512 | 9 | 0.166016 | 0.39
171 | 1024 | 10 | 0.166992 | 0.19
341 | 2048 | 11 | 0.166504 | 0.09
683 | 4096 | 12 | 0.166748 | 0.04

Note that the errors in the table are halved for every additional shift. So if you implement a function to divide by 3!, it could look like this:

#define DIVIDE_THREE_FACT_MULT 171
#define DIVIDE_THREE_FACT_SHIFT 10

int16_t DivideThreeFactorial(int16_t input) {
  int32_t tmp = input* DIVIDE_THREE_FACT_MULT;
  return (tmp >> DIVIDE_THREE_FACT_SHIFT);
}

Note that the input and output are 16 bits, but a 32-bit variable is necessary to hold the multiplication result. Even using a larger temporary variable, this function is cheaper than division. Probably.

Tip

If you don’t do the final shift, the result can be more precise than the truncated division. You’ll need to remember that the value is multiplied by 1,024 when you use it.

However, there is a big downside to this implementation: you need to know the divisor ahead of time to construct the function. Although it is useful any time you need to divide by a constant, it isn’t generic enough to get rid of all division.

Scaling the Input

The sine function takes an input from –π to π and outputs a value from 0 to 1. That is all pretty difficult to do if you are avoiding floating-point numbers. However, you can multiply everything by a constant to get more granularity than an integer implements. For example, if you want to get rid of floating-point numbers in the sine function, you can change the input to the range +/–(1,024*π) and compensate for getting an output that is too great by a factor of 1,024.

For mathematical operations where f(Ax) = Af(x), optimization is as simple as using the scaled input. Even for functions that are f(Ax) = f(A)f(x), removing the scalar is straightforward: divide by f(A). However, sine is more complex than that; there is no easy way to remove a scalar.

Instead, we’ll need to make sure that each term of the Taylor expansion is multiplied by 1,024 once and only once. That means any time x is multiplied by x, we need to divide by 1,024 so that our scale factor doesn’t get squared along with the input value:

xSq = (x*x) >> 10;  // right-shift by 10 is equal to divide by 1,024
tmp = - DivideSevenFactorial(xSq);
tmp = (xSq * (INVERSE_FIVE_FACTORIAL + tmp)) >> 10;
tmp = (xSq * (-INVERSE_THREE_FACTORIAL + tmp)) >> 10;
sinX = x - ((x * tmp)>>10);

The great news here is that with the scaling, the division at some of the steps becomes easier. Everything must be multiplied, including the constants. Instead of multiplying by 1/3! (essentially dividing by 6), we are multiplying by 1,024/3!, which is approximately 171. Since 5! is 120, there is more error when rounding 1,024/120 to 8 (8.533). The last term (7! = 5,040) is still large, so it requires some actual division, though still dividing by a constant. Because it is a small constant, there is some error associated with it, as shown in Figure 9-6.

Figure 9-6. Errors in scaling the input with a Taylor series (0 to π)

There are ways to alleviate these errors (such as scaling by a larger number). As you are implementing your changes, you will need to understand where error comes into the system. As with profiling, the bulk of your time should be spent on the sources of the largest error, not optimizing a minor contributor.

These ideas of scaling and division by shifting are common in fixed-point math. They are going to help us create a method to avoid floating-point numbers entirely. Before we go there, let’s look at a very popular way to save processor cycles.

Lookup Tables

As mentioned in Chapter 8 (and expounded on in the interview question), lookup tables are blazingly fast and just require a bit of code space.

However, to use a lookup table, you need to know the range of the input and the acceptable error. In Taylor expansions, these considerations drive the number of terms you calculate. In lookup tables, they determine the number of entries in your table.

Implicit input

Each entry in the lookup table requires two pieces of information: the input (x) and the output (y). However, not all lookup tables require both to be in the table. With implicit input lookup tables, the position in the array indicates the input value. For example, here is a sine lookup table with an input in milliradians (–π/1,000 to +π/1,000) and an output also scaled by 1,000. (Lookup tables reduce the need for shifts and therefore power-of-two operations, often leading to more human-sensible scaling.)

const int16_t sinLookup[] = {
  -58,   // x =  -3200
  -335,  // x =  -2800
  -676,  // x =  -2400
  -910,  // x =  -2000
  -1000, // x =  -1600
  -933,  // x =  -1200
  -718,  // x =  -800
  -390,  // x =  -400
   0,    // x =  0
   389,  // x =  400
   717,  // x =  800
   932,  // x =  1200
   999,  // x =  1600
   909,  // x =  2000
   675,  // x =  2400
   334,  // x =  2800
  -59    // x =  3200
};

Where the Taylor expansion covered all possible inputs (even though I showed only 0 to π), this table covers only the range –π to π. If you put in 2π for the Taylor expansion, it will work. That is not true for lookup tables; you have to know the range of your input.

To use the table, you’ll need to calculate the index that matches your input. To do that, subtract the lowest value in the table and divide by the step size of the table:

uint8_t index = (x - (-3200))/400;
y = sinLookup[index];

Wait a minute, why are we killing ourselves optimizing a sine function if we are just going to do a division now? Right. The step size of the table not only changes the precision of your table, it can also change your processing requirements. The non-power-of-two step size was a bad choice. If we’d chosen 512, the table would be only a little bit smaller (less precise), but the processing would have been shorter:

uint8_t index = (x - (-3145))>>9; // use #defs, not magic numbers!
y = sinLookup[index];

Given only 13 entries, the table is not very precise, but will the results be accurate? Figure 9-7 shows points and where they’ll land in this calculation. By default, the lookup results are offset (the triangles in the figure). The table would be more accurate if the lookup value covered the center of the range, as shown with the Xs. Either way, we get a stair-step approximation, but we can center the stairs on the goal output to reduce the error.

Figure 9-7. Comparing sine offset lookup table output with half-step centered lookup table output

The table itself doesn’t need to change to make this happen, just the indexing. In effect, we need to add half a step when calculating the index:

uint8_t index = (x - (-3145 - 256))>>9;
y = sinLookup[index];

Tip

This idea of adding a half step to center the result is prevalent in almost every well-implemented lookup table.

Once you change the valid range, I’d recommend making the comments describe the range as well as the value:

#define BASE_SIN_LOOKUP (-3124 - 256)
#define SHIFT_SIN_LOOKUP 9
const int16_t sinLookup[] = {
  3,     // x @ -3145, for range -3401  to  -2888
  -487,  // x @ -2633, for range -2889  to  -2376
  -853,  // x @ -2121, for range -2377  to  -1864
  -1000, // x @ -1609, for range -1865  to  -1352
  -890,  // x @ -1097, for range -1353  to  -840
  -553,  // x @ -585,  for range -841   to  -328
  -73,   // x @ -73,   for range -329   to  182
  425,   // x @ 493,   for range 183    to  695
  813,   // x @ 951,   for range 695    to  1207
  994,   // x @ 1463,  for range 1207   to  1719
  919,   // x @ 1975,  for range 1719   to  2231
  608,   // x @ 2487,  for range 2231   to  2743
  142,   // x @ 2999,  for range 2743   to  3255
};

Note that this new table has a step size of 512 and 13 entries. The values look like Figure 9-8. This table covers the expected input (–π to +π) and a little more besides. However, if there is a possibility of getting data outside the table’s range, add code to verify the inputs. After all, if your input is 2π, your index will be 19, outside the range of the table. However, accessing sinLookup[19] may work in C/C++ but give you a completely incorrect result (or it may just crash the system).

Figure 9-8. Points in the sine lookup table

Linear interpolation

In the lookup table, even with the center biasing, there are times when your result should be more accurate than a single number covering a range of inputs. In that case, linear interpolation may be the way to go.

Tip

You can use other interpolation methods. Consider the trade-offs between the increased computational requirements of a polynomial fit against the code space of putting in a larger table.

As shown in Figure 9-9, linear interpolation isn’t hard; it’s just a bit of algebra you might not have used for a while. You’ll need two values from the lookup table: the one below the input you have and the one above. In the figure, the goal is to find the output for a particular value of x, so the two closest points are used. Since the input and output values are linked, for interpolation, I find it simpler to treat them as points instead of individual values:

struct sPoint {
  int16_t x;
  int16_t y
};

Figure 9-9. Linear interpolation

The linear interpolation code is simple math; just be sure to keep the bits from overflowing during the multiplication:

// This linear interpolation code does:
// y = p0.y + ((x-p0.x)*(p1.y-p0.y))/(p1.x-p0.x);
// but in a bit-safe way.
int16_t Interpolate(struct sPoint p0, struct sPoint p1, int16_t x) {
  int16_t y;
  int32_t tmp; // start and end with int16s, but can need a larger intermediate
  tmp = (x - p0.x);
  tmp *= (p1.y - p0.y);
  tmp /= (p1.x - p0.x);
  y = p0.y + tmp; // now safe to go back to 16 bits
  return (y);
}

This method works even when the input is not between the two points. If you end up with an input that is just outside your table, you can use the last two points in the table to interpolate past the end (though possibly with a degradation of accuracy).

Note that a division is used in linear interpolation. You are trading some processor cycles to get more accuracy with a smaller table size (code space). You can avoid division by making the size of the x-steps in the lookup table a power of two (as I did with the last sine lookup table I showed). The divisor is then a power of two, so the divide can become a shift.

While the table lookup depends on the table itself, interpolation is a step away from that. When you optimize away the division, your interpolation function depends on the table data. If you have multiple tables, you’ll need multiple interpolate functions (or a parameter to describe the shift value). Be careful, as it will make your code less readable, and linking data and code will make it more brittle. If you need the processing cycles, it may be the best way to conserve them. But it makes for awfully tangled code.

Explicit input in the table

For some functions, you can create a smaller or more accurate table by allowing variable step sizes, interpolating between them. The sine function is very linear toward the center, as seen in Figure 9-8. We could easily take out the three centermost points and let linear interpolation handle it (which is why sin(x) = x for small x is so popular). Even the outermost edges could have a sample removed; it is those tricky curves that need more points.

Sometimes a lookup table has a variable step size to decrease errors in certain areas. Linear interpolation can be used between the areas, as shown in Figure 9-10.

Figure 9-10. Using linear interpolation between points on an explicit lookup table

The lookup table is now nearly double in size because we have to put in the start of the covered range as well as the value. It takes up more code space, but we gain accuracy in the areas we need it most:

struct sPoint sinLookup[] ={
  { -3145 , 3 },
  { -2121 , -853 },
  { -1865 , -958 },
  { -1609 , -1000 },
  { -1353 , -977 },
  { -1097 , -890 },
  { 951 , 813 },
  { 1207 , 934 },
  { 1463 , 994 },
  { 1719 , 989 },
  { 1975 , 919 },
  { 2999 , 142 }
};

Tip

An explicit input lookup table is often used if you have to build the table from the environment, such as when calibrating a sensor for temperature effects. The x value would be the temperature read via a thermistor (aka temperature sensor), and the y value would be the offset from the expected value.

Unfortunately, using an explicit table means you need to search through the entries to find the correct input range. As long as the table is in order, the search is straightforward. The goal is to find the index into the table with the closest x value without going above the best value:

int SeachLookupTable(int32_t target, struct sPoint const *table, int tableSize) {
  int i;
  int bestIndex = 0;

  for (i=0; i<tableSize; i++) {
    if (target > table[i].x) {
      bestIndex = i;
    } else {
      return bestIndex;
    }
  }
  return bestIndex;
}

You can pair this with interpolation:

  index = SeachLookupTable (x, sinLookup, sizeof(sinLookup));
  if (index+1 < sizeof(sinLookup)) {
    y = interpolate(x, sinLookup[index], sinLookup [index + 1]);
  } else {
    y = interpolate(x, sinLookup[index-1], sinLookup [index]);
  }

Unlike the implicit input, this method allows the input to exceed the range of the table. It will interpolate linearly using the first two points (if the input is less than the first entry in the table) or the last two points (if the input is greater than the last lookup point).

[10] This method is called Horner’s scheme and was developed by mathematician William George Horner.

Fake Floating-Point Numbers

Linear interpolation doesn’t always work on nonlinear curves. Even with a lookup table, if the curve is very twisty or the area of interest is very large, you may need to implement the math necessary to generate a solution.

For example, many sensors require some sort of compensation from parameters determined in manufacturing, such as temperature compensation. A temperature compensation equation might look like:

y = Ax² + Bx + C

The variable x is an input from a 10-bit ADC; A, B, and C are compensation parameters set in manufacturing as part of a calibration process; and y is the compensated sensor value. The compensation parameters are based on the sensor, so the minimum and maximum values come from the tolerances specified in the datasheet. Figure 9-11 shows some possible solutions given different compensation parameters. Even though these look relatively linear, trying to use linear interpolation may lead to too large of an error.

Figure 9-11. Temperature compensation on ADC input

However, the compensation parameters are floating-point numbers. Floating-point math is very expensive on fixed-point processors (which are the most common ones in embedded applications). Compared to integer math, floating-point math is dead slow. Each operation calls a library to implement the function—a very large library. Even if the floating-point math you need is very simple, the size may be significant. For example, I looked at the map as I added two numbers together. For a compiler optimized for space, modifying an integer add to use floating point took 532 bytes of code space (to add the library functions __aeabi_fadd and __aeabi_fsub, though, I used only the add function).

Warning

Standard input and output functions (such as printf and iostream) often include floating-point handling. Even when you don’t use floating-point numbers in your code, if you use these library functions, your code will still include the floating-point math libraries. Check your map file!

Unless you have a really good reason to do otherwise, avoid floating-point numbers like the plague. If you can’t avoid them, you can fake them. I’ve been creeping up on a method of doing that (divide by constant using shifts and scaling the input). In this section, though, we’ll go through it step by step, ending with a return to the temperature compensation example.

Rational Numbers

To get the idea, remember back to grade school, when there weren’t floating-point numbers in your life. Even before we could write 0.25, we knew what a quarter of a pie was and that it could be written as one over four (1/4). Any rational number can be written as a fractional number (a ratio of a numerator over a denominator). Even irrational numbers can be approximated using a large denominator to reduce error. (For example, π is often approximated with 22/7.)

If we stopped there, we wouldn’t get a big improvement (divides are slow). But we’ve already gotten around divides by shifting by a power of two. I didn’t give it a name before, but this technique is called binary scaling and forms the basis for our fake floating-point numbers. We’ll start with a 32-bit numerator and an 8-bit exponent. The exponent is the number of bits to shift to the left (for positive values) or to the right (for negative values).

We’ll start off by defining a structure to hold the values:

struct sFakeFloat {
  int32_t num;  // numerator
  int8_t shift; // right-shift values (use negative for left-shift)
}

The number held in this structure is represented by:

floatingPointValue = num / 2shift     (In terms of math)
floatingPointValue = num >> shift;    // in the actual code

And now for a few examples. Let’s start by going back to a quarter of a pie. The numerator is one, and the denominator is already a power of two:

struct sFakeFloat oneFourth = {1, 2};

A negative shift value changes the direction of the shift. We could rewrite 4 using this notation as well:

struct sFakeFloat four = {1, -2};
floatingPointValue = four.num >> shift ==> 1/(2⁽⁻²⁾) ==> 1 * 2² ==> 4

Fake floating point can also be used to work with numbers larger than you can store in a 32-bit value (or whatever the size of the numerator). The result will be less precise than a larger variable would be. For example, say we want to store ten billion and one (10,000,000,001). We can make the value fit into a signed 32-bit integer by dividing by 8:

struct sFakeFloat notTenBillionOne = { 1250000000, -3 };

We’ve lost the last digit, so the resulting value is only ten billion. But this is within one ten billionth of being correct, and that’s good enough for many applications.

Precision

In the structure shown in the previous section, suppose we used a 24-bit numerator and an 8-bit shift for the denominator. Although that would keep everything in a convenient 32-bit variable, it might decrease precision. We’ve already seen how we can lose digits for big numbers, but the same can happen with more normal-sized numbers as well.

For example, the number 12.345 could be represented as 49/4 with an error of 0.095. With a larger denominator shift value, more precision can be obtained. However, a too-large denominator will cause the numerator to overflow. See Table 9-2 for some of your options.

Table 9-2. Representing the number 12.345 using binary scaling

Numerator | Number of bits needed in numerator | Denominator shift values | Equivalent floating-point number | Error
12 | 4 | 0 | 12 | 0.345
25 | 5 | 1 | 12.5 | 0.155
99 | 7 | 3 | 12.375 | 0.030
395 | 9 | 5 | 12.34375 | 0.00125
12641 | 14 | 10 | 12.34472656 | 0.000273
12944671 | 24 | 20 | 12.34500027 | 2.67E–07
414229463 | 29 | 25 | 12.345 | 1.19E–09
1656917852 | 31 | 27 | 12.345 | 1.19E–09

So as you increase the shift in the denominator, you can decrease the error, but you’ll also need to increase the number of bits in the numerator. This brings us back to two questions: how much error can your system tolerate for this mathematical operation? And how large can you expect numbers to get with the multiplications and additions in your algorithm?

Addition (and Subtraction)

Let’s look at addition for a pair of fake floating-point variables. Thinking back to grade school days and fractional math, you need to make their denominators the same before trying to combine numbers. In this case, it means making the shift values the same, generally by left-shifting the smaller number before doing the addition.

The best way to explain this is to walk through an example. Here we’ll use a byte for the floating-point numerator. This can be useful when not a lot of range is needed. Looking back at Table 9-2 for 12.345, using 7 bits gave us an error of 0.030, or less than 1%. (Also, using an 8-bit numerator means you and I can imagine what happens at each stage, seeing overflows without trying to keep huge numbers in our heads.)

struct sFakeFloat {
  int8_t num;
  int8_t shift;
};

This example is going to show addition using only positive numbers, but the method is mostly the same for subtraction and for negative numbers. First, we need to set up our floating-point numbers with some values:

struct sFakeFloat a = { 99, 3 };  // 12.375, not quite 12.345
struct sFakeFloat b = {111, 5 }; // 3.46875, not quite 3.456
struct sFakeFloat result;        // 15.84375, close to ideal of 15.831

Next, find the least common denominator so we can add them together. Finding the least common denominator when all denominators are powers of two is very easy. In the following, if b.shift is greater than a.shift, you’ll get the left-shift you want. And if a.shift is greater than b.shift, conveniently enough, you’ll get a negative left-shift that turns into the right-shift you want. (The shift field is essentially a logarithm.)

int16_t tmp = a.num;
tmp = tmp << (b.shift - a.shift);

A larger temporary variable must be used, or the result may overflow and get truncated. Note that the numerator is not the only part that can overflow. The denominator (b.shift - a.shift) must be less than 8 bits (so an overflow may occur if one or both of them are negative). To avoid overflow, in addition to shifting the variable with the smaller denominator up, you may also need to shift the variable with the large denominator down (dividing by two and losing precision).

The sum of the numerators is kept in the temporary variable until we make sure it is small enough to fit into our result variable:

tmp = tmp + b.num;

At this point, the shift value is the larger of the two (b.shift = 5), and the temporary variable is 507, too large for an 8-bit variable. Until this temporary variable can safely fit back into the variable size we have, we’ll need to make the numerator smaller (by dividing by two):

result.shift = b.shift;
while (tmp > INT8_MAX || tmp < -INT8_MAX) {
   tmp = tmp >> 1;
   result.shift--;
}
result.num = tmp;

Finally, we have a result that describes the addition of two floating-point numbers.

The result’s numerator is 126, and the shift is 3, for a floating-point equivalent of 15.84 (plus some unnecessary precision). This is no different than if we’d added the two numbers in floating point (other than the initial precision error in representing each number).

Multiplication (and Division)

Multiplication is a little simpler than addition because you don’t have to make the denominators match. In fractional math, you just multiply the top and the bottom separately to get the answer (for instance, 2/3 * 5/4 = 10/12).

When you multiply two fake floating-point numbers, multiply the numerators safely, then multiply the denominators (sort of).

We again start by putting the numerator of one number in a larger temporary variable so we don’t get an overflow. Unlike addition, where we needed to check the size, we can be certain that the result of two 32-bit variables will be no more than 64 bits. Then multiply the temporary variable by the numerator of the second.

The shift value is not exactly the denominator; it is the exponent of the denominator:

denominator = 2shift;

A shift value is a logarithm (for instance, a bit position of 5 represents 2⁵, or 32.) So instead of multiplying the shift values, we only need to add them together. If the resulting numerator is greater than the number of bits we have available, shift to the right until it fits.

For this multiplication example, we’ll return to int32_t to hold the numerator:

struct sFakeFloat {
  int32_t num;
  int8_t shift;
} ;
struct sFakeFloat a = { 1656917852,  27 }; // 12.345
struct sFakeFloat b = { 128, 8};   // 0.5
struct sFakeFloat result;

For this example, we’ll give the variables precise values so we can check the process at each stage. (Although one half could be trivially implemented as {1, 1}).

The temporary variable noted must be at least as many bits as both numerators combined. Since we are using 32-bit numerators, the temporary variable has to be 64 bits. If we were using 16 bits in the numerator, we’d need at least 32 bits to hold the multiplication. Note that a 64-bit integer might not be native to your processor, so using it will incur some overhead, but not nearly as much as there would be if you used variables of type float or double:

int64_t tmp;
tmp = a.num;
tmp = tmp * b.num;

The multiplication is done in two steps to upgrade the variables into the larger-sized variables. If you’d just multiplied the numerators together, the result would have been an int32_t before it got upgraded and stored in an int64_t. You need to upgrade, then multiply:

result.shift = a.shift + b.shift;
while (tmp > INT32_MAX || tmp < -INT32_MAX) {
  tmp = tmp >> 1;
  result.shift--;
}
results.num = tmp;

As with addition, as long as the results of the numerator multiplication don’t fit in the results numerator, we need to decrease the shift to avoid overflow. For the values in our example, the loop decreases the shift eight times.

We can also make sure that the shift values don’t overflow the shift variable:

tmp = a.shift + b.shift;
ASSERT(tmp < INT8_MAX);
result.shift = tmp;

The result of the operation is 6.1725 {1656917852, 28}.

Division is a bit more complicated, but works along the same lines as multiplication. It divides the numerators and subtracts the shifts instead of adding them.

Determining the Error

The algorithms we’ve used for both addition and multiplication can sometimes cause errors. So you’ll need to limit the sizes of your variables or return those errors to a calling function. Recognizing the limitations of the binary scaling algorithms I’ve shown is one of your responsibilities. The results will be best if you have plenty of prior knowledge about the numbers you’ll be working with, so you can handle overflows and verify the stability of your system. You could make a very generic library to handle every possible case, but you run the risk of reimplementing floating-point numbers with all the overhead. As you look at your algorithm, you’ll need to know the range (min and max) of the variables and the precision you need for dealing with them at each point in time.

Let’s go back to our temperature compensation example:

y = Ax² + Bx + C

The ADC input (x) is 10 bits, so the minimum value is 0 and the maximum value is 1,023. Suppose we want the output to be within 0.0015 of the compensated value in floating point (this is part of the system error budget). The compensation coefficients A, B, and C are set in the factory as a calibration. We have control over the manufacturing process and can fail a unit in the factory if the coefficients don’t fall within a tolerated range (usually this indicates the sensor is damaged in some manner; it isn’t just for ease of coding). See Table 9-3 for other information.

Table 9-3. ADC example limits of parameters

| Input from ADC | A | B | C
Minimum | 0 | –0.001 | 0.002 | –2
Maximum | 1023 | 0.001 | 0.2 | 3

We can determine the maximum output value (in floating point) using the maximum coefficients in the equation. The minimum is a little more complicated because A can be negative. Instead of using the minimum ADC value (0) with the minimum coefficients, the minimum result can be obtained only with the maximum ADC, minimum A parameter, and zero B and C parameters.

First, we make sure we can hold our range of outputs in a binary-scaled floating-point value by trying different shift values and calculating the error caused by each shift. Table 9-4 shows an example where the scaling is –1, indicating the numbers will be shifted to the left by one place (so everything is multiplied by 2).

Table 9-4. Example of error examination (bad result)

| Minimum result | Maximum result | Notes (and Excel formulas)
A*ADC² + B*ADC +C | –1046.483 | 1249.641 | Calculated from the parameters in Table 9-3.
Shift value | –1 | –1 | Try different ones to get to an error level that is acceptable.
Numerator | –2093 | 2499 | =ROUND(result*2shift)
Bits needed in the numerator | 12+1= 13 | 12+1= 13 | =CEILING(LOG(numerator, 2)) + 1 (if signed)
Size of numerator variable | 16 | 16 | Needs to be greater than the previous line. Signed values require one more bit.
Precision granularity | 0.5 | 0.5 | =2shift
Absolute error in calculation | 0.017 | 0.129 | =ABS(result–(numerator / 2shift))

The error is pretty far off from our error goal of 0.0015. How do we go forward? We need to determine the range of each variable (including whether the inputs and outputs are signed or unsigned). From there, for each input and output, check the representation and verify that the granularity is acceptable as follows:

Determine how many bits should be in your numerator. Ideally, this is the number of bits native to your processor: 8, 16, or 32. You can make it smaller so that your whole structure fits into 32 bits, but you run the risk of creating more (and slower) code as the assembly level tries to shift the variable around.

Choose a shift value that allows your numerator to fit in the number of bits you have available. You may have a range of viable shift values (note the range because you may need it later).

Verify that the number can be represented in a binary-scaled format at the extreme values without overflowing.

Verify that the granularity is acceptable; it should be less than your goal error.

Look at the errors at the extreme values and for a nominal value in the normal functioning range. These will always be less than the granularity, but errors provide another check that this scaling value is valid.

With this process, we can get much better shift values, as shown in Table 9-5. It is an iterative process, so I tend to use Excel or some other spreadsheet program to help calculate the values.

Table 9-5. Example of error examination (good result)

| Minimum result | Maximum result | Notes (and Excel formulas)
A*ADC² + B*ADC +C | –1046.483 | 1254.129 | Calculated from the parameters in Table 9-3.
Shift value | –8 | –8 | Try different ones to get to an error level that is acceptable.
Numerator | –267900 | 321057 | =ROUND(result*2shift)
Bits needed in the numerator | 19+1= 20 | 19+1= 20 | =CEILING(LOG(numerator, 2)) + 1 (if signed)
Variable size of numerator | 31 | 31 | Needs to be greater than the previous line. Note that signed values require one more bit.
Precision granularity | 0.00390625 | 0.00390625 | =2shift
Absolute error in calculation | 0.001375 | 0.000094 | =ABS(result–(numerator / 2shift))

We can represent the range of interest with very little error inherent in the fixed-point number. Although not required, it is good that we can represent both the minimum result and the maximum result with the same shift value, and we can still optimize the values later. So that covers the minimum and maximum of the output. Now we need to go through the same process for each input and determine how big the variables need to be between each step.

Since the A coefficient gets multiplied by the square of the ADC input (1,023*1,023), any error in its representation is going to get multiplied by a million. Let’s look at the range of the A coefficient and how that might change the representation needed, as shown in Table 9-6. Note that the shift value (32) was chosen to represent the small range (–0.001 to 0.001), with high precision to reduce numeric errors.

Table 9-6. Example of error examination on the coefficients

| Minimum | Maximum | Notes and formulas
Coefficient A in floating point | –0.001 | 0.001 | In manufacturing, we’ll reject any calculated coefficient that does not fit in this range.
A shift | 32 | 32 | Choose this so that the numeric error is small.
A numerator | –4294967 | 4294967 | =ROUND(coeff*2shift)
Actual bits in numerator | 23+1= 24 | 23+1= 24 | =CEILING(LOG(numerator, 2)) + 1 (if signed)
Granularity | 4.2950E+09 | 4.2950E+09 | =1/(2shift)
Error associated with coefficient A | 6.89179E–11 | 6.89179E–11 | =ABS(coeff–(numerator / 2shift))
Multiplied by max input value (1024*1024) | –4.5036E+12 | 4.5036E+12 | Multiply out the input and the coefficient.
Number of bits required to hold the results | 44 | 44 | Note that this will not fit in a 32-bit integer, unless we shift it down.
Floating-point A*x² | –1048.576 | 1048.576 | Using floating point, this is the answer we expect.
Faked-point A*x² | –1048.575928 | 1048.575928 | Using fake floating point, this is the answer we get.
Error between floating and faked | 7.22656E–05 | 7.22656E–05 | Error due to using fixed point. This can be made smaller by increasing A’s shift value.

Note that the A part requires 44 bits (43 + a sign bit), so during calculations, the intermediate value will need to be an int64_t. At the end of calculating Ax², we can keep the intermediate variable around for further use or put the A part back into our structure. In this example, to go back in the structure, it needs to fit into a 32-bit signed integer, so it will need to be right-shifted by (44–32) = 12.

Fortunately, this makes no difference for the error. If we’d used 16-bit signed integers, then going back into the structure would lose too much precision (and would be noted in the “Error between floating and faked” column in Table 9-6). If even the 32-bit integers couldn’t retain the necessary precision, we might keep the intermediate value around until the shift was safe. (If we’d tried to use 16-bit integers, our original check would have shown us that the result could not be represented without overshooting our error budget.)

So once we do the same for B and C (both a little easier), we can add up the errors for each part to find the maximum error expected. If it is outside your error budget, look at the largest error sources and increase their shift values. (If they eventually exceed the variable size, look at the next largest error source and increase its shift value, and so on.)

The next step is to add each part of the equation. In this example, it is straightforward, but you do have to remember to shift them all to the same shift value so they have the same denominator before adding the numerators.

As part of adding these together, we have to check that the variable we are using to store each operation is large enough. If this part of the equation used all 64 bits of our variable, we’d have to shift down before doing the next operation and therefore check that the shift down did not increase the error outside the acceptable range. The addition will require a larger intermediate variable, because adding two 32-bit integers may lead to a 33-bit integer. After the addition, check that the number can be shifted to fit back in the structure.

The order of operations and shifting is important but very application-specific. You must determine where overflows and underflows can occur, or you’ll end up with some fairly strange behavior.

The last part of the process is to check the final fixed-point formulated numbers against the expected floating-point version. The error here may be different (smaller) from the errors added together in Table 9-5 because the coefficient errors don’t always combine in a maximally bad way (they tend to cancel out).

This process of determining the possible error is laborious and often requires multiple passes as other parts of the system change. Creating a well-documented spreadsheet will save you time. Further, creating a sandbox to run on a PC (as described earlier in Chapter 2) can help you test your code against corner cases and look for numerical instabilities.

Further Reading

Someday I’d like to sit down and read Donald Knuth’s Art of Computer Programming front to back. The scholastically inclined part of me is slightly stunned I haven’t done it already. The more pragmatic (um, lazy) part convinces me to just look up what I need, when I need it, and not get too distracted by all of the interesting, shiny objects around in the book. Regardless of whether you’ve read the book, it should be on your shelf:

Knuth, Donald E. 1998. The Art of Computer Programming, volume 2: Seminumerical Algorithms. Boston: Addison-Wesley.

Alternatively, you may want to use a numerical recipes book for more implementation-specific information:

Press, William H., Saul A. Teukolsky, William T. Vetterling, and Brian P. Flannery. 2007. Numerical Recipes, Third Edition: The Art of Scientific Computing. New York: Cambridge University Press. Similar books come in language-specific forms (C, C++, Java, etc.).

At the deeply embedded scope there is Henry Warren’s Hacker’s Delight (Addison-Wesley Professional). This is an interesting book to flip through, almost a grimoire of slightly scary optimization techniques. Compiler designers tend to need to know these things. You want people to be able to read and reuse your code, so don’t go too overboard in optimization at this level.

Interview Question: Handling Very Large Numbers

Write a program to add the numbers from 1 to 10 in the language of your choice. Use C if you don’t care. As the interviewee completes each piece, a new question is asked: what do you need to do to make it generic so it adds 1 to N? What sorts of optimizations can you provide? What limitations are inherent in the implementation? How would you change it for the input to be arbitrary length? (Thanks to Rob Calhoun for this question!)

As the interviewee starts with the first question, does he create a function that takes a parameter to stop the addition (1 to N)? That is better than hardcoding it immediately to go from 1 to 10. From there, I look at the size of variables for both input and output variables. Using integers or long variables is OK, but can he explain the limitations it places on the code? Does he use unsigned or signed variables, and can he explain why unsigned variables are preferable?

If he doesn’t know the formula for the sum from 1 to N is equal to N*(N+1)/2, I help him through it, typically by writing out 1, 2, 3, 4, 5, 6 and pairing up 1+6, 2+5, 3+4. The interviewee loses no points for not knowing this formula, as long as he’s awake and figures it out. Points are lost for interrupting and insisting upon an incorrect formula, even in the face of counterexamples (it happens more often than you’d think).

Next, we go back to the size of the variables. What is the size of N*(N+1)/2 compared with N? If N is a 16-bit integer, how big does the output need to be? What if N is 64 bits? I prod to ensure he knows that (2¹⁶)² is 2(16*2).

Finally, we get to the really interesting part of the question, where I get to ask how he’d change it if it needed to work for inputs up to 2⁶⁴. Now what can he use as an output? If he picks double-precision floats, well, we discuss the limitations of floats. There is no wrong answer here, but I keep building up: what if we need to use even larger numbers? I limit the question to on the order of a few hundred or a few thousand digits, always constraining it to not be more than a million hexadecimal digits.

Now how can the interviewee modify his function? I walk him through constructing the data structure used for the input buffer and the output buffer (and he should know by this point how big to make the output buffer: twice the input) and the function prototype.

I look for thread safety and failure to keep track of the size of the byte string, discussing as necessary. Finally, I get him to do as much of the implementation as he can for computing (N*N – 1)/2 for arbitrary-length integers, reading from the input array, and writing to the output array. If he’s having some trouble, I suggest using an 8-bit machine because it is more straightforward to draw out.

Depending on the skill the interviewee shows, this question can move in different directions, drilling down to show strengths and weaknesses. It could easily take the whole interview slot to work through only part of it.

Chapter 10. Reducing Power Consumption

Whether you are building a device that fits in your pocket or trying to save the world by reducing your company’s carbon footprint, decreasing a system’s power consumption can take an order of magnitude more time than implementing the product features. Choosing all the right hardware components is a huge part of making a system power-efficient. But because the processor is likely to be one of the largest consumers of power in the system, software can play a big role in saving electricity.

The pressures to decrease power usage and cost are what lead us to select processors that don’t have enough resources to comfortably perform the product features. Since you are a relatively expensive resource, using your time to save on cost of the processor is sensible only if you are building enough units to amortize your time.

Tip

How expensive are you? Take your annual salary and divide by a thousand. That is about what each hour of your time costs your company, counting salary, benefits, office space, and all the little things that add up. So if you are making an example salary of $1,000 per year, each hour is worth about $1. If you can buy a $3.50 tool to tool to save four hours, it is usually worth it. Of course, there is a difference between capital outlay (cash) and sunk cost (your salary), so although this is a good rule of thumb, your boss might not let you buy the scooter to get from your desk to the break room, even after you describe the cost benefit of saving 30 seconds every day.

On the other hand, a processor with the minimum amount of power cycles, RAM, and code space consumes less power than one with plenty of resources. As suggested in Chapter 8, you may want to optimize to “as low as possible,” but that will take an infinitely long time. Start with a quantifiable system goal. Let your electrical engineering team do its part when choosing the components. Then use the points in this chapter to reduce the power further.

Understanding Power Consumption

All electrical circuits can be modeled as resistors, though this is akin to saying all differential equations can be modeled linearly. It is true, but depending on the circumstances, you might expect to see a lot of error. However, it is a good start. Power is measured in watts and is proportional to the square of the current used in the system:

P = I²*R
power = (current)² * resistance
Watts (W) = (Amps (A))² * Ohms (Ω)

Because your system is modeled as a resistor, if you focus on decreasing your current, you decrease your power consumption. Another way to look at power is as the product of voltage and current:

P = V * I
power = voltage * current
Watts (W) = Volts (V) * Amps (A)

This is why your processor core may run at 1.8 V even though other parts of your system run at 3 or 5 V. Because the core takes a lot of current, the lower voltage means less power consumption. The two ways of looking at power are equivalent, according to the golden rule of electrical engineering, Ohm’s law:

V = I * R
voltage = current * resistance
Volts (V)  = Amps (A)  * Ohms (Ω)

There is a lot more that Ohm’s law can do for you, so if this overview has given you a buzz of interest, check out Further Reading for some electrical engineering reading.

Tip

One more useful equation is for energy:

      W = P * T
      energy = power * time
      Joules (J) = Watts (W) * seconds

For the first time, we have an equation with time. Basically, it says that if you are designing an energy-efficient system, you can minimize the power it uses or the amount of time it is powered on.

Batteries have a voltage rating and a capacity. An alkaline AA battery has a nominal voltage of 1.5 V and a capacity of about 3,000 mAh (milliamp hours). So capacity isn’t rated in power, but in current supplied over time. If your system takes 30 mA and 1.5 V when it is on, with an AA battery it should last about 100 hours (~4 days). If your system takes 300 mA, it should last about 10 hours. However, if your system consumes 3,000 mA, the AA battery won’t last an hour, because just as with calling your system a resistor, this rule of thumb goes only so far. Your battery will have a datasheet that explains its capacity, chemistry, peak current draw, and other characteristics.

From both perspectives (power and battery capacity), minimizing current consumption is the key to reducing power usage or increasing battery life.

Measuring Current

To keep score during your efforts to reduce power consumption, you’ll need to know how to measure current consumption. If you have a bench-top power supply, ignore what it says, as they aren’t very accurate.

Chapter 3 suggested that you get your own digital multimeter (DMM). If you get a good one, you can measure current directly, as shown in part b) of Figure 10-1. Most cheap DMMs will let you measure in the range of milliamps (mA). A small embedded system consisting of a low-power processor and a few peripheral chips will draw tens to hundreds of milliamps. So the DMM is an easy way to measure the current of a running system. However, if you are trying to look at power consumption in sleep mode, you may need to measure lower amounts of current. A good DMM may let you look at hundreds of microamps (μA), but you may need even finer granularity than that.

Figure 10-1. Two methods of measuring current

Another way to measure current is to put a small resistor in series with the system, as shown in part c) of Figure 10-1. Switch your DMM to voltage mode, something it is better at measuring. You may need to tweak the value of the resistor to get the voltage in the range that your DMM can read. Then apply Ohm’s law:

current = voltage / resistance

This table shows some possible combinations to give you the idea. Depending on the current you expect to read, you may have to tweak the resistor value.

Expected current | Resistor in circuit | Voltage measured on DMM | Note
12 mA | 1 Ω | 0.012 V | Easily measured on most DMMs.
23 μA | 1 Ω | 0.000023 V | Impossible to read on most DMMs.
34 μA | 10 Ω | 0.00034 V | A very good DMM might be able to read this.
45 μA | 100 Ω | 0.0045 V | Once again, within reach of most DMMs, but resistor is getting to be too large.

The resistor likely needs to be large enough to read the tiny sleep currents but still small enough that it doesn’t become a large consumer of power itself and interfere with the measurement. Since it consumes power on its own, you may choose to have the resistor available only on development boards, with a zero-ohm resistor (also known as a “wire”) on the production boards.

Tip

Resistors are associated with an accuracy. After you measure current consumption, power off your system and measure the true resistance value of the resistor with your DMM.

Figure 10-1 shows how to measure the system current, but if you have multiple power rails (e.g., 5 V, 3.3 V, 1.8 V), you may want to measure each individually or focus on the power rail most closely associated with the processor. The process is the same, though you might need a hardware engineer to set it up on your board.

Warning

Measuring the system current with your debugging tools attached might change the results.

Finally, looking at voltage across a resistor with a DMM is good measure of static consumption, but looking at changing states (e.g., sleep to active transitions) requires an oscilloscope.

(Thanks to Robert Mitchell for his excellent help on this sidebar!)

Turn Off the Light When You Leave the Room

The easiest way to reduce power consumption is to turn off components that are not needed. The downside is that those components won’t be ready when you need them, and bringing them back will add both some power usage and some latency in responding to events. You’ll need to investigate the trade-offs; I tend to use a spreadsheet to help me weigh the power savings.

The following sections detail some of the ways to turn off or reduce the power drain of components.

Turn Off Peripherals

As you review your schematic and your design, consider what your processor has access to and how to design your system to turn off peripherals that are not needed. For example, if you have external RAM, you may be able to move the data to a local location and then power down the RAM instead of leaving it powered on for extended periods of time.

A chip external to the processor will consume no current if it does not have power. If the processor doesn’t have access to the power lines, holding the peripheral in reset should nearly eliminate the current consumption (check the peripheral datasheet!). This isn’t as optimal as turning off the power, but better than leaving the peripheral running.

Many chips need time to return to functioning after reset, so there is some overhead in powering them down. The goal is to turn off the component for a reasonable amount of time. ADCs are some of the peripherals with longer reset times, but they are often big power consumers, so it is often worth taking the time to balance the power consumption versus the power-on time.

Turn Off Unused I/O devices

If you have spare I/O devices, you can save a little bit of current by configuring them to be inputs with internal pull-downs. If your chip doesn’t have internal pull-downs, try tristating it (no pull-up and no pull-downs, just floating) or setting it to be a low output. If that doesn’t work for you, an input with pull-ups still has reasonably small amounts of leakage current.

Tip

Don’t remember pull-ups as anything other than gym torture? See Chapter 3 for more information on both pull-ups and pull-downs.

As for those I/Os connected to the component you’ve powered off, the preferred order is the same (input with pull-downs, output low, input with pull-ups). Configuring one I/O in this manner isn’t likely to save a lot, but if you have a whole bunch of them, the small savings can add up. On the other hand, if some I/Os on your system have an external pull-up or pull-down, set the internal one to match (or eliminate the external ones as redundant). Fighting external resistors wastes power.

Warning

If you have a peripheral that is powered off (or in reset), be careful how you connect other processor I/Os to it. Don’t leave the lines pulled-up or set high. Chips often have internal protection diodes that could use the high signal to back-power and damage the chip.

Turn Off Processor Subsystems

In addition to setting I/O devices to be low power, often you can turn off whole subsystems of the processor that you don’t need (are you using that second SPI port?) Your processor manual will tell you whether this is a supported feature of the processor. Sometimes you can turn off all functionality (best); sometimes you can only turn off the clock to the peripheral (still pretty good).

Slowing Down to Conserve Energy

Lowering your clock frequency saves power. This is true for all of the clocks you can control, but especially the processor clock. However, a slower processor clock gives you fewer processor cycles to run in. That is why we spent so much time on optimizing code in Chapter 8, even though optimizing for processor speed tends to decrease the quality (robustness, readability, and debuggability) of your code. Often the most straightforward way to decrease power consumption of your system by 10% is to decrease the clock by about 10%. In other words, the power consumption of the chip is proportional to the frequency it runs at.

If the processor has to be on all the time (for example, monitoring its environment), being able to slow down may be crucial to achieving low power. Some embedded operating systems will do frequency scaling for you, consuming power efficiently when possible and still having the speed available when you need it. You can do this without an operating system. Instead of scaling over the whole range, you might want to create slow, medium, and fast modes and change the performance level based on events. However, as with running a race, there is a balance between slowing the processor down and keeping it on for a longer time (being a tortoise) or sprinting for a shorter period of time and then sleeping (being a hare).

Dealing with Multiple Clock Sources of Different Accuracies

Many power-efficient systems have a slow clock that can drive the processor in low power modes, often generated via a 32 kHz oscillator.[11] This slow clock is used to keep a part of the system alive while in low-power state, often to drive a real-time clock (RTC) so the system knows the current time even when it has been powered “off.”

Such systems also have a crystal-generated fast clock with higher power consumption, higher accuracy, and less temperature-related error. You can use the high-accuracy clock to correct the errors in the low-accuracy one (aka time base compensation, or disciplining the timing source). The goal is to determine the difference between the low-accuracy clock’s actual value and expected values.

Say your fast clock is 50 MHz for this example. To determine the compensation, set the slow clock as an input to a capture timer. Next, set up a one-second timer interrupt using the more accurate fast clock. The slow clock should have given you 32,768 ticks on the capture counter when the one-second timer expired. However, it probably gave you a value that is a few off from that (depending on ambient temperature and the manufacturing process used on the 32 kHz clock). You can use this offset to move between the 32 kHz cycles that occurred while the system was asleep and 50 MHz clock cycles (or between 32 kHz cycles and real time). For example, say that when you did the compensation, you got 32,760 captured ticks per one-second period (which should be equivalent to 50 M crystal clock cycles). Now the processor can expect 32,760 cycles per second from the 32 kHz clock.

The final outcome is no better than the most accurate clock you have. In fact, it is likely to be worse because you can’t tell whether the compensation should have been fractional (32,760.5). (Remember jitter from Chapter 5? It applies here.) Also, as temperature changes, the compensation varies, so you’ll need to redo the compensation periodically (how frequently depends on the timing accuracy your product requires). The precision clock might not be the fast one. Systems that use GPS for location and timing have access to the pulse-per-second (PPS) signal. This is a very accurate (and precise) clock, but it is too slow to drive a system. However, it can be used to correct your clocks in much the same manner as described previously:

Interrupt on the PPS signal.

Take a measure of how many system ticks (processor or timer) passed since the last PPS interrupt.

Adjust the system timing accordingly.

(Thanks to Matthew Hughes for recommending the addition of clock compensation!)

[11] Actually, it is usually a 32.768 kHz oscillator, so you can count to 1 second with a 15-bit counter.

Putting the Processor to Sleep

Even at a slow clock speed, the processor is consuming power. Slowing down will help, but what if your code spends a lot of time waiting for things to happen?

Many processors designed for low-power systems can go into an energy-conserving sleep when they aren’t needed. They use interrupts to tell the processor when to wake up.

On your computer, sleep is a standby mode where processing is off but the memory is still powered. This lets the operating system wake up quickly, exactly where it had left off. Alternatively, in hibernation, the contents of RAM are written to the disk (or other nonvolatile memory). It takes longer to wake up from hibernation, but the system uses much less power while in that state (maybe almost no power, depending on the make of your computer).

Most embedded processors offer a similar range of sleep possibilities, with some processors having lots of granularity and others opting to have only one low power mode. In decreasing levels of power consumption, some sleep modes you might find in your processor manual include:

Slow down

Going beyond frequency scaling, some processors allow you to slow the clock down to hundreds of hertz.

Idle or sleep

This turns off the processor core but keeps enabled timers, peripherals, and RAM alive. Any interrupt can return the processor to normal running.

Deep sleep or light hibernation

In addition to the processor core being disabled, some (possibly all) on-chip peripherals can be configured to turn off. Be careful not to turn off the subsystem that generates the interrupt that will wake up the processor.

Deep hibernation or power down

The processor chooses which on-chip peripherals to be turned off (usually almost all of them). RAM is usually left in an unstable state, but the processor registers are retained, so the system doesn’t need initialization on boot. Usually only a wakeup pin or a small subset of interrupts can restart the processor.

Power off

The processor does not retain any memory, and starts from a completely clean state.

Deeper sleep modes place the processor into a lower-power state but take longer to wake from, increasing system latency. You’ll need to spend some time with the user manual to determine how to achieve the lowest power level possible that will meet your product’s needs.

The wake-up interrupts could be buttons, timers, other chips (e.g., ADC conversion complete), or traffic on a communications bus. It depends on your processor (and your sleep level).

Once you’ve determined that your processor can sleep, how do you design a system to make the best use of it? We’ll explore that now.

Interrupt-Based Code Flow Model

Understanding the interrupt-based code flow model is critical for power-sensitive applications. Breaking down the long name in the title of this section, interrupt-based indicates that the code will listen to interrupts to do its business. And if you have a subsystem that doesn’t normally cause an interrupt, you’ll need to make one, probably with a timer that can do all of the regular housekeeping. The next part of the name is code flow to show that the program tends to flow along like a waterfall. It doesn’t have multiple tasks, and it tends to be linear.

If you are familiar with an event loop for handling button presses in user interface code, the principle here is very similar. Button handlers tend to spend a lot of time idling, waiting for a button to be pressed. Once one is, the handler calls the appropriate function, and after it returns, the handler goes back to waiting for something to happen. Instead of idling, the microprocessor spends time sleeping, waking up only when it needs to, then handling the interrupt and going back to sleep. The goal is to maximize the amount of time the processor is asleep, thereby maximizing power efficiency. See Figure 10-2 for a flow chart describing the interrupt-based code flow model.

Figure 10-2. Interrupt-based code flowchart

The interrupt may be generated from a peripheral to indicate it needs processor attention (like an ADC finishing with its data, needing the processor to read the result and start another conversion). In other systems, the interrupt may be generated by a button the user presses or a sensor rising above a threshold. The interrupts may also be generated internally, for example, by a timer.

When an interrupt occurs, the processor wakes up and calls the appropriate interrupt service routine (ISR). The ISR sets a flag to indicate the cause for the wake-up and modifies the sleep register to keep the processor awake. When the ISR returns, because the sleep register is set to awake, the main loop runs. The main loop checks each possible flag and, when it finds a flag set, runs the appropriate handler (also clearing the flag). When the main loop completes and all of the flags have been cleared, the processor returns to sleep mode. Figure 10-2 illustrates the general idea with two handlers to check.

Let’s say you have a processor that can sleep. To experiment, start with a timer interrupt. These are common in the interrupt flow model because the timer lets you wake up to do any necessary housekeeping. Run the processor without sleep, and set up an interrupt to run so it toggles an output line or LED. The frequency doesn’t matter, though a slow interrupt is better. I’d recommend about twice per second (2 Hz). Measure the current of the system as described in Measuring Current.

Now set your processor to sleep between interrupts. From the outside, the processor behaves the same. However, when you measure the system current, it should be smaller. How much smaller depends on many aspects of your system. For instance, I tried this procedure with the Texas Instruments (TI) MSP430 Launchpad development kit using the G2231 processor. When it was in a while loop, waiting for the next timer interrupt, the system consumed 9 mW (3 mA at 3 V). When I let it enter a low-power mode instead of idling in the while loop, the results were dramatically different. I used the third level of sleep (the processor has four). However, even with a handy 100 Ω resistor, I couldn’t measure any voltage drop over the resistor with my cheapo DMM. When I was consuming 3 mA with the 100 Ω resistor, I got about 300 mV of drop. Lower current implies lower voltage, of course. But my DMM wasn’t sensitive enough to make the measurement in the low-power state.

The datasheet informed me that the current should be about 0.9 uA (2.7 uW) in this state, which would require additional special tools for me to measure (and/or a much larger resistor, probably 100 kΩ). The timer interrupt to toggle the LED must have spent some time at 3 mA, but the processor woke up, set the timer flag, toggled the LED in the main loop, and went back to sleep before it could make a blip on my meter. Remember, energy efficiency is about time as well as current.

The processor on TI’s Launchpad kit is designed to be super low power when it is sleeping, so the results might not be so dramatic on another device.

A Closer Look at the Main Loop

Handling the interrupts in the main loop allows you to keep the interrupt service routines short because all they do is set a couple flags and exit. Long interrupts tend to decrease system responsiveness. Plus, going through the main loop can aid in debugging, as the system revolves around sleeping and the flags set by the most recent interrupts. (If you can output the cause variable on every pass, you’ll get a good view of what the system is doing.) Note that additional interrupts may happen while executing the handlers for the interrupt that caused the processor to wake up.

By putting the flags in the bits of a variable (or two), it becomes simple to check that all interrupts have been processed:

volatile uint16_t gInterruptCause = 0; // Bitmasked flags that describe
                                       // which interrupt has occurred

void main(){
    uint16_t cause;
    Initialize();
    while (1) {
        cause = gInterruptCause; // atomic interrupt-safe read of gInterruptCause
        while (cause) {
            // each handler will clear the relevant bit in gInterruptCause
            if (cause & 0x01) { HandleSlowTimer();}
            if (cause & 0x02) { HandleADCConversion();}
            FeedWatchdog(); // while awake, make sure the watchdog stays happy
            cause = gInterruptCause;
        }
        // need to read the cause register again without allowing any new interrupts:
        InterruptsOff();
        if (gInterruptCause == 0) {
            InterruptsOn();
            GoToSleep(); // an interrupt will cause a wake-up and run the while loop
         }
        InterruptsOn();
    }
} // end main

The interrupts themselves just need to set the flag and modify the sleep register to keep the CPU awake after they return. The handlers will perform the necessary actions and clear the appropriate bits in the cause register (gInterruptCause) in an interrupt-safe manner.

Warning

Remember that working with interrupts can be tricky due to race conditions. See Interrupts for more tips on interrupts.

If you have multiple power modes and need to decide which to enter when your main loop has finished, you may need to examine a lot of state scattered through your system. For example, if a peripheral is busy and can’t be shut down, or if a communication bus remains active, you may not be able to achieve the lower states of sleep until they have finished. One option is to have a power management module with a table of registered function pointers. Each module that might preclude some sleep level registers a callback. When considering the next sleep state, all functions in the table get called and can veto certain sleep levels.

Processor Watchdog

In Watchdog, I talked about the importance of having a watchdog and how you shouldn’t use a timer interrupt to service the watchdog. Now that your system is entirely interrupt-based, how are you going to make sure the watchdog remains content?

In the lightest levels of sleep, your processor will probably let the watchdog continue to run. The processor may let you configure whether you want the watchdog to run while the processor is asleep (consuming some power during sleep and forcing the processor to wake up to service it) or want to have the watchdog sleep when the processor does (losing a fail-safe mechanism for your system). Recognizing the trade-off will help you make the right decision for your system.

If you leave the watchdog on, you’ll need to set a timer to wake the processor up before the watchdog expires. This goes against the grain of my earlier advice. However, here the watchdog is not only verifying that the system is running properly, but also verifying that the system is waking up from sleep properly.

Avoid Frequent Wake-Ups

Since each wake-up requires some overhead from the chip (and the deeper the sleep, the more overhead is required as it wakes up), avoid frequent wake-ups. If you have a low-priority task that doesn’t have a hard real-time requirement, piggyback it upon another task. This spreads the overhead of waking up the processor over several tasks.

Although a timer to do housekeeping can be valuable, it may be better to check whether enough time has passed to do the low-priority tasks and reset the timer so you can skip a wake-up.

Chained Processors

It is pretty common to have a small, very efficient processor monitor the important signals, waking up a larger processor when needed. In this case the larger processor requires more power to wake up (and possibly more time if it is running an operating system). So the small processor does the housekeeping: checking for buttons pressed, looking for lower power conditions that indicate the system should shut down, or waking the large processor due to a preset alarm.

In such systems, both processors spend as much time asleep as possible, but the system is designed so that the interrupts get triaged in the small processor, which can opt to handle them itself or wake up its larger partner.

Further Reading

As noted in Chapter 3, for a gentle introduction to electronics, I recommend Make: Electronics by Charles Platt (O’Reilly). This step-by-step introduction to putting a system together (and soldering) is an easy read.

For a more serious look at electrical engineering, The Art of Electronics by Paul Horowitz and Winfield Hill (Cambridge University Press) is the seminal book, along the lines of Knuth’s Art of Computer Programming.

If you are somewhere in between those two and looking for a more intuitive grasp of analog components, try There Are No Electrons: Electronics for Earthlings by Kenn Amdahl (Clearwater Publishing). This fun book gets away from the plumbing model of electronics and gives you a different story about how resistors and capacitors work.

The TI MSP430 line of processors is geared toward inexpensive and power-sensitive devices. The “MSP430 Software Coding Techniques Application Report” is an excellent reference for learning more about the interrupt flow model and other power-saving tips.

Interview Question: Is the Fridge Light On?

In two minutes, how many different ways can you figure out how to tell whether the light in a fridge is going off when you shut the door? Please don’t damage the fridge. (Thanks to Jen Costillo for this question.)

This question is all about quantity and creativity of solutions offered. This is not a question with a lot of depth (though I will ask a follow-up if I want more depth). Lots of people come up with the common solutions, some of which are:

Take a video recording.

Wireless (and presumably battery-powered) light sensor.

Solar panel charger (check the charge of a battery after a period of time, with and without the door open).

Disable the cooling assembly so the fridge uses only a small amount of power, then check the difference in current with the door open and closed.

With the door open, press the door-closing button to see what happens.

Interviewees get credit for the number of solutions, uniqueness of them, what implicit assumptions they make, and how they apply scientific method to solve the problem. I like to see them note the trade-offs in terms of time and cost, but I’d rather they spend the time generating different answers.

There seem to be three types of responses when faced with this question. First (and worst) are the interviewees who come up with one of these and can’t figure something else out, even with prompting. Second (and most common) are the interviewees who come up with a solution and tweak one thing at a time until they get to another solution. They generally rack up enough solutions and often surprise me with a few really creative ones. The last set of responses can only be classified as mildly insane. These are the most fun, and as long as they manage to get a few of the more sensible answers, these are the best people to work with.

One person suggested a statistical method: First, you get 100 units. Leave the door open on 50 of them and the door closed on 50. Determine the mean time between failure (MTBF) for the lightbulbs in the open units. Now open all the closed doors and wait for those to fail. If they fail about the same time as the initially opened ones, then the light was off with the door closed.

This isn’t necessarily sensible or easily accomplished in a reasonable amount of time for testing the lights in fridges, but there are times when this sort of method will work very well. It gets bonus points for extra creativity.

Index

A note on the digital index

A link in an index entry is displayed as the section title in which that entry appears. Because some sections have multiple index markers, it is not unusual for an entry to have several links to the same section. Clicking on any link will take you directly to the place in the text in which the marker appears.

Symbols

# (pound sign), Copy Loader to RAM, Reading a Map File (Part 1), Constants and Strings

#define statements, Reading a Map File (Part 1)

#pragma directives, Copy Loader to RAM

in preprocessing directives, Constants and Strings

1-wire bus, 1-wire

& (ampersand), Starting with Registers, Circular buffers

bitwise AND operator, Starting with Registers, Circular buffers

< > (angle brackets), Starting with Registers, Starting with Registers, Dividing by a Constant, Scaling the Input, Rational Numbers, Addition (and Subtraction), Addition (and Subtraction)

<< (bitwise shift left) operator, Starting with Registers, Addition (and Subtraction)

>> (bitwise shift right) operator, Starting with Registers, Dividing by a Constant, Scaling the Input, Rational Numbers, Addition (and Subtraction)

@ (at symbol), compiler RAM allocation using, Copy Loader to RAM

^ (caret), bitwise XOR operator, I/O-Handling Code

! (exclamation mark), logical NOT operator, Starting with Registers

# (pound sign), Identifying Fast and Slow Operations

#define statements in C, Identifying Fast and Slow Operations

? (question mark), ?: ternary conditional operator, Functions and Macros

~ (tilde), bitwise NOT operator, Starting with Registers

| (vertical bar), Starting with Registers

bitwise OR operator, Starting with Registers

A

abs function, replacing with macro, Libraries

absolute maximum ratings, information in datasheets, Evaluating Components Using the Datasheet

absolute position, Position encoding

accuracy versus precision, Math

action register, Timer Pieces

active low pins, Datasheet Sections You Need When Things Go Wrong

activity flow, managing, Managing the Flow of Activity (see flow of activity, managing)

actuators, Actuators, Motors, PID control, Position encoding, PID control, Displays

motors, Motors, PID control, Position encoding, PID control, Displays

PID control, PID control, Displays

position encoding, Position encoding

adapter pattern, Adapter Pattern, Facade Pattern

ADC to DAC data-driven system, Circular buffers, Changing Algorithms

ADCs (analog-to-digital converters), Analog sensors, Fake Floating-Point Numbers, Determining the Error

temperature compensation on ADC input, Fake Floating-Point Numbers, Determining the Error

addition, Identifying Fast and Slow Operations, Addition (and Subtraction)

of fake floating-point values, Addition (and Subtraction)

AES encryption, Authentication and encryption, Lookup tables

lookup tables for, Lookup tables

algorithms, A Sandbox to Play In, A Sandbox to Play In, Changing Algorithms, Changing Algorithms, Changing Algorithms, Functions and Macros, Use an Existing Algorithm, Use an Existing Algorithm, Designing and Modifying Algorithms, Explicit input in the table, Factor Polynomials, Taylor Series, Dividing by a Constant, Scaling the Input, Lookup Tables

changing, Changing Algorithms, Changing Algorithms, Changing Algorithms

strategy pattern, Changing Algorithms

template pattern, Changing Algorithms

designing and modifying, Designing and Modifying Algorithms, Explicit input in the table, Factor Polynomials, Taylor Series, Dividing by a Constant, Scaling the Input, Lookup Tables

dividing by a constant, Dividing by a Constant

factoring polynomials, Factor Polynomials

lookup tables, Lookup Tables

scaling input, Scaling the Input

Taylor series, Taylor Series

minimum-of-three variables, Functions and Macros

segregating from user interface, A Sandbox to Play In

using an existing algorithm, Use an Existing Algorithm, Use an Existing Algorithm

using sandbox for, in MVC pattern, A Sandbox to Play In

analog sensors, Analog sensors

analog-to-digital conversions, by digital sensors, Digital sensors

analog-to-digital converters, Analog sensors (see ADCs)

antialiasing of a character, Pixel displays

APIs, From Diagram to Architecture, Sharing private globals, Encapsulate Modules, Driver Interface: Open, Close, Read, Write, IOCTL, Adapter Pattern, Example: A Logging Interface, Sharing private globals, Interrupts

designing and creating, From Diagram to Architecture, Sharing private globals, Encapsulate Modules, Driver Interface: Open, Close, Read, Write, IOCTL, Adapter Pattern, Example: A Logging Interface, Sharing private globals

driver interface, Driver Interface: Open, Close, Read, Write, IOCTL

encapsulating modules, Encapsulate Modules

logging interface (example), Example: A Logging Interface, Sharing private globals

using adapter pattern, Adapter Pattern

similarity of processors and interfaces to, Interrupts

application layer, OSI model, So Many Ways of Communicating

application section, datasheets, Evaluating Components Using the Datasheet

Arduino, setting up timer interrupts, Further Reading

arithmetic operations, speed and cost of performing, Identifying Fast and Slow Operations

Art of Computer Programming, Further Reading, Further Reading

Art of Electronics, The, Further Reading

ASCII characters, Miscellaneous other protocols

assembly code, examining, Look at the assembly, Consider the instructions, Coding in assembly language

instructions, Consider the instructions

optimizing assembly code, Coding in assembly language

ASSERT( ) function, Consistent Methodology

asynchronous communication pathways, Serial

Atmel’s A Ttiny AVR microcontroller manual, 8-bit microcontroller, Toggling an Output

atomic actions, Avoiding Race Conditions

attacks, guarding against, Authentication and encryption

authentication, Authentication and encryption, Further Reading, Security

bootloaders and, Security

resources for further information, Further Reading

Auto button, on oscilloscopes, Setting up a scope

average, calculating, Taking an Average, Taking an Average, Taking an Average

rolling average versus block average, Taking an Average

truncation in integer division, Taking an Average

B

bandwidth, Analog sensors

batteries, Understanding Power Consumption

voltage rating and capacity, Understanding Power Consumption

baud rate (bits per second), RS-232 and TTL, SPI, I2C, Parallel, Comparison of buffering schemes

I2C bus speeds, I2C

impact of DMA, Comparison of buffering schemes

parallel communications, Parallel

in RS-232 communications, RS-232 and TTL

in SPI communication, SPI

baud rate (bits per second)k, USB

USB communications, USB

beginner's guide to electronics, Datasheet Sections You Need When Things Go Wrong

binary numbers, Starting with Registers, Circular buffers

introduction to binary math, Starting with Registers

representing signed numbers, Circular buffers

binary scaling, Rational Numbers, Precision

precision and, Precision

binary search for prescaler and compare register values in timer solution, Doing the Math

bit shifts, Starting with Registers, Identifying Fast and Slow Operations, Identifying Fast and Slow Operations, Dividing by a Constant, Scaling the Input, Scaling the Input, Implicit input, Rational Numbers, Multiplication (and Division), Determining the Error

>> (right-shift) operator, Scaling the Input

economy of, Identifying Fast and Slow Operations

with faked floating-point values, Determining the Error

making division faster, Dividing by a Constant

reducing need for with lookup tables, Implicit input

scaling and division by, Scaling the Input

shift value in multiplication of fractions, Multiplication (and Division)

signed shift values, Rational Numbers

bit-banging, SPI, USB, Comparison of buffering schemes

implementing a bus master with, Comparison of buffering schemes

serial protocols, USB

SPI interface implemented as bit-bang interface, SPI

bitmaps, Pixel displays, Pixel displays, Pixel displays

antialiasing characters in, Pixel displays

displaying on the screen, Pixel displays

bits, Doing the Math, Analog sensors, Further Reading

in a number, Doing the Math

number in ADCs and DACs, Analog sensors

reversing in a byte, Further Reading

bitwise logic operation, replacing math with, Identifying Fast and Slow Operations

bitwise operators, Starting with Registers, Starting with Registers, Starting with Registers, Starting with Registers, Starting with Registers, I/O-Handling Code, Circular buffers, Reduce math in loops, Rational Numbers, Addition (and Subtraction), Addition (and Subtraction)

& (AND) operator, Starting with Registers, Circular buffers

<< (bitwise shift left) operator, Starting with Registers, Addition (and Subtraction)

>> (bitwise shift right) operator, Starting with Registers, Rational Numbers, Addition (and Subtraction)

reducing use of, Reduce math in loops

^ (XOR), I/O-Handling Code

| (OR) operator, Starting with Registers

~ (NOT) operator, Starting with Registers

blinking LEDs, Creating a command, Toggling an Output, Blinking the LED, Using a Timer, Using Pulse-Width Modulation

command table, Creating a command

dimming with PWM, Using Pulse-Width Modulation

as "hello world", Toggling an Output

using a timer, Using a Timer

block average versus rolling average, Taking an Average

block diagrams, The Block Diagram

board bring-up, Board Bring-Up, Your Processor Is a Language, Reading a Schematic, Reading a Schematic, Having a Debugging Toolbox (and a Fire Extinguisher), Keep Your Board Safe, Toolbox, Oscilloscopes and Logic Analyzers, Setting up a scope, Testing the Hardware (and Software), Command Pattern, Watchdog

allocating boards, Keep Your Board Safe

disabling watchdog, Watchdog

initial test software, Testing the Hardware (and Software), Command Pattern

keeping boards safe, Having a Debugging Toolbox (and a Fire Extinguisher)

reading a schematic, Reading a Schematic, Reading a Schematic

toolbox for, Toolbox

using a development kit, Your Processor Is a Language

using an oscilloscope, Oscilloscopes and Logic Analyzers, Setting up a scope

bootloaders, Updating Code, Onboard Bootloader, Security

(see also updating code)

onboard, Onboard Bootloader

security, Security

bouncing, debouncing switches, Debouncing Switches, Debouncing Switches

breakpoints, Debugging

brick loader, Brick Loader, Reset to New Code, Copy Loader to RAM

(see also updating code)

brushed DC motors, Motors

brushless motors, Motors

brute-force timer, Doing the Math

bss segment, Linker Scripts, Reading a Map File (Part 2)

in linker, Linker Scripts

in map file, Reading a Map File (Part 2)

buffers, Circular buffers, Circular buffers, Hardware FIFOs, Direct memory access, Comparison of buffering schemes, Further Reading, Copy Loader to RAM, Linker Scripts, Remove malloc, Memory Overlays, Memory Overlays, Taking an Average

circular, Circular buffers, Circular buffers

comparison of buffering schemes, Comparison of buffering schemes

creating at hardcoded addresses using linker scripts, Linker Scripts

dynamic memory allocation and, Remove malloc

FIFO versus DMA, Further Reading

hardware FIFO buffers, Hardware FIFOs

large buffers in a system, use of, Memory Overlays

ping-pong, in DMA implementations, Direct memory access

use by code loader, Copy Loader to RAM

used in calculating signal average, Taking an Average

using RAM overlays, Memory Overlays

bugs, Board Bring-Up, Your Processor Is a Language, Troubleshooting, Interrupts, When to Use Interrupts, Reading a Map File (Part 2)

in hardware, Board Bring-Up, Your Processor Is a Language

in interrupts, Interrupts, When to Use Interrupts

stack overflow, Reading a Map File (Part 2)

troubleshooting blinking LEDs, Troubleshooting

buttons, A Simple Interface to a Button, Momentary Button Press, Debouncing Switches, Debouncing Switches, Buttons and Key Matrices, Segmented displays

debouncing switches, Debouncing Switches, Debouncing Switches

key matrices, Buttons and Key Matrices, Segmented displays

momentary button press, Momentary Button Press

simple interface to, A Simple Interface to a Button

bytes, Starting with Registers, Miscellaneous other protocols, Version everything and then checksum it, Code Space, Constants and Strings, RAM, Memory Overlays, Further Reading

calculating checksum, Version everything and then checksum it

decimal and hexadecimal values for, Starting with Registers

display on oscilloscope, Miscellaneous other protocols

optimizing code space, Code Space, Constants and Strings

optimizing RAM, RAM, Memory Overlays

reversing bits in, Further Reading

C

C/C++, Compilers, Languages, and Object-Oriented Programming, State of logging, Sharing private globals, Creating a command, Dependency Injection, Reentrant functions, Restore the Context, External Memory, Linker Scripts, Registers and Local Variables, Identifying Fast and Slow Operations

#define statements in C, Identifying Fast and Slow Operations

dependency injection, Dependency Injection

function pointers, Creating a command

interrupt keyword, Restore the Context

linker scripts, Linker Scripts

object oriented programming in C, State of logging

reentrant objects, Reentrant functions

register keyword in C, Registers and Local Variables

returning a pointer to private variables in C, Sharing private globals

support by embedded software compilers, Compilers, Languages, and Object-Oriented Programming

volatile keyword, External Memory

cables, serial connections, RS-232 and TTL

CAD packages, Ideal Project Flow

call stack, Nonmaskable interrupts, Looking up the ISR, Function Chains, Sampling profiler

function chains in, Function Chains

restoring from interrupt, Looking up the ISR

use in profiling, Sampling profiler

callback functions, Command and Response, Creating a command, Dependency Injection, Interrupts

command handling, Command and Response

dependency injection, Dependency Injection

function pointers, Creating a command

interrupt service routines (ISRs), Interrupts

cause register, Multiple sources for one interrupt

cellular data modems, serial connection to, RS-232 and TTL

chained processors, Chained Processors

character encodings, Displays, Miscellaneous other protocols, Miscellaneous other protocols

LCD display, Displays

signal display on oscilloscope, Miscellaneous other protocols

Unicode, Miscellaneous other protocols

characters, antialiasing, Pixel displays

Charlieplexing, Buttons and Key Matrices

checksums, Version everything and then checksum it, Version everything and then checksum it

types of, Version everything and then checksum it

when to use, Version everything and then checksum it

Chip Select (CS) or Slave Select (SS) lines, SPI

circular buffers, Circular buffers, Circular buffers, Circular buffers, Circular buffers, Circular buffers, Circular buffers, Circular buffers, Comparison of buffering schemes

comparison to other buffering schemes, Comparison of buffering schemes

initialization, Circular buffers

with multiple pointers, Circular buffers

reading from, Circular buffers

size of, Circular buffers

writing to, Circular buffers

clear register, An IRQ Happens

clock polarity and phase in communication, SPI

clocks, Timer Pieces, Timer Pieces, Timer Pieces, Doing the Math, Polling, Digital sensors, Serial, Serial, SPI, SPI, Memory timing, Turn Off Processor Subsystems, Slowing Down to Conserve Energy, Slowing Down to Conserve Energy

Clock (SCK, CLK, or SCLK) lines, SPI

clock configure register, Timer Pieces

dealing with multiple sources of different accuracies, Slowing Down to Conserve Energy

equation for relationship with timer frequency, prescaler, and compare register, Doing the Math

for timer input, Timer Pieces

lowering frequency to conserve power, Slowing Down to Conserve Energy

memory timing, Memory timing

noise from, Digital sensors

peripheral bus and, Serial

reducing power consumption, Turn Off Processor Subsystems

serial communication and, Serial

SPI serial communication, SPI

system statistics, Timer Pieces

system tick, Polling

close function, Driver Interface: Open, Close, Read, Write, IOCTL

code, Driver Interface: Open, Close, Read, Write, IOCTL, Typical calls needed for logging, Further Reading, Updating Code, Doing More with Less, Further Reading, Code Space, Constants and Strings, Reading a Map File (Part 1), RAM, Memory Overlays, Speed, Coding in assembly language, Memory timing

moving to faster memory, Memory timing

optimization, Doing More with Less, Further Reading, Code Space, Constants and Strings, RAM, Memory Overlays, Speed, Coding in assembly language

code space, Code Space, Constants and Strings

RAM usage, RAM, Memory Overlays

speed, Speed, Coding in assembly language

style, Driver Interface: Open, Close, Read, Write, IOCTL

unused, in code space, Reading a Map File (Part 1)

updating, Updating Code (see updating code)

versions, Typical calls needed for logging

What is wrong with this code? interview question, Further Reading

code base, trimming down before shipping product, Shipping the Product

code space, More Challenges, Linker Scripts, Code Space, Constants and Strings, Reading a Map File (Part 1), Reading a Map File (Part 1), Process of Elimination, Constants and Strings, Libraries, Functions and Macros, Constants and Strings, Lookup tables

reading a map file, Reading a Map File (Part 1), Reading a Map File (Part 1)

reducing, Process of Elimination, Constants and Strings, Libraries, Functions and Macros, Constants and Strings

constants and strings, Constants and Strings

functions and macros, Functions and Macros

libraries, Libraries

text segment in linker scripts, Linker Scripts

trading for speed, Lookup tables

code space memory, Updating Code

code storage mechanisms, Updating Code, Modifying the Resident Updater

code-read protection, Security

command pattern, Command and Response, Command Pattern, Creating a command, Creating a command, Invoking a command, Command Pattern, Changing Algorithms

changing algorithms, Changing Algorithms

commands with parameters, Creating a command

creating commands, Creating a command

how command handler works, Command Pattern

invoking commands, Invoking a command

communication, So Many Ways of Communicating, Ethernet and WiFi, So Many Ways of Communicating, Serial, Miscellaneous other protocols, Parallel, Ethernet and WiFi, Adding Robustness to the Communication, Changing Algorithms, Version everything and then checksum it, Authentication and encryption, Changing Data, Changing Algorithms

adding robustness to, Adding Robustness to the Communication, Changing Algorithms, Version everything and then checksum it, Authentication and encryption, Changing Data, Changing Algorithms

authentication and encryption, Authentication and encryption

changing algorithms, Changing Algorithms

changing data, Changing Data

using versions and checksums, Version everything and then checksum it

OSI layer model, So Many Ways of Communicating

parallel, Parallel

serial protocols, Serial, Miscellaneous other protocols

wireless, Ethernet and WiFi

compare (or match) register, Timer Pieces, Doing the Math, Doing the Math

equation for relationship with clock input, prescaler, and timer frequency, Doing the Math

heuristics for finding to provide timer frequency needed, Doing the Math

compilers, Compilers, Languages, and Object-Oriented Programming, Copy Loader to RAM, Process of Elimination, Functions and Macros, Minimize scope, Look at the assembly

optimizations, Process of Elimination, Minimize scope, Look at the assembly

limiting scope of variables, Minimize scope

for RAM usage, Look at the assembly

RAM allocated to loader program at certain base address, Copy Loader to RAM

suggestions to, Functions and Macros

components, Important Text for Software Developers, Evaluating Components Using the Datasheet, Evaluating Components Using the Datasheet, Reading a Schematic, Reading a Schematic, Reducing Power Consumption

choosing for low power, Reducing Power Consumption

evaluating, Evaluating Components Using the Datasheet

ID, in schematics, Reading a Schematic

looking at family of, Evaluating Components Using the Datasheet

revisions of, Important Text for Software Developers

on a schematic, Reading a Schematic

composition versus inheritance, Changing Algorithms

compression of data, Pixel displays

connectors in schematics, Reading a Schematic

const keyword, Reading a Map File (Part 1), Identifying Fast and Slow Operations

constants, Reading a Map File (Part 1), Constants and Strings, Identifying Fast and Slow Operations, Taking an Average, Dividing by a Constant, Scaling the Input

defining real constants, Identifying Fast and Slow Operations

division by, Dividing by a Constant

listed in map files, Reading a Map File (Part 1)

multiplying by, Scaling the Input

multiplying by a constant to take an average, Taking an Average

reducing number of, Constants and Strings

context, state machines, State Machines, State-Centric State Machine, State Pattern

changing, State-Centric State Machine

context object in state pattern, State Pattern

context, system, Save the Context, Restore the Context

restoring after calling ISRs, Restore the Context

saving before calling ISRs, Save the Context

control register, Timer Pieces

conversion to the frequency domain, Analog sensors

CPUs (central processing units), Terminology

CRCs (cyclic redundancy checks), Version everything and then checksum it

critical sections of code, Avoiding Race Conditions, Nonmaskable interrupts, Critical sections

avoiding race conditions, Critical sections

nonmaskable interrupts and, Nonmaskable interrupts

cross-compilers, Compilers, Languages, and Object-Oriented Programming

cross-debugger for embedded systems, Debugging

cryptography, Further Reading (see encryption)

CS (Chip Select) lines, SPI

current, Digital Multimeter, Troubleshooting, Understanding Power Consumption, Understanding Power Consumption

current mode on DMM, Digital Multimeter

driving motors, Troubleshooting

measuring consumption of, Understanding Power Consumption

power and, Understanding Power Consumption

D

DACs (digital-to-analog converters), A Sandbox to Play In, Analog sensors, Circular buffers, Changing Algorithms

ADC to DAC data-driven system, Circular buffers, Changing Algorithms

data handling, Data Handling, Adding Robustness to the Communication, Adding Robustness to the Communication, Authentication and encryption, Changing Data, Changing Algorithms, Copy Loader to RAM

changing algorithms for, Changing Algorithms

changing data, Changing Data

data processing buffer, Copy Loader to RAM

knowing that data received is data that was transmitted, Adding Robustness to the Communication, Authentication and encryption

data hiding, State of logging

data segment, executable code, Linker Scripts

data tables, state machines as, Table-Driven State Machine

data-driven systems, State Machines, Choosing a State Machine Implementation, Data Handling, Circular buffers, Circular buffers, Hardware FIFOs, Direct memory access, Comparison of buffering schemes, Changing Algorithms

ADC to DAC, Changing Algorithms

circular buffers, Circular buffers, Circular buffers

comparison of buffering schemes, Comparison of buffering schemes

hardware FIFO buffers, Hardware FIFOs

processors supporting DMA, Direct memory access

state machines, State Machines, Choosing a State Machine Implementation

datasheets, Reading a Datasheet, Your Processor Is a Language, Important Text for Software Developers, Evaluating Components Using the Datasheet, Your Processor Is a Language

evaluating components with, Evaluating Components Using the Datasheet

important text for software developers, Important Text for Software Developers

DC motors, Motors

debouncing, Debouncing Switches, Debouncing Switches, Further Reading

resources for further information, Further Reading

debugging, Debugging, Debugging, Having a Debugging Toolbox (and a Fire Extinguisher), Debugging Timing Errors, Troubleshooting, Watchdog, Constants and Strings, Reading a Map File (Part 2)

(see also bugs)

cross-debugger for embedded systems, Debugging

disabling watchdog for, Watchdog

having a debugging toolbox, Having a Debugging Toolbox (and a Fire Extinguisher)

removing debug strings from code space, Constants and Strings

stack overflow, Reading a Map File (Part 2)

timing errors, Debugging Timing Errors

toggled output LED, Troubleshooting

deep hibernation or power down setting, Putting the Processor to Sleep

deep sleep or light hibernation setting, Putting the Processor to Sleep

#define statements in C, An IRQ Happens, Reading a Map File (Part 1)

#define statements in C, Identifying Fast and Slow Operations

dependency injection, Dependency Injection

DES encryption, Authentication and encryption

design patterns, Preface, Further Reading, Adapter Pattern, Pattern: Singleton, A Sandbox to Play In, Further Reading, Command Pattern, Facade Pattern, State Machines, State Pattern, A Very Small Scheduler, A Very Small Scheduler, A Very Small Scheduler, Changing Data, Changing Data, Changing Algorithms, Changing Algorithms, Further Reading

adapter, Adapter Pattern

command, Command Pattern

facade, Facade Pattern

factory, Changing Data

flyweight factory, Changing Data

MVC (Model-View-Controller), A Sandbox to Play In

observer, A Very Small Scheduler

publish/subscribe, A Very Small Scheduler, A Very Small Scheduler

resources for further information, Further Reading, Further Reading, Further Reading

singleton, Pattern: Singleton

state, State Machines, State Pattern

strategy, Changing Algorithms

template, Changing Algorithms

Design Patterns: Elements of Reusable Object-Oriented Software, Further Reading, Further Reading, Further Reading

development kits, Ideal Project Flow, Your Processor Is a Language

documentation, Your Processor Is a Language

mitigating risks, Ideal Project Flow

digital multimeter (DMM), Digital Multimeter, Understanding Power Consumption

measuring current, Understanding Power Consumption

digital sensors, Digital sensors

digital signal processors, Terminology (see DSPs)

digital-to-analog converters, Analog sensors (see DACs)

Direct Memory Access (DMA), Direct memory access, Comparison of buffering schemes, Comparison of buffering schemes, Further Reading

comparison with other buffering methods, Comparison of buffering schemes

ping-pong buffers, Comparison of buffering schemes

resources for further information, Further Reading

disabling interrupts, Disabling interrupts, Critical sections, Critical sections, Hardware FIFOs

avoiding race conditions, Critical sections

in critical sections, Critical sections

effect on latency, Hardware FIFOs

displays, Displays, Pixel displays, Segmented displays, Pixel displays, One last look at function chains

LCD display drivers, One last look at function chains

pixel, Pixel displays

segmented, Segmented displays

distributed sensor networks, Pixel displays

division, Reading a Map File (Part 1), Identifying Fast and Slow Operations, Identifying Fast and Slow Operations, Taking an Average, Taking an Average, Dividing by a Constant, Multiplication (and Division)

calculation of signal average, Taking an Average

by a constant, Dividing by a Constant

of fake floating-point values, Multiplication (and Division)

integer division truncating results, Taking an Average

using a library, Reading a Map File (Part 1)

using bitwise logic operations for, Identifying Fast and Slow Operations

DMA, Direct memory access (see Direct Memory Access)

DMM, Digital Multimeter (see digital multimeter)

drivers, Driver Interface: Open, Close, Read, Write, IOCTL, Adapter Pattern, Your Processor Is a Language, Changing Data

example code included with processors, Your Processor Is a Language

factory pattern, Changing Data

implementation of adapter pattern, Adapter Pattern

standard interface, Driver Interface: Open, Close, Read, Write, IOCTL

DSPs (digital signal processors), Terminology, Analog sensors, Further Reading, Identifying Fast and Slow Operations

further reading on, Further Reading

multiplication on, Identifying Fast and Slow Operations

dynamic memory allocation, Nonmaskable interrupts, Remove malloc

eliminating, Remove malloc

on heap, Nonmaskable interrupts

E

EEPROMs, Typical calls needed for logging, Flash Test Example, External Memory, Version everything and then checksum it, Version everything and then checksum it

adding version and checksum to, Version everything and then checksum it

errors in, Version everything and then checksum it

flash memory versus, Flash Test Example

programmed in manufacturing, Typical calls needed for logging

electromagnetic compatibility (EMC) testing, Digital sensors

electromagnetic interface (EMI), Digital sensors

embedded systems, defined, Introduction

emulators, ICE (in-circuit emulator), Debugging

encapsulation, Principles to Confront Those Challenges, From Diagram to Architecture, Adapter Pattern, Changing Data

of modules, From Diagram to Architecture

using adapter pattern, Adapter Pattern

using factory flyweight pattern, Changing Data

using in embedded systems, Principles to Confront Those Challenges

encoders, Position encoding

encryption, Authentication and encryption, Further Reading, Security, Lookup tables

loading new code, Security

lookup tables for, Lookup tables

resources for further information, Further Reading

endian-ness, Reduce math in loops

energy efficiency, Motors, Interrupt-Based Code Flow Model

(see also power consumption)

of motors, Motors

energy equation, Understanding Power Consumption

energy, consumption of, Chained Processors (see power consumption)

enqueue function, Circular buffers

Enter function, state pattern, State Pattern

erasing old code, Dangerous Time: Erase and Program

errors, Dealing with Errors, Debugging Timing Errors, Consistent Methodology, Consistent Methodology, Consistent Methodology, Error-Handling Library, Debugging Timing Errors, Further Reading, System Tick, Watchdog, PID control, Version everything and then checksum it, Scaling the Input, Determining the Error, Determining the Error

in addition and multiplication of fake floating-point numbers, Determining the Error, Determining the Error

catching with checksums, Version everything and then checksum it

consistent methodology for handling, Consistent Methodology

debugging timing errors, Debugging Timing Errors

error codes, Consistent Methodology

error-handling library, Error-Handling Library

fencepost, System Tick

handling with watchdog, Watchdog

interview question, talking about failures, Further Reading

minimizing number and types of, Consistent Methodology

scaling input with Taylor series, Scaling the Input

steady state, PID control

Ethernet, Ethernet and WiFi

EventGo function, state pattern, State Pattern

events, Event-Centric State Machine, Table-Driven State Machine, Time-Based Events, Timer profiler 2 (response timer)

event handlers in table-driven state machines, Table-Driven State Machine

event-centric state machines, Event-Centric State Machine

responding to, timer profiler for, Timer profiler 2 (response timer)

time-based, avoiding use of interrupts for, Time-Based Events

EventStop function, state pattern, State Pattern

exclusive or (XOR) bitwise operator (^), I/O-Handling Code

Exit function, state pattern, State Pattern

F

fabrication of the board, Ideal Project Flow

facade pattern, Facade Pattern

factoring polynomials, Factor Polynomials

factory pattern, Changing Data

failure, talking about, Further Reading

Faraday cage, Digital sensors

fencepost error, System Tick

FIFO (first in, first out), Circular buffers, Hardware FIFOs, Comparison of buffering schemes, Further Reading

circular buffers, Circular buffers

DMA versus FIFO buffers, Further Reading

hardware FIFO buffers, Hardware FIFOs, Comparison of buffering schemes

comparison to other buffering schemes, Comparison of buffering schemes

fill sections in map files, Reading a Map File (Part 1)

finite state automata, state machines as, State Machines

fixed-point processors, Debugging, External Memory, Fake Floating-Point Numbers

external memory device, External Memory

floating-point math on, Fake Floating-Point Numbers

running code from, Debugging

flash memory, The Block Diagram, Building Tests, Flash Test Example, Test wrap-up, External Memory, Version everything and then checksum it

adding version and checksum to, Version everything and then checksum it

schematic snippet for, Building Tests

testing (example), Flash Test Example, Test wrap-up

flashing the chip, Updating Code

floating input line, Reading a Schematic

floating point unit (FPU), Identifying Fast and Slow Operations

floating-point numbers, Libraries, Identifying Fast and Slow Operations, Fake Floating-Point Numbers, Determining the Error, Precision, Addition (and Subtraction), Multiplication (and Division), Determining the Error, Determining the Error

faking, Fake Floating-Point Numbers, Determining the Error, Precision, Addition (and Subtraction), Multiplication (and Division), Determining the Error, Determining the Error

addition and subtraction of values, Addition (and Subtraction)

determining error in addition and multiplication, Determining the Error, Determining the Error

multiplication and division of values, Multiplication (and Division)

precision and, Precision

replacing with fixed-point representations, Libraries

slowness and cost of operations on, Identifying Fast and Slow Operations

flow charts, representing state machines, State Machines

flow control, in serial communication, RS-232 and TTL

flow of activity, managing, Managing the Flow of Activity, Further Reading, Scheduling and Operating System Basics, Priority Inversion, State Machines, Choosing a State Machine Implementation, Interrupts, Interrupts, When to Use Interrupts, An IRQ Happens

(see also interrupts; state machines)

interrupts, Interrupts, When to Use Interrupts, An IRQ Happens

occurrence of an IRQ, An IRQ Happens

scheduling and operating system basics, Scheduling and Operating System Basics, Priority Inversion

using state machines, State Machines, Choosing a State Machine Implementation

flyweight factory pattern, Changing Data

fonts, Pixel displays

forums, for users of processors, Your Processor Is a Language

fractional numbers, Rational Numbers, Addition (and Subtraction), Multiplication (and Division)

adding and subtracting, Addition (and Subtraction)

multiplying and dividing, Multiplication (and Division)

fragmentation, heap, Remove malloc

frequency, Timer Pieces, Analog sensors, Analog sensors

ADCs and DACs, Analog sensors

conversion to the frequency domain, Analog sensors

setting for a timer, Timer Pieces

frequency scaling, Slowing Down to Conserve Energy

full duplex peripheral bus, Serial

function chains, Function Chains, One last look at function chains

optimizing, One last look at function chains

function pointers, Creating a command, Creating a command, Creating a command, Invoking a command, Dependency Injection, Run the Loader

for code loader in RAM, Run the Loader

command pattern, Creating a command, Invoking a command

dependency injection, Dependency Injection

performance and, Creating a command

function register, An IRQ Happens

functions, Command and Response, Invoking a command, Consistent Methodology, Nonmaskable interrupts, Calculating system latency, Reentrant functions, Restore the Context, Reading a Map File (Part 1), Functions and Macros, Remove malloc, Function parameters, Timer profiler 1 (function timer), Timer profiler 2 (response timer)

called during interrupts, Calculating system latency

handling errors, Consistent Methodology

interrupt, Restore the Context

listed in map files, Reading a Map File (Part 1)

malloc and, Remove malloc

parameters, Function parameters

pointers, Command and Response (see function pointers)

profiling, Timer profiler 1 (function timer), Timer profiler 2 (response timer)

timer profiler for response time, Timer profiler 2 (response timer)

receiver, to perform commands, Invoking a command

reducing in code space, Functions and Macros

reentrant, Reentrant functions

stack frame for every function call, Nonmaskable interrupts

G

G2231 processor, Interrupt-Based Code Flow Model

general I/O (GIO) pins, Toggling an Output

general-purpose I/O (GPIO) pins, Toggling an Output

getting started guides, Your Processor Is a Language

global interrupt-enable, An IRQ Happens

global variables, State of logging, State of logging, Pattern: Singleton, Avoiding Race Conditions, Reentrant functions, Linker Scripts, Reading a Map File (Part 1), Reading a Map File (Part 2), Pros and Cons of Globals

data segment in linker script, Linker Scripts

in interrupts, Avoiding Race Conditions

listed in map file, Reading a Map File (Part 1)

nonreentrant functions using, Reentrant functions

pros and cons of, Pros and Cons of Globals

reducing RAM consumed by, Reading a Map File (Part 2)

replacing with a parameter, State of logging

singleton pattern, Pattern: Singleton

static keyword, State of logging

glyphs, Pixel displays

GNU linker for the LPC 13xx, Reading a Map File (Part 1)

graceful degradation, Dealing with Errors

graphic assets, Pixel displays, Pixel displays, Pixel displays, Changing Data, Changing Algorithms

displaying on screen, steps in process, Pixel displays

in factory flyweight pattern, Changing Data, Changing Algorithms

packing into storage, Pixel displays

H

hackers, protecting data from, Authentication and encryption

Hacker’s Delight, Further Reading, Further Reading

half duplex peripheral bus, Serial

hardware, Introduction, Getting Your Hands on the Hardware, Further Reading, Hardware/Software Integration, Board Bring-Up, Hardware/Software Integration, Board Bring-Up, Board Bring-Up, Reading a Datasheet, Your Processor Is a Language, Important Text for Software Developers, Evaluating Components Using the Datasheet, Your Processor Is a Language, Reading a Schematic, Reading a Schematic, Having a Debugging Toolbox (and a Fire Extinguisher), Toolbox, Digital Multimeter, Oscilloscopes and Logic Analyzers, Testing the Hardware (and Software), Command Pattern, Testing the Hardware (and Software), Command and Response, Dealing with Errors, Further Reading, Further Reading, Separating the Hardware from the Action, Serial, Hardware FIFOs, Further Reading

analyzing signals with oscilloscopes and logic analyzers, Oscilloscopes and Logic Analyzers

constraints in embedded systems, Introduction

dealing with errors, Dealing with Errors

digital multimeter (DMM) for, Digital Multimeter

FIFO buffers, Hardware FIFOs

integration with software, Hardware/Software Integration, Board Bring-Up, Hardware/Software Integration, Board Bring-Up

board bring-up, Board Bring-Up

ideal project flow, Hardware/Software Integration

interview question, talking about failure, Further Reading

keeping your board safe, Having a Debugging Toolbox (and a Fire Extinguisher)

learning your processor, Your Processor Is a Language

lines needed by peripheral bus, Serial

making each component individually testable, Board Bring-Up

reading datasheets, Reading a Datasheet, Your Processor Is a Language, Important Text for Software Developers, Evaluating Components Using the Datasheet

evaluating components, Evaluating Components Using the Datasheet

important text for software developers, Important Text for Software Developers

reading schematics, Reading a Schematic, Reading a Schematic

resources for further information, Further Reading, Further Reading

separating from the action, Separating the Hardware from the Action

testing, Testing the Hardware (and Software), Command Pattern, Testing the Hardware (and Software), Command and Response

command and response, Command and Response

unit tests, Testing the Hardware (and Software)

toolbox for adjustments to boards, Toolbox

Head First Design Patterns, Further Reading, Further Reading

headers for memory map of chip, Set the Pin to Be an Output, Turn On the LED, Board-Specific Header File, An IRQ Happens

accessing registers via, Turn On the LED

interrupts, An IRQ Happens

using board-specific header files for pin modifications, Board-Specific Header File

heap, Nonmaskable interrupts, Remove malloc, Remove malloc, Reading a Map File (Part 2)

allocation for, Reading a Map File (Part 2)

dynamic memory allocation and, Remove malloc

fragmentation, Remove malloc

help command, Creating a command

hexadecimal numbers, Starting with Registers

hibernation, Putting the Processor to Sleep, Putting the Processor to Sleep

light or deep, Putting the Processor to Sleep

sleep versus, Putting the Processor to Sleep

hierarchy of control software diagram, Hierarchy of Control

home sensor on motors, Position encoding

Horner's scheme, Factor Polynomials, Taylor Series

using with sine function in Taylor series, Taylor Series

Horowitz, Paul, Further Reading

host computers, Pixel displays, RS-232 and TTL

and other processors, Pixel displays

serial connection from embedded system to PC, RS-232 and TTL

Housekeeping function, state object, State Pattern

I

I/O, Board-Specific Header File, I/O-Handling Code, Main Loop, Facade Pattern, The Input in I/O, Dependency Injection, Buttons and Key Matrices, Motors, SPI, Parallel, I/O lines and an O-scope, Turn Off Unused I/O devices

(see also inputs; outputs; pins)

code for handling, I/O-Handling Code

code in man function loop, Main Loop

facade pattern for simplified interface, Facade Pattern

lines used by bit-bang interface for communication, SPI

matrixing inputs and outputs with key matrices, Buttons and Key Matrices

multiple lines for stepper motors, Motors

parallel communication with peripherals over I/O lines, Parallel

passing I/O handler to LED in initialization code, Dependency Injection

profiling using I/O lines and an oscilloscope, I/O lines and an O-scope

turning off unused devices to reduce power consumption, Turn Off Unused I/O devices

using board-specific header files, Board-Specific Header File

I/O pins, Toggling an Output

idle or sleep setting, Putting the Processor to Sleep

if statements, Memory timing, Reduce math in loops

keeping out of loops, Reduce math in loops

image update, synchronizing with screen refresh, Pixel displays

images, Pixel displays

in-circuit emulator (ICE), Debugging

indexing, lookup tables, Implicit input

indirect registers, An IRQ Happens

inheritance, in object-oriented programming, Changing Algorithms

inline functions, Functions and Macros

inputs, Reading a Schematic, The Input in I/O, A Simple Interface to a Button, Momentary Button Press, Interrupt on a Button Press, Debouncing Switches, Scaling the Input, Implicit input, Explicit input in the table, Fake Floating-Point Numbers

explicit lookup table for, Explicit input in the table

floating, Reading a Schematic

implicit input lookup tables, Implicit input

momentary button press, Momentary Button Press, Interrupt on a Button Press, Debouncing Switches

debouncing switches, Debouncing Switches

interrupt on, Interrupt on a Button Press

scaling, Scaling the Input

simple interface to a button, A Simple Interface to a Button

standard input functions, floating-point handling in, Fake Floating-Point Numbers

integer math, problems with, Taking an Average

inter-integrated circuit (I2C), I2C

interfaces, Driver Interface: Open, Close, Read, Write, IOCTL, Adapter Pattern, Example: A Logging Interface, A Simple Interface to a Button, Interrupts, Pixel displays, SPI

adapters for, Adapter Pattern

bit-bang, SPI

driver interface, Driver Interface: Open, Close, Read, Write, IOCTL

logging interface (example), Example: A Logging Interface

similarity of APIs, Interrupts

simple interface to a button, A Simple Interface to a Button

system's interface to host computer, Pixel displays

interpolate function, Linear interpolation

interrupt keyword, Restore the Context

interrupt latency, Save the Context

interrupt numbers in IVT, Looking up the ISR

interrupt register, Timer Pieces

interrupt requests (IRQs), Interrupts, An IRQ Happens, Multiple sources for one interrupt, Interrupt priority, Nested interrupts, Nonmaskable interrupts

occurrence of, An IRQ Happens, Multiple sources for one interrupt, Interrupt priority, Nested interrupts, Nonmaskable interrupts

interrupt priority, Interrupt priority

multiple sources for an interrupt, Multiple sources for one interrupt

nested interrupts, Nested interrupts

non-maskable interrupts, Nonmaskable interrupts

interrupt service routines (ISRs), A Simple Interface to a Button, Interrupts, Multiple sources for one interrupt, Save the Context, Reentrant functions, Get the ISR from the Vector Table, Calling the ISR, Calling the ISR, Disabling interrupts

calling, Calling the ISR, Disabling interrupts

disabling interrupts, Disabling interrupts

functions using global variables without protecting them against interrupts, Reentrant functions

getting from vector table, Get the ISR from the Vector Table

important rules for, Calling the ISR

sources of interrupts, Multiple sources for one interrupt

time between IRQ and, Save the Context

interrupt vector table (IVT), Get the ISR from the Vector Table, Initializing the vector table, Looking up the ISR

initializing, Initializing the vector table

looking up the ISR, Looking up the ISR

interrupt-based code flow model, Interrupt-Based Code Flow Model

interrupts, A Simple Interface to a Button, A Simple Interface to a Button, Interrupt on a Button Press, Configuring the Interrupt, Timer Pieces, A Long Wait Between Timer Ticks, Using Pulse-Width Modulation, Interrupts, A Very Small Scheduler, An IRQ Happens, An IRQ Happens, Multiple sources for one interrupt, Interrupt priority, Nested interrupts, Nonmaskable interrupts, Save the Context, Reentrant functions, Get the ISR from the Vector Table, Calling the ISR, Restore the Context, When to Use Interrupts, How Not to Use Interrupts, A Very Small Scheduler, Watchdog, Circular buffers, Hardware FIFOs, Comparison of buffering schemes, Dangerous Time: Erase and Program, Sampling profiler, A Closer Look at the Main Loop

avoiding use of, How Not to Use Interrupts, A Very Small Scheduler

on button press, Interrupt on a Button Press, Configuring the Interrupt

calling the ISR, Calling the ISR

calling when timer expires, A Long Wait Between Timer Ticks

circular buffers and, Circular buffers

disabling before erasing old code, Dangerous Time: Erase and Program

for different buffering schemes, Comparison of buffering schemes

getting ISR from vector table, Get the ISR from the Vector Table

ISR (interrupt service routine), A Simple Interface to a Button

masking, An IRQ Happens

multiple sources for an interrupt, Multiple sources for one interrupt

nested, Nested interrupts

non-maskable, Nonmaskable interrupts

ping-ponging between two timer interrupts, Using Pulse-Width Modulation

priority of, Interrupt priority

processing in main loop, A Closer Look at the Main Loop

profiler timer interrupts, Sampling profiler

restoring the context, Restore the Context

saving the context before calling ISR, Save the Context, Reentrant functions

timer, Timer Pieces

transmit and receive FIFO, Hardware FIFOs

turning on, steps in, An IRQ Happens

using a watchdog, Watchdog

when to use, When to Use Interrupts

invoker, in command handler, Command Pattern

ioctl (inpt/output (I/O) control, Driver Interface: Open, Close, Read, Write, IOCTL

ISO-7816 protocol, Miscellaneous other protocols

ISRs, A Simple Interface to a Button (see interrupt service routines)

IVT, Get the ISR from the Vector Table (see interrupt vector table)

J

jitter, System Tick, Slowing Down to Conserve Energy

JTAG interface, Debugging, Authentication and encryption

protecting code from being read via, Authentication and encryption

K

key matrices, Buttons and Key Matrices, Segmented displays

using for segmented display output, Segmented displays

using with buttons, Buttons and Key Matrices

Knuth, Donald E., Principles to Confront Those Challenges, Use an Existing Algorithm, Further Reading

L

latency, Avoiding Race Conditions, Save the Context, Calculating system latency, Hardware FIFOs, Comparison of buffering schemes

calculating system latency, Calculating system latency

and communication, Hardware FIFOs, Comparison of buffering schemes

interrupt, Save the Context

layered software architecture diagram, Layered View

layout of board, Ideal Project Flow

LCDs, Segmented displays, Pixel displays, Pixel displays, Parallel, One last look at function chains

automatic refresh, Pixel displays

displays, Segmented displays

drivers, One last look at function chains

organizing display assets, Pixel displays

parallel interfaces for, Parallel

lead times, Evaluating Components Using the Datasheet

LEDs, Reading a Schematic, Set the Pin to Be an Output, Facade Pattern, Dependency Injection, Buttons and Key Matrices, Segmented displays

Charlieplexing used with, Buttons and Key Matrices

facade for blinking LED, Facade Pattern

in schematics, Reading a Schematic

removing dependency on I/O pins with dependency injection, Dependency Injection

schematic of processor with LED attached, Set the Pin to Be an Output

segmented displays, Segmented displays

libraries, Reading a Map File (Part 1), Reading a Map File (Part 1), Libraries

functions listed in map files, Reading a Map File (Part 1)

modules included in build, Reading a Map File (Part 1)

reducing in code space, Libraries

life span, of motors, Motors

LIFO (last in, first out), stacks, Circular buffers

linear interpolation, Linear interpolation

linker scripts, Linker Scripts, Summary, Linker Scripts, Linker Scripts, Linker Scripts, Reading a Map File (Part 1), Reading a Map File (Part 1), Reading a Map File (Part 2), Reading a Map File (Part 2)

defining size and addresses of memory so linker can give errors, Linker Scripts

heap and stack allocation in, Reading a Map File (Part 2)

map file information on RAM, Reading a Map File (Part 2)

modifying to move code or place buffers at certain memory addresses, Linker Scripts

output map file, Reading a Map File (Part 1), Reading a Map File (Part 1)

segments in resulting executable code, Linker Scripts

linker variables, Copy Loader to RAM

loaders, Updating Code, Updating Code, Onboard Bootloader, Modifying the Resident Updater, Brick Loader, Reset to New Code, Reset to New Code, Security, Summary

(see also updating code)

brick loader, Brick Loader, Reset to New Code

important points about, Reset to New Code

modifying resident updater, Modifying the Resident Updater

on-board bootloader, Onboard Bootloader

questions about design, Summary

security and bootloaders, Security

types of, Updating Code

loading code, interview question about, Summary

local variables, Remove malloc, Reading a Map File (Part 2), Registers and Local Variables

in long function call chains, Reading a Map File (Part 2)

RAM consumed by, Registers and Local Variables

logging, Constants and Strings, Timer profiler 1 (function timer)

profiling and, Timer profiler 1 (function timer)

turning debugging on and off per subsystem at runtime, Constants and Strings

logging interface (example), Example: A Logging Interface, Typical calls needed for logging, State of logging, Pattern: Singleton, Sharing private globals

sharing private globals, Sharing private globals

state of logging, State of logging

typical calls for logging, Typical calls needed for logging

using singleton pattern, Pattern: Singleton

logic analyzers, Oscilloscopes and Logic Analyzers

lookup tables, Lookup tables, Lookup Tables, Implicit input, Implicit input, Linear interpolation, Explicit input in the table

adding half step to indexing to center result, Implicit input

explicit input, Explicit input in the table

implicit input, Implicit input

using linear interpolation, Linear interpolation

loops, Timer profiler 2 (response timer), Reduce math in loops, Loop unrolling

loop unrolling, Loop unrolling

reducing math in, Reduce math in loops

in timer profiler, Timer profiler 2 (response timer)

M

macros, Creating a command, Command Pattern, Disabling interrupts, Functions and Macros, One last look at function chains, Lookup tables

command pattern, Creating a command, Command Pattern

converting functions to, One last look at function chains

for disabling interrupts, Disabling interrupts

trading code space for speed, Lookup tables

main function, Hierarchy of Control, Main Loop, Watchdog, Reading a Map File (Part 1), A Closer Look at the Main Loop

building a system hierarchy, Hierarchy of Control

I/O handling code in, Main Loop

interrupt processing in, A Closer Look at the Main Loop

profiler example, Reading a Map File (Part 1)

watchdog, Watchdog

Make: Electronics, Further Reading

malloc, removing, Remove malloc

map files, reading, Reading a Map File (Part 1), Reading a Map File (Part 1), Reading a Map File (Part 1), Reading a Map File (Part 1), Reading a Map File (Part 1), Libraries, Reading a Map File (Part 2), Sampling profiler, Identifying Fast and Slow Operations

checking size of a library, Libraries, Identifying Fast and Slow Operations

constants, Reading a Map File (Part 1)

finding information on code space, Reading a Map File (Part 1)

finding information on RAM, Reading a Map File (Part 2)

functions and variables cluttering code, Reading a Map File (Part 1)

with the sampling profiler, Sampling profiler

masking an interrupt, An IRQ Happens, Nonmaskable interrupts

non-maskable interrupts, Nonmaskable interrupts

master clock, Timer Pieces

Master-In Slave-Out (MISO) lines, SPI

Master-Out Slave-In (MOSI) lines, SPI

match control register, An IRQ Happens

math, Doing the Math, A Long Wait Between Timer Ticks, Circular buffers, Reduce math in loops, Math, Further Reading, Math, Identifying Fast and Slow Operations, Taking an Average, Taking an Average, Use an Existing Algorithm, Use an Existing Algorithm, Designing and Modifying Algorithms, Explicit input in the table, Fake Floating-Point Numbers, Determining the Error, Precision, Addition (and Subtraction), Further Reading

accuracy versus precision, Math

adding signed numbers in binary, Circular buffers

calculating timer frequency, Doing the Math, A Long Wait Between Timer Ticks

designing and modifying algorithms, Designing and Modifying Algorithms, Explicit input in the table

downside of integer math, Taking an Average

faking floating-point numbers, Fake Floating-Point Numbers, Determining the Error, Precision, Addition (and Subtraction)

addition and subtraction of faked values, Addition (and Subtraction)

precision and, Precision

identifying fast and slow operations, Identifying Fast and Slow Operations, Taking an Average

calculating an average, Taking an Average

interview question, handling very large numbers, Further Reading

reducing in loops, Reduce math in loops

using an existing algorithm, Use an Existing Algorithm, Use an Existing Algorithm

matrices, Buttons and Key Matrices, Segmented displays

buttons and key matrices, Buttons and Key Matrices

using for segmented display outputs, Segmented displays

memory, More Challenges, Reading a Schematic, Flash Test Example, Test wrap-up, Starting with Registers, Communication Between Tasks, Nonmaskable interrupts, Nonmaskable interrupts, External Memory, Parallel, Direct memory access, Updating Code, Linker Scripts, Linker Scripts, Linker Scripts, Remove malloc, Memory timing

code space memory, Updating Code

creating buffers at hardcoded addresses, Linker Scripts

Direct Memory Access (DMA), Direct memory access

eliminating dynamic memory allocation, Remove malloc

example memory map, Linker Scripts

external, External Memory, Parallel

parallel communication with, Parallel

heaps, Nonmaskable interrupts

hexadecimal values for, Starting with Registers

in schematics, Reading a Schematic

linker script representing memory map, Linker Scripts

nonvolatile, Flash Test Example

optimizing timing, Memory timing

shared between tasks, Communication Between Tasks

stacks and, Nonmaskable interrupts

tests for, Test wrap-up

memory overlays, Memory Overlays

memory-mapped registers, An IRQ Happens (see registers)

microcontrollers, Terminology

microelectromechanical systems (MEMS), Analog sensors

microprocessors, Outputs, Inputs, and Timers (see processors)

MISO (Master-In Slave-Out) lines, SPI

mixed signal oscilloscopes, Oscilloscopes and Logic Analyzers

model, in MVC pattern, A Sandbox to Play In

Model-View-Controller pattern, A Sandbox to Play In (see MVC pattern)

modulo math, Identifying Fast and Slow Operations

MOSI (Master-Out Slave-In) lines, SPI

motors, Motors, PID control, Motors, Position encoding, PID control, Displays, Further Reading

brushless, Motors

control of, Further Reading

PID control, PID control, Displays

position encoding, Position encoding

MSP430 line of processors (TI), Further Reading

multiplication, Identifying Fast and Slow Operations, Identifying Fast and Slow Operations, Multiplication (and Division)

of fake floating-point values, Multiplication (and Division)

using bitwise shift operations for, Identifying Fast and Slow Operations

multipoint protocols, I2C

mutual exclusion (mutex), Avoiding Race Conditions

MVC (Model-View-Controller) pattern, A Sandbox to Play In, A Sandbox to Play In, A Sandbox to Play In

different interpretations of the parts, A Sandbox to Play In

using in a sandbox, A Sandbox to Play In

N

names of pins, Datasheet Sections You Need When Things Go Wrong

namespaces, State of logging, Minimize scope

nested interrupts, Nested interrupts, Disabling interrupts

disabling, Disabling interrupts

network analyzers, Oscilloscopes and Logic Analyzers

network layer, OSI model, So Many Ways of Communicating

networking protocols, Ethernet and WiFi

networks, sensor, Pixel displays

nibbles, Starting with Registers

noise, Analog sensors, Digital sensors, Digital sensors

analog systems and, Digital sensors

finding sources of, Digital sensors

non-maskable interrupts, Nonmaskable interrupts

nonvolatile memory, Flash Test Example, External Memory

Numerical Recipes, Third Edition: The Art of Scientific Computing, Further Reading

NXP LPC 13xx User Manual, 32-bit ARM Cortex microcontroller, Toggling an Output

NXP’s LPC17xx processor, interrupt handling, An IRQ Happens

O

object-oriented programming, Compilers, Languages, and Object-Oriented Programming, The Block Diagram, State of logging, Pattern: Singleton, Creating a command, Dependency Injection, State Machines, State Pattern, Changing Data, Changing Algorithms, Changing Algorithms, Changing Algorithms

in C, State of logging

dependency injection, Dependency Injection

embedded system block diagrams, The Block Diagram

factory flyweight pattern, Changing Data

function pointers, Creating a command

inheritance in, Changing Algorithms

singleton pattern, Pattern: Singleton

state machines, State Machines, State Pattern

strategy pattern, Changing Algorithms

template pattern, Changing Algorithms

observer pattern, A Very Small Scheduler

Ohm's law, Understanding Power Consumption, Understanding Power Consumption

1-wire bus, 1-wire

Open Systems Interconnection (OSI) model, So Many Ways of Communicating

operating systems, About This Book, Scheduling and Operating System Basics, Priority Inversion

embedded systems and, About This Book

functions of, Scheduling and Operating System Basics, Priority Inversion

Operating Systems: Design and Implementation, Further Reading

optimizations, Principles to Confront Those Challenges, Process of Elimination, Optimizing, Coding in assembly language, Memory timing, Variable size, One last look at function chains, Consider the instructions, Reduce math in loops, Loop unrolling, Loop unrolling, Lookup tables, Coding in assembly language, Summary, Summary, Further Reading, Math, Further Reading

(see also math)

assembly language code, Coding in assembly language

considering instructions, Consider the instructions

explaining to business manager, Loop unrolling

function chains, One last look at function chains

interview question, reversing bits in a byte, Further Reading

lookup tables, Lookup tables

loop unrolling, Loop unrolling

making code smaller, Process of Elimination

memory timing, Memory timing

reducing math in loops, Reduce math in loops

resources for further information, Summary, Further Reading

as the root of all evil, Principles to Confront Those Challenges

summary of process, Summary

variable size, Variable size

organizational diagram of software architecture, Hierarchy of Control

oscillators, Timer Pieces, Timer Pieces, SPI, Slowing Down to Conserve Energy

communication precision requirements, SPI

different accuracies, Slowing Down to Conserve Energy

internal RC oscillation as clock source in microcontrollers, Timer Pieces

system statistics, Timer Pieces

oscilloscopes, Oscilloscopes and Logic Analyzers, Oscilloscopes and Logic Analyzers, Setting up a scope, Further Reading, I/O lines and an O-scope

mixed signal, Oscilloscopes and Logic Analyzers

resources for further information, Further Reading

setting up, Setting up a scope

traditional, Oscilloscopes and Logic Analyzers

using in profiling, I/O lines and an O-scope

OSI (Open Systems Interconnection) model, So Many Ways of Communicating

outputs, Toggling an Output, Troubleshooting, Dependency Injection, PID control, Fake Floating-Point Numbers

LED toggling example, Toggling an Output, Troubleshooting

of PID controller, PID control

removing LED dependency on I/O pins using dependency injection, Dependency Injection

standard output functions, floating-point handling in, Fake Floating-Point Numbers

P

parallel communication, Parallel, Ethernet and WiFi

parity, RS-232 and TTL

part numbers, in schematics, Reading a Schematic

PB&J, breading down process of making, Reduce math in loops

PCBAs (assembled printed circuit boards), Ideal Project Flow

PCBs (printed circuit boards), Ideal Project Flow

period, timer frequency and, Reading a Datasheet, Doing the Math, Using Pulse-Width Modulation, Slowing Down to Conserve Energy

Late Cretaceous, Reading a Datasheet

pulse-width modulation (PWM), Using Pulse-Width Modulation

synchronizing clocks of different accuracies, Slowing Down to Conserve Energy

peripherals, More Challenges, The Block Diagram, Reading a Datasheet, Your Processor Is a Language, An IRQ Happens, Interrupt priority, Looking up the ISR, Communicating with Peripherals, Further Reading, The Wide Reach of Peripherals, External Memory, Buttons and Key Matrices, Sensors, Digital sensors, Actuators, PID control, Displays, Pixel displays, So Many Ways of Communicating, Putting Peripherals and Communication Together, Adding Robustness to the Communication, Turn Off Peripherals

actuators, Actuators, PID control

block diagrams, The Block Diagram

buttons and key matrices, Buttons and Key Matrices

communicating with, So Many Ways of Communicating (see communication)

data handling, Putting Peripherals and Communication Together, Adding Robustness to the Communication

defined, The Wide Reach of Peripherals

displays, Displays, Pixel displays

external memory, External Memory

interrupts, An IRQ Happens, Interrupt priority, Looking up the ISR

priority by, Interrupt priority

reading datasheets, Reading a Datasheet, Your Processor Is a Language

sensors, Sensors, Digital sensors

turning off to reduce power consumption, Turn Off Peripherals

physical layer, OSI model, So Many Ways of Communicating

PID (proportional, integral, and derivative) control, PID control, Displays

ping-pong buffers, Direct memory access, Comparison of buffering schemes, Remove malloc

comparison to other buffering schemes, Comparison of buffering schemes

pins, Datasheet Sections You Need When Things Go Wrong, Toggling an Output, Set the Pin to Be an Output, Troubleshooting, Board-Specific Header File, I/O-Handling Code, The Input in I/O, The Input in I/O, Configuring the Interrupt, Runtime Uncertainty, Dependency Injection, Using Pulse-Width Modulation

configuring I/O pin for interrupt, Configuring the Interrupt

datasheet information on, Datasheet Sections You Need When Things Go Wrong

digital states of input and output pins, Toggling an Output

I/O, checking configuration, Troubleshooting

I/O-handling code for, I/O-Handling Code

initializing as outputs, Runtime Uncertainty

input, pull-up resistors, The Input in I/O

modifications of, using board-specific header file, Board-Specific Header File

PWM outputs, Using Pulse-Width Modulation

removing LED dependency on I/O pins, Dependency Injection

setting I/O pin as input, The Input in I/O

setting I/O pin as output, Set the Pin to Be an Output

pixel displays, Pixel displays, Parallel

parallel interfaces for, Parallel

Platt, Charles, Further Reading

point-to-point or point-to-multipoint communication, Serial

pointers, Sharing private globals, Creating a command, Circular buffers, Circular buffers, Circular buffers, Changing Algorithms, Run the Loader

in a circular buffer, Circular buffers, Circular buffers, Circular buffers

multiple pointers in, Circular buffers

setting, Circular buffers

function pointers, Creating a command, Run the Loader

using to change data-processing algorithms, Changing Algorithms

polling, A Simple Interface to a Button, Polling, Buttons and Key Matrices

defined, A Simple Interface to a Button

interrupts versus, Polling

matrixed buttons, Buttons and Key Matrices

polynomials, Factor Polynomials, Taylor Series, Linear interpolation, Explicit input in the table

factoring, Factor Polynomials

linear interpolation, Linear interpolation, Explicit input in the table

Taylor series for, Taylor Series

position encoding, Position encoding

POST (power-on-self) tests, Testing the Hardware (and Software)

power consumption, More Challenges, Motors, Reducing Power Consumption, Further Reading, Understanding Power Consumption, Understanding Power Consumption, Turn Off the Light When You Leave the Room, Slowing Down to Conserve Energy, Putting the Processor to Sleep, Chained Processors, Further Reading

measurement of power, Understanding Power Consumption

of motors, Motors

reducing, Turn Off the Light When You Leave the Room, Slowing Down to Conserve Energy, Putting the Processor to Sleep, Chained Processors, Further Reading

lowering clock frequency, Slowing Down to Conserve Energy

putting processor to sleep, Putting the Processor to Sleep, Chained Processors

resources for further information, Further Reading

turning off unneeded components, Turn Off the Light When You Leave the Room

understanding, Understanding Power Consumption

power down setting, Putting the Processor to Sleep

power management module, A Closer Look at the Main Loop

power off setting, Putting the Processor to Sleep

power-of-two divisors, Dividing by a Constant

powers of two, Doing the Math

#pragma, Copy Loader to RAM

precision, Math, Precision

accuracy versus, Math

of faked floating-point numbers, Precision

prescale register, Timer Pieces, Timer Pieces, Doing the Math, Doing the Math

equation for relationship with timer frequency, compare register, and clock input, Doing the Math

heuristics for finding to provide timer frequency needed, Doing the Math

presentation layer, OSI model, So Many Ways of Communicating

printed circuit boards (PCBs), Ideal Project Flow

printf function, Debugging, Typical calls needed for logging, Debugging Timing Errors, Reading a Map File (Part 1), Libraries, Identifying Fast and Slow Operations, Fake Floating-Point Numbers

debugging via, Debugging

logging API, Typical calls needed for logging

processing costs of, Identifying Fast and Slow Operations

replacing with functions that don't take variable arguments, Libraries

size of code, Reading a Map File (Part 1), Fake Floating-Point Numbers

timing errors caused by, Debugging Timing Errors

priority inversion, Priority Inversion

priority of interrupts, Interrupt priority

probes, on oscilloscopes, Setting up a scope

process value (PV), PID control

processors, Terminology, Ideal Project Flow, Your Processor Is a Language, Your Processor Is a Language, Toggling an Output, Troubleshooting, Toggling an Output, Starting with Registers, Set the Pin to Be an Output, Set the Pin to Be an Output, Turn On the LED, Turn On the LED, Blinking the LED, Troubleshooting, Board-Specific Header File, I/O-Handling Code, Interrupts, Further Reading, Pixel displays, Further Reading, Turn Off Processor Subsystems, Chained Processors

choosing, interview question on, Further Reading

development kits for, Ideal Project Flow

documentation from the vendor, Your Processor Is a Language

host computer and other processors, Pixel displays

I/O-handling code, I/O-Handling Code

learning, similarity to learning languages, Your Processor Is a Language

microcontrollers and CPUs, Terminology

pin modifications, using board-specific header file, Board-Specific Header File

programming a small processor, information on, Further Reading

reducing power consumption of, Turn Off Processor Subsystems, Chained Processors

register names in different processors, Set the Pin to Be an Output

similarity to APIs, Interrupts

toggling outputs, Toggling an Output, Troubleshooting, Toggling an Output, Starting with Registers, Set the Pin to Be an Output, Turn On the LED, Turn On the LED, Blinking the LED, Troubleshooting

blinking an LED, Blinking the LED

processors used as examples, Toggling an Output

registers, Starting with Registers

setting pin to be output, Set the Pin to Be an Output

troubleshooting the LED, Troubleshooting

turning off an LED, Turn On the LED

turning on an LED, Turn On the LED

profiling, Profiling, Sampling profiler, I/O lines and an O-scope, Timer profiler 1 (function timer), Timer profiler 2 (response timer), Sampling profiler

using I/O lines and oscilloscope, I/O lines and an O-scope

using sampling profiler, Sampling profiler

using timer profiler for functions, Timer profiler 1 (function timer)

using timer profiler for response time, Timer profiler 2 (response timer)

proportional, integral, and derivative (PID) control, PID control, Displays

protocol analyzers, Oscilloscopes and Logic Analyzers

prototyping paths with hardware, Evaluating Components Using the Datasheet

publish/subscribe pattern, A Very Small Scheduler

pull-downs, Reading a Schematic, The Input in I/O

internal, The Input in I/O

pull-ups, Reading a Schematic, The Input in I/O

internal, The Input in I/O

pulse-per-second (PPS) signal, Slowing Down to Conserve Energy

pulse-width modulation (PWM), Using Pulse-Width Modulation, Using Pulse-Width Modulation, Motors, PID control

using to control motors, Motors, PID control

using to dim LEDs, Using Pulse-Width Modulation, Using Pulse-Width Modulation

R

race conditions, Communication Between Tasks, Critical sections

radiation hardening your system, Version everything and then checksum it

radio networks, Ethernet and WiFi

RAM, More Challenges, External Memory, Brick Loader, Reset to New Code, Linker Scripts, RAM, Memory Overlays, Remove malloc, Reading a Map File (Part 2), Registers and Local Variables, Function Chains, Pros and Cons of Globals, Memory Overlays, Memory timing

code running from, Brick Loader, Reset to New Code

consumed by registers and local variables, Registers and Local Variables

copying code to, Memory timing

creating loader based in, or any code running from, Linker Scripts

function chains, Function Chains

global variables in, Pros and Cons of Globals

memory overlays, Memory Overlays

reading map file for information on, Reading a Map File (Part 2)

removing malloc, Remove malloc

rational numbers, Rational Numbers

read function, Driver Interface: Open, Close, Read, Write, IOCTL

read-modify-write cycle for modifying registers, Set the Pin to Be an Output

receive (RX) signals, RS-232 and TTL

receive buffer register (RBR), Hardware FIFOs

receiver functions, Invoking a command, Command Pattern

recursion, Reading a Map File (Part 2), Function Chains

tail recursion, Function Chains

reentrant functions, Reentrant functions

refreshes, LCD display, Pixel displays

register keyword, Registers and Local Variables

registers, Your Processor Is a Language, Starting with Registers, Set the Pin to Be an Output, Turn On the LED, Turn On the LED, Timer Pieces, Timer Pieces, An IRQ Happens, An IRQ Happens, An IRQ Happens, Hardware FIFOs, Hardware FIFOs, Registers and Local Variables

different names in different processors, Set the Pin to Be an Output

for interrupts, An IRQ Happens

local variables as, Registers and Local Variables

modifying, Starting with Registers

prescale register, Timer Pieces

reading, modifying its contents by, An IRQ Happens

receive buffer (RBR), Hardware FIFOs

for timer clock, Timer Pieces

transmit holding register (THR), Hardware FIFOs

turning off an LED, Turn On the LED

turning on LED, Turn On the LED

writing an indirect register, An IRQ Happens

relative position, Position encoding

reset vector, Looking up the ISR

resistance check mode of DMM, Digital Multimeter

resistors, Reading a Schematic, The Input in I/O, The Input in I/O, Understanding Power Consumption, Understanding Power Consumption

connected to processor and power, in schematics, Reading a Schematic

electrical circuits modeled as, Understanding Power Consumption

pull-downs on I/O pins, The Input in I/O

pull-ups on I/O pins, The Input in I/O

in series, measuring current, Understanding Power Consumption

RF network analyzers, Oscilloscopes and Logic Analyzers

RLE (run length encoding), Pixel displays

rolling average versus block average, Taking an Average

ROM (read-only memory), More Challenges, Updating Code

row-column-scan-style key matrix, Buttons and Key Matrices

RS-232 protocol, Serial, RS-232 and TTL, SPI

limitations of, SPI

and TTL, RS-232 and TTL

RS-485 protocol, Miscellaneous other protocols

RSA encryption, Authentication and encryption

rts and rti opcodes, Restore the Context

RTS/CTS handshaking, RS-232 and TTL

run length encoding (RLE), Pixel displays

RX (receive) signals, RS-232 and TTL

S

safety for boards, Keep Your Board Safe

sampling profiler, Sampling profiler

sandbox, for testing algorithms, A Sandbox to Play In

scaling input, Scaling the Input

scheduling, Scheduling and Operating System Basics, Priority Inversion, Tasks, Communication Between Tasks, Avoiding Race Conditions, Priority Inversion, A Very Small Scheduler, A Very Small Scheduler

avoiding race conditions, Avoiding Race Conditions

communication between tasks, Communication Between Tasks

priority inversion, Priority Inversion

tasks, Tasks

using timer as very small scheduler, A Very Small Scheduler, A Very Small Scheduler

publish/subscribe pattern, A Very Small Scheduler

schematic capture programs (CAD packages), Ideal Project Flow

schematics, Ideal Project Flow, Datasheet Sections You Need When Things Go Wrong, Reading a Schematic, Reading a Schematic, Reading a Schematic, Building Tests, Set the Pin to Be an Output, The Input in I/O

common components in example schematic, Reading a Schematic

creation of, Ideal Project Flow

flash memory schematic snippet, Building Tests

with LED and button, The Input in I/O

processor with LED attached, Set the Pin to Be an Output

reading, Reading a Schematic, Reading a Schematic

sample schematics section in datasheets, Datasheet Sections You Need When Things Go Wrong

SCK, CLK, or SCLK (Clock) lines, SPI

scope of variables, minimizing, Minimize scope

screen refresh, synchronizing with image update, Pixel displays

SDI (Serial Data In) lines, SPI

SDO (Serial Data Out) lines, SPI

security, Authentication and encryption, Changing Data, Further Reading, Security

adding to loaders, Security

authentication and encryption in communication, Authentication and encryption, Changing Data

resources for further reading, Further Reading

segmented displays, Segmented displays

semaphores, Avoiding Race Conditions

sensors, Sensors, Digital sensors, Analog sensors, Digital sensors, Position encoding, Pixel displays, Fake Floating-Point Numbers, Determining the Error

analog, Analog sensors

digital, Digital sensors

distributed sensor networks, Pixel displays

encoder, Position encoding

temperature compensation for, Fake Floating-Point Numbers, Determining the Error

serial communication, Serial, Miscellaneous other protocols, RS-232 and TTL, SPI, I2C, 1-wire, USB, Miscellaneous other protocols, Further Reading

1-wire, 1-wire

inter-integrated circuit, I2C

miscellaneous other protocols, Miscellaneous other protocols

RS-232 protocol and TTL, RS-232 and TTL

SPI, SPI

USB, USB

Wikipedia information on protocols, Further Reading

Serial Peripheral Interface, Serial (see SPI protocol)

serial USB-to-TTL cables, RS-232 and TTL

serial-to-Ethernet converter, hooking up to, RS-232 and TTL

session layer, OSI model, So Many Ways of Communicating

set point (SP), PID control

set register, An IRQ Happens

shadow variable, An IRQ Happens

signal processing, Analog sensors, Changing Algorithms, Further Reading

ADC to DAC data-driven system, Changing Algorithms

resources for further information, Further Reading

signal-to-noise ratio (SNR), Analog sensors

signals, Datasheet Sections You Need When Things Go Wrong, Important Text for Software Developers, Oscilloscopes and Logic Analyzers, Debouncing Switches, Further Reading, Analog sensors, Digital sensors, Digital sensors, Serial, RS-232 and TTL, RS-232 and TTL, Taking an Average, Slowing Down to Conserve Energy

analog to digital conversion, Digital sensors

analyzing with oscilloscopes and logic analyzers, Oscilloscopes and Logic Analyzers

from button, Debouncing Switches

GPS pulse-per-second, Slowing Down to Conserve Energy

hardware lines, Datasheet Sections You Need When Things Go Wrong

interview question on, Further Reading

peripheral bus and, Serial

RS-232-level serial signals, RS-232 and TTL

shielding analog signals from noise, Digital sensors

taking the average, Taking an Average

timing diagrams, Important Text for Software Developers

TTL-level serial signals, RS-232 and TTL

signed numbers, binary representation, Circular buffers, Circular buffers

using two's complement, Circular buffers

sine function (example), Taylor Series, Explicit input in the table

singleton pattern, Pattern: Singleton

Slave Select (SS) lines, SPI

sleep, Understanding Power Consumption, Putting the Processor to Sleep, Chained Processors, Putting the Processor to Sleep, Interrupt-Based Code Flow Model

defined, Putting the Processor to Sleep

measuring power consumption in, Understanding Power Consumption

putting processor to sleep, Putting the Processor to Sleep, Chained Processors

sleep register, Interrupt-Based Code Flow Model

slow down setting, Putting the Processor to Sleep

smart cards, Miscellaneous other protocols, Further Reading

implementing contact interface to, Miscellaneous other protocols

security and, Further Reading

smart commands, Invoking a command

smart sensors, Digital sensors

SNR (signal-to-noise ratio), Analog sensors

software, Introduction, The Block Diagram, Testing the Hardware (and Software), Command Pattern, Facade Pattern, Changing Data

block diagrams, The Block Diagram

constraints in embedded systems, Introduction

design patterns, Facade Pattern (see design patterns)

testing, Testing the Hardware (and Software), Command Pattern

UML diagrams of, Changing Data

solar-powered calculators, Segmented displays

solenoids, Actuators

SP (set point), PID control

speed, Speed, Coding in assembly language, Profiling, Sampling profiler, Optimizing, Coding in assembly language, Memory timing, Variable size, One last look at function chains, Consider the instructions, Reduce math in loops, Loop unrolling, Lookup tables, Coding in assembly language, Slowing Down to Conserve Energy

optimizing, Optimizing, Coding in assembly language, Memory timing, Variable size, One last look at function chains, Consider the instructions, Reduce math in loops, Loop unrolling, Lookup tables, Coding in assembly language

assembly language code, Coding in assembly language

considering instructions, Consider the instructions

function chains, One last look at function chains

lookup tables, Lookup tables

loop unrolling, Loop unrolling

memory timing, Memory timing

reducing math in loops, Reduce math in loops

variable size, Variable size

profiling your application, Profiling, Sampling profiler

slowing down to conserve energy, Slowing Down to Conserve Energy

SPI (Serial Peripheral Interface) protocol, The Block Diagram, Building Tests, Set the Pin to Be an Output, Serial, SPI, SPI, Comparison of buffering schemes

flash memory attached via SPI bus, The Block Diagram

flash memory device communicating via, Building Tests

implementing, buffering schemes and, Comparison of buffering schemes

pin shared between SPI port and I/O subsystem, Set the Pin to Be an Output

wires connecting processor to SPI peripheral, SPI

spreadsheet tables, state machines as, Table-Driven State Machine

square root, in standard deviation calculations, Use an Existing Algorithm

SS (Slave Select) lines, SPI

ST Microelectronics AN1447 Application Note, Segmented displays

stacks, Nonmaskable interrupts, Copy Loader to RAM, Reading a Map File (Part 2), Reading a Map File (Part 2), Function parameters, Function Chains

determining size of, Reading a Map File (Part 2)

function chains in, Function Chains

function input parameters in registers instead of, Function parameters

runtime program using RAM for, Copy Loader to RAM

stack overflow bugs, Reading a Map File (Part 2)

standard deviation, calculating, Use an Existing Algorithm, Use an Existing Algorithm

start and stop bits, number of, RS-232 and TTL

state machines, State Machines, Choosing a State Machine Implementation, State Machine Example: Stoplight Controller, State-Centric State Machine, State-Centric State Machine with Hidden Transitions, Event-Centric State Machine, Table-Driven State Machine, Table-Driven State Machine, Choosing a State Machine Implementation

choosing an implementation, Choosing a State Machine Implementation

event-centric, Event-Centric State Machine

state-centric, State-Centric State Machine, State-Centric State Machine with Hidden Transitions

with hidden transitions, State-Centric State Machine with Hidden Transitions

stoplight controller (example), State Machine Example: Stoplight Controller

table-driven, Table-Driven State Machine, Table-Driven State Machine

state pattern, State Machines, State Pattern

state transitions, State Machine Example: Stoplight Controller, State-Centric State Machine with Hidden Transitions

hidden, in state-centric state machine, State-Centric State Machine with Hidden Transitions

state variables for logging, State of logging

static keyword, State of logging, Reading a Map File (Part 1)

static variables, Nonmaskable interrupts, Reentrant functions

nonreentrant functions using, Reentrant functions

status registers, An IRQ Happens

steady state errors, PID control

stepper motors, Motors, Position encoding

position information, Position encoding

stoplight controller (example), State Machines, Choosing a State Machine Implementation, Further Reading

interview question on, Further Reading

strategy pattern, Changing Algorithms

strcpy function, replacing, Libraries

strings, Reading a Map File (Part 1), Libraries, Constants and Strings

constant strings in code space, Reading a Map File (Part 1)

debug strings in code space, Constants and Strings

replacing strcpy in code space, Libraries

structures, State of logging, Circular buffers, Linker Scripts

circular buffer, Circular buffers

creating ad-hoc struct, sDataProcessing, Linker Scripts

malloc'd during initialization, State of logging

subtraction, Identifying Fast and Slow Operations, Addition (and Subtraction)

of fake floating-point values, Addition (and Subtraction)

switch statements, Creating a command, State-Centric State Machine, Event-Centric State Machine, Lookup tables

lookup tables for statements for, Lookup tables

state machines as, State-Centric State Machine, Event-Centric State Machine

synchronous communication pathways, Serial, SPI

SPI protocol, SPI

system architecture, Creating a System Architecture, Further Reading, Creating System Diagrams, The Block Diagram, Hierarchy of Control, Layered View, From Diagram to Architecture, Delegation of Tasks, Driver Interface: Open, Close, Read, Write, IOCTL, Adapter Pattern, Example: A Logging Interface, A Sandbox to Play In, A Sandbox to Play In, Further Reading

adapter pattern, Adapter Pattern

block diagram, The Block Diagram

delegation of tasks, Delegation of Tasks

driver interface, Driver Interface: Open, Close, Read, Write, IOCTL

encapsulation of modules, From Diagram to Architecture

hierarchy of control diagram, Hierarchy of Control

interview question, describing the architecture of an object, Further Reading

layered software diagram, Layered View

logging interface (example), Example: A Logging Interface

proceeding in view of resource and time constraints, A Sandbox to Play In

segregating the algorithm, A Sandbox to Play In

types of diagrams, Creating System Diagrams

system clock, Timer Pieces

system failure, monitoring with watchdog, Watchdog

system latency, Avoiding Race Conditions, Calculating system latency

interrupts and functions calls during, Calculating system latency

system statistics, Timer Pieces

system tick, System Tick, Time-Based Events, System Tick, System Tick, System Tick

fenceposts and jitter, System Tick

measuring time with TimePassed function, System Tick

TimeNow function to return tick counter, System Tick

T

table-driven state machines, Table-Driven State Machine, Table-Driven State Machine, Table-Driven State Machine, Table-Driven State Machine

creating tables, Table-Driven State Machine

event handlers, Table-Driven State Machine

tail recursion, Function Chains

Tannenbaum, Andrew, Further Reading

tasks, Tasks, Communication Between Tasks, An IRQ Happens, A Very Small Scheduler, A Very Small Scheduler

attaching to scheduler, A Very Small Scheduler

communication between, Communication Between Tasks

task-like interrupts, An IRQ Happens

very small scheduler for, A Very Small Scheduler

Taylor series, Taylor Series, Scaling the Input, Lookup Tables

errors in scaling sine function input with, Scaling the Input

TCP, Ethernet and WiFi

tearing, Pixel displays

temperature compensation (example), Fake Floating-Point Numbers, Determining the Error

template pattern, Changing Algorithms

ternary conditional operator (?:), Functions and Macros

testing, Delegation of Tasks, A Sandbox to Play In, Board Bring-Up, Testing the Hardware (and Software), Command Pattern, Testing the Hardware (and Software), Testing the Hardware (and Software), Building Tests, Flash Test Example, Test wrap-up, Test wrap-up, Command and Response, Command and Response, Command Pattern, Further Reading, Digital sensors, Changing Algorithms

building tests, Building Tests

command pattern and, Command and Response

creating test functions for each piece of hardware, Board Bring-Up, Command and Response

EMC testing, Digital sensors

flash memory text (example), Flash Test Example, Test wrap-up

modularity and, Delegation of Tasks

resources for further information, Further Reading

template pattern, Changing Algorithms

tests created during bring-up, Testing the Hardware (and Software)

tests for various forms of memory, Test wrap-up

types of tests used with embedded systems, Testing the Hardware (and Software)

using command pattern, Command Pattern

using sandbox to test algorithms, A Sandbox to Play In

text segment, executable code, Linker Scripts

THR (transmit holding register), Hardware FIFOs

throughput, Analog sensors, Serial, Parallel

parallel interfaces for high data throughput, Parallel

peripheral bus and, Serial

TI MSP430 line of processors, Toggling an Output, Interrupt-Based Code Flow Model, Further Reading

Launchpad development kit, Interrupt-Based Code Flow Model

User’s Guide, 16-bit RISC processor, Toggling an Output

ticks, System Tick (see system tick)

time-based events, avoiding use of interrupts for, Time-Based Events

timer counter register, Timer Pieces

timers, Using a Timer, Using Pulse-Width Modulation, Timer Pieces, Timer Pieces, Doing the Math, A Long Wait Between Timer Ticks, Using the Timer, Using Pulse-Width Modulation, Further Reading, An IRQ Happens, System Tick, A Very Small Scheduler, Watchdog, SPI, Timer profiler 1 (function timer), Timer profiler 2 (response timer), Sampling profiler

getting ready to use, Using the Timer

interrupt implementing system tick, System Tick

interrupts, An IRQ Happens

long wait between timer ticks, A Long Wait Between Timer Ticks

prescaling and matching, Timer Pieces

relating from series of registers to actual time, Doing the Math

resources for further information, Further Reading

sampling profiler with timer-based interrupt, Sampling profiler

setting frequency, Timer Pieces

SPI interface, SPI

timer profiler, Timer profiler 1 (function timer)

timer profiler for response times, Timer profiler 2 (response timer)

using as mini-scheduler, A Very Small Scheduler

using pulse-width modulation (PWM), Using Pulse-Width Modulation

watchdog, Watchdog

timescale for oscilloscope, Setting up a scope

timing diagrams, Important Text for Software Developers

timing errors, debugging, Debugging Timing Errors

toolbox, Toolbox

torque, of motors, Motors

transitions, State-Centric State Machine with Hidden Transitions (see state transitions)

transmit (TX) signals, RS-232 and TTL

transmit holding register (THR), Hardware FIFOs

transport layer, OSI model, So Many Ways of Communicating

TTL, RS-232 protocol and, RS-232 and TTL

tuning (PID terms), PID control

two's complement, Circular buffers

U

UART, Serial, RS-232 and TTL

UDP, Ethernet and WiFi

UML diagrams, Changing Data

Unicode, Miscellaneous other protocols

unit tests, Testing the Hardware (and Software)

universal synchronous/asynchronous receiver/transmitter (USART), Serial

Unix systems, driver interface, Driver Interface: Open, Close, Read, Write, IOCTL

updating code, Updating Code, Summary, Updating Code, Onboard Bootloader, Build Your Own Updater, Modifying the Resident Updater, Modifying the Resident Updater, Brick Loader, Copy Loader to RAM, Run the Loader, Run the Loader, Copy New Code to Scratch, Dangerous Time: Erase and Program, Reset to New Code, Linker Scripts, Summary, Summary, Summary

building your own updater, Build Your Own Updater, Modifying the Resident Updater, Modifying the Resident Updater

modifying resident updater, Modifying the Resident Updater

interview question on getting goat safely across river, Summary

loaders, types of, Updating Code

questions about loader design, Summary

using brick loader, Brick Loader, Copy Loader to RAM, Run the Loader, Run the Loader, Copy New Code to Scratch, Dangerous Time: Erase and Program, Reset to New Code

copying loader to RAM, Copy Loader to RAM

copying new code to scratch, Copy New Code to Scratch

erase and program, Dangerous Time: Erase and Program

reset to new code, Reset to New Code

running the loader, Run the Loader, Run the Loader

using linker scripts, Linker Scripts, Summary

using on-board bootloader, Onboard Bootloader

USART (universal synchronous/asynchronous receiver/transmitter), Serial

USB, USB

USB-to-serial cables, RS-232 and TTL

user manuals (or user guides), Your Processor Is a Language, Toggling an Output

V

variables, State of logging, State of logging, Sharing private globals, Interrupt on a Button Press, Debouncing Switches, Doing the Math, Nonmaskable interrupts, Reentrant functions, Reading a Map File (Part 2), Minimize scope, Variable size, Identifying Fast and Slow Operations, Use an Existing Algorithm

avoiding use of large intermediate variables, Use an Existing Algorithm

constants versus, Identifying Fast and Slow Operations

for debouncing switches, Debouncing Switches

limiting scope of, Minimize scope

number of different values possible, Doing the Math

reducing RAM consumed by, Reading a Map File (Part 2)

reducing size of, Variable size

scoping in C to mimic private variables, State of logging

sharing private global variables, Sharing private globals

and the stack, Nonmaskable interrupts

static, State of logging

used by nonreentrant functions, Reentrant functions

volatile, Interrupt on a Button Press

variance, Use an Existing Algorithm, Use an Existing Algorithm

calculation without large intermediate variables and in single pass, Use an Existing Algorithm

vector segment, executable code, Linker Scripts

vector table, Get the ISR from the Vector Table (see interrupt vector table)

vendors, Evaluating Components Using the Datasheet, Your Processor Is a Language

reputation of, Evaluating Components Using the Datasheet

visits to processor vendors, Your Processor Is a Language

Venn diagram, representing XOR, I/O-Handling Code

versions, Typical calls needed for logging, Version everything and then checksum it

adding to pieces of system that can be attached or detached, Version everything and then checksum it

of code, Typical calls needed for logging

view, in MVC pattern, A Sandbox to Play In, A Sandbox to Play In

sandbox environment in, A Sandbox to Play In

virtual box, A Sandbox to Play In, A Sandbox to Play In

(see also sandbox)

volatile keyword, Further Reading

volatile memory, External Memory

voltage, Understanding Power Consumption, Understanding Power Consumption

power and, Understanding Power Consumption

across a resistor, measure of static consumption, Understanding Power Consumption

voltage mode of DMM (digital multimeter), Digital Multimeter

W

wait states for memory, Memory timing

wake-ups, Putting the Processor to Sleep, Interrupt-Based Code Flow Model, Avoid Frequent Wake-Ups

avoiding frequent wake-ups, Avoid Frequent Wake-Ups

interrupts for, Putting the Processor to Sleep

Warren, Henry, Further Reading, Further Reading

watchdog, Watchdog, Watchdog, Processor Watchdog

interrupt-based system and, Processor Watchdog

terminology, Watchdog

watts, Understanding Power Consumption

web page for this book, How to Contact Us

while statements, Reduce math in loops

WiFi, Ethernet and WiFi

Wikipedia, SPI, Further Reading, Use an Existing Algorithm, Use an Existing Algorithm

methods of computing square roots, Use an Existing Algorithm

serial communication protocols, Further Reading

SPI page, SPI

variance, Use an Existing Algorithm

wikis, for processors, Your Processor Is a Language

write function, Driver Interface: Open, Close, Read, Write, IOCTL

X

XOR (^) bitwise operator, I/O-Handling Code

Z

Ziegler-Nichols method, tool to help with PID tuning, PID control

About the Author

Elecia White has worked on DNA scanners, inertial measurement unitsfor airplanes and race cars, toys for preschoolers, a gunshot locationsystem for catching criminals, and assorted other medical and consumerdevices. She is the founder of Logical Elegance, an embedded systemsconsulting company based in San Jose. Elecia has developed strongskills in signal processing, hardware integration, complex systemdesign, and performance. Having been through FAA and FDA certificationprocesses, she understands the importance of producing quality designsand how they lead to quality implementations.

Elecia has spent several years in management roles but enjoys hands-onengineering and the thrill of delivering excellent products. Whilecontinuing to provide leadership and mentoring, she prefers to focuson the technical aspects of a project. A graduate of Harvey MuddCollege in Claremont, CA, Elecia enjoys sharing her passion forscience, engineering and interesting gizmos, particularly how thesethings can make the world a better place.

Colophon

The animal on the cover of Making Embedded Systems is a great-eared goatsucker.

Great-eared goatsuckers are members of the family Caprimulgidae, informally referred to as “nightjars.” The term “goatsucker” stems from an inaccurate belief that the birds drink milk from goats; it is not to be confused with “chupacabra,” the fabled cryptid believed by some to inhabit the Americas and drink the blood of goats. The name “great-eared nightjar” is more commonly used today.

Great-eared nightjars inhabit subtropical or tropical moist lowland forests in areas of Southeast Asia, including India, Bangladesh, Myanmar, Thailand, Malaysia, Indonesia, Laos, Cambodia, Vietnam, the Philippines, and China. They are crepuscular, meaning mostly active at dusk and at night, during which time they satisfy their diets of flying insects and moths and sound their distinctive calls: a sharp “tissk,” followed by a two-syllable “ba-haw.”

Great-eared nightjars are characterized by pronounced ear tufts, which, in addition to their average length of 16 inches, make them more conspicuous among nightjars. Their soft gray and brown plumage resembles their preferred habitat, leaf litter and bracken. They lay their eggs either directly on bare ground or in leaf-litter nests on the ground. Nightjar nestlings have been observed to be completely silent and motionless, which, in addition to their leaf-colored plumage, may help protect them from danger while nesting.

The cover image is from Wood’s Animate Creation. The cover font is Adobe ITC Garamond. The text font is Linotype Birka; the heading font is Adobe Myriad Condensed; and the code font is LucasFont’s TheSansMonoCondensed.