Reusable Firmware Development
Reusable Firmware Development
Firmware
Development
A Practical Approach to APIs, HALs
and Drivers
—
Jacob Beningo
Reusable Firmware
Development
A Practical Approach to APIs,
HALs and Drivers
Jacob Beningo
Reusable Firmware Development: A Practical Approach to APIs, HALs and Drivers
Jacob Beningo
Linden, Michigan, USA
Preface�������������������������������������������������������������������������������������������������������������������xix
Introduction������������������������������������������������������������������������������������������������������������xxi
v
Table of Contents
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
x
Table of Contents
xi
Table of Contents
Index��������������������������������������������������������������������������������������������������������������������� 301
xii
About the Author
Jacob Beningo is an embedded software consultant with
over 15 years of experience in microcontroller-based real-
time embedded systems. After spending over ten years
designing embedded systems for the automotive, defense,
and space industries, Jacob founded Beningo Embedded
Group in 2009. Jacob has worked with clients in more than a
dozen countries to dramatically transform their businesses
by improving product quality, cost, and time to market. He
has published more than 200 articles on embedded software
development techniques and is a sought-after speaker and
technical advisor. Jacob is an avid writer, trainer, consultant,
and entrepreneur who transforms the complex into simple and understandable
concepts that accelerate technological innovation.
Jacob has demonstrated his leadership in the embedded-systems industry by
consulting and working as a trusted advisor at companies such as General Motors, Intel,
Infineon, and Renesas. Jacob also speaks at and is involved in the embedded track-
selection committees at ARM Techcon, Embedded System Conferences, and Sensor
Expo. Jacob holds bachelor’s degrees in electrical engineering, physics, and mathematics
from Central Michigan University and a master’s degree in space systems engineering
from the University of Michigan.
In his spare time, Jacob enjoys spending time with his family, reading, writing, and
playing hockey and golf. When there are clear skies, he can often be found outside with
his telescope, sipping a fine scotch while imaging the sky.
xiii
About the Technical Reviewers
Ahmed S. Hag-ElSafi (Khartoum, 1978) holds Bachelor
of Science and Master of Science degrees in electronics
and communications engineering from the Arab Academy
for Science and Technology, earned in 2002 and 2004
respectively.
He has 15 years of experience of research and industrial
development in the areas of embedded systems and
machine learning. He has published more than fifteen
papers in the areas of IOT security, biometrics, machine
learning, and medical image processing. He is currently the
co-founder and principal researcher at Smart Empower Innovation Labs Inc. in Alberta,
Canada.
Mr. Hag-ElSafi is a member the Smart City Alliance in Alberta, Canada, and the
Association of Professional Engineers and Geoscientists of Alberta (APEGA).
xv
About the Technical Reviewers
Dr. Zewail is a member of the Institute of Electrical and Electronics Engineers (IEEE),
the Association of Professional Engineers & Geoscientists (APEGA), and the Canadian
Association for Artificial Intelligence. He also served as a reviewer for the Journal of
Electronics Imaging and the Journal of Optical Engineering for the SPIE society in the
United States.
xvi
Acknowledgments
I would like to thank my parents, teachers, and family for inspiring me and encouraging
me to pursue my passions. Without their help, this book and the very direction my career
has taken would never have happened.
I would also like to thank the countless and often nameless software engineers
who came before us and laid the foundation upon which this book sits. Without their
contributions to this industry and their inspiration, I would never have embarked on
such an undertaking.
I would also like to thank Salvador Almanza and Benjamin Sweet for acting as
sounding boards and reviewing portions of the manuscript.
Finally, I would like to thank Max “The Magnificient” Maxfield for encouraging me to
write this book and sharing his publishing experiences with me.
xvii
Preface
In 2001, when I was a bright-eyed college sophomore, I would spend my evenings doing
something a bit unusual—writing embedded software. Writing embedded software
is not necessarily unusual, except that any observer would think that I wasn’t writing
the software for any particular purpose. I was not designing any specific product or
experimenting to understand how things work. Instead, I was focused on understanding
how to write portable and reusable software for microcontroller-based systems.
My idea and hope was that I could develop libraries and code modules that would
allow me to quickly meet any project requirements that might be thrown my way. In
theory, these libraries would allow me to get a microcontroller up and running and
interface with external communication devices at a fraction of the time and cost that it
would take if I started from scratch every time.
Looking back on this endeavor, I realize that this was a pivotal period that would
permeate my professional career, even now. Unfortunately, as a college student in 2001,
the libraries and components that I created were written in assembly and closely tied to
a single target device. Assembly language compilers were freely offered in those days,
and the preferred C compilers cost several thousand dollars, with no code-size limitation
trials. (The microcontrollers I was using did not have a GCC variant available at that
time).
The fortunes of time have thankfully made C compilers more readily available, and
assembly language code has gone nearly the way of the dinosaurs. What is perhaps far
more interesting about this tale is that this early interest in developing modular and
reusable components in assembly language found its way into my professional career
developing embedded software in C/C++. The result has been a steadily improving
set of techniques, APIs, HALs, components, and design patterns that can be applied to
resource-constrained embedded systems.
As a consultant and technical educator, each year I work with companies by the
dozens and engineers by the thousands who struggle to develop portable and reusable
embedded software. Many efforts are repeated from one project to the next, resulting in
wasted time, effort, money, and potential to innovate.
xix
Preface
One of my hopes with this book and the associated API and HAL Standard is to
share my experiences and provide a framework that other developers may leverage and
use in their own development efforts. My goal is that readers won’t just become better
developers but will also be able to keep pace with the demanding modern development
cycle and still have time to innovate and push the envelope.
Implementing the processes and techniques contained in this book should help any
developer decrease their development costs and time to market while improving the
portability and reliability of their software. At a minimum, developers will find that they
no longer need to keep reinventing the wheel every time a new project starts.
Happy coding,
Jacob Beningo
September 2017
xx
Introduction
Since the turn of the twenty-first century, microcontroller-based systems have become
extremely complex. Microcontrollers started out as simple 8-bit devices running at
bus speeds in the 8 MHz to 48 MHz range. Since then, microcontrollers have become
complex and powerful 32-bit devices running at clock speeds faster than 200 MHz
with every imaginable peripheral, including USB, TCP/IP, and Wi-Fi, and some
microcontrollers now even have an internal cache. This dramatic explosion of capability
and complexity has left the embedded software developer scrambling to understand
how to do the following:
Traditionally, many embedded systems were written in such a way that the code was
used once, on a single platform, and then tossed out. Software, for the most part, could
be referred to as spaghetti code and did not follow any object-oriented or software-reuse
model. In today’s development environment, developers need to write their software
with reusability and portability in mind. The teams that are the most successful can
leverage existing intellectual property and quickly innovate on it.
The purpose of this book is to help the embedded software engineer learn and
understand how they can develop reusable firmware that can be used across multiple
microcontroller platforms and software products. The fundamental pieces to firmware
reuse that we will be focusing on are HALs, APIs, and drivers. These are the core pieces
that will allow us to develop a layered software architecture and define how those
different layers interact with each other.
Chapters 1 through 5 lay the foundation on which a developer can start writing
reusable firmware. In these chapters, we examine the C constructs that best lend
themselves to portability and define what a hardware abstraction layer (HAL) is and
xxi
Introduction
how it differs from application programming interfaces (APIs). We will discuss different
design methodologies developers can use to write low-level drivers and examine
the design patterns, along with their pros and cons. Along the way, we’ll look at real-
world examples and even take a chapter to discuss how reusable firmware should be
documented.
With the foundation laid, Chapters 6 through 10 examine the processes that can be
followed to create HALs and APIs. We examine common elements, such as GPIO, SPI,
and external memory devices, before moving on to looking at high-level application
frameworks that can aid reuse and accelerate software design.
Chapter 11 discusses how developers should develop tests to ensure that their
reusable software remains usable with a minimal bug count. Finally, Chapter 12 walks
developers through how they can start developing reusable software no matter the
environment or challenges that they may be facing and how they can succeed in those
environments.
The chapters don’t necessarily need to be read in order, but they are put together in
an order that builds upon what came before. A developer with reasonable experience
developing reusable software could easily skip around whereas developers new to
writing reusable software should read the chapters in order.
xxii
CHAPTER 1
Concepts for Developing
Portable Firmware
“A good scientist is a person with original ideas. A good engineer is a person
who makes a design that works with as few original ideas as possible.”
—Freeman Dyson
1
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_1
Chapter 1 Concepts for Developing Portable Firmware
1
Embedded Marketing Study, 2009 – 2015, UBM
2
Chapter 1 Concepts for Developing Portable Firmware
can now start utilizing design methods that decouple the application code from the
hardware and allow a radical increase in code reuse.
Portable Firmware
Firmware developed today is written in a rather archaic manner. Each product-
development cycle results in limited to no code reuse, with reinvention being a major
theme among development teams. A simple example is when development teams refuse
to use an available real-time operating system (RTOS) and instead develop their own
in-house scheduler. Beyond wanting to build their own custom scheduler, there are two
primary examples that demonstrate the issue with reinvention.
SOFTWARE TERMINOLOGY
Portable firmware is embedded software that is designed to run on more than one
microcontroller or processor architecture with little or no modification.
First, nearly every development team writes their own drivers because
microcontroller vendors provide only example code and not production-ready drivers.
Examples provide a great jump-start to understanding the microcontroller peripherals,
but it still requires a significant time investment to get a production-intent system.
There could be a hundred companies using the exact same microcontroller, and each
and every one will waste as much as 30 percent or more of their total development time
getting their microcontroller drivers written and integrated with their middleware! I
have seen this happen repeatedly among my client base and have heard numerous
corroborating stories from the hundreds of engineers I interact with on a yearly basis.
Second, there are so many features that need to be packed into a product, and with
a typical design cycle being twelve months,1 developers don’t take the time to properly
architect their systems for reuse. High-level application code becomes tightly coupled
to low-level microcontroller code, which makes separating, reusing, or porting the
application code costly, time consuming, and buggy. The end result—developers just
start from scratch every time.
3
Chapter 1 Concepts for Developing Portable Firmware
In order to keep up with the rapid development pace in today’s design cycles,
developers need to be highly skilled in developing portable firmware. Portable firmware
is embedded software that is designed to run on more than one microcontroller or
processor architecture with little to no modification. Writing firmware that can be ported
from one microcontroller architecture to the next has many direct advantages, such as:
Portable firmware also has several indirect advantages that many teams overlook but
that can far outweigh the direct benefits, such as:
• Decreased team stress levels due to limiting how much total code
needs to be developed (happy, relaxed engineers are more innovative
and efficient)
Using portable and reusable code can result in some very fast and amazing results, as
seen in the case study “Firmware Development for a Smart Solar Panel,” but there are also
a few disadvantages. The disadvantages are related to upfront time and effort, such as:
4
Chapter 1 Concepts for Developing Portable Firmware
For development teams to successfully enjoy the benefits of portable code use,
extra time and money needs to be spent up-front. However, after the initial investment,
development cycles have a jump-start to potentially decrease development time by
months versus the traditional embedded-software design cycle. The long-term benefits
and cost savings usually overshadow the up-front design costs, along with the potential
to speed up the development schedule.
Developing firmware with the intent to reuse also means that developers may
be stuck with a single programming language. How does one choose a language for
software that may stick around for a decade or longer? Using a single programming
language is not a major concern in embedded-software development, despite what one
might initially think. The most popular embedded language, ANSI-C, was developed in
1972 and has proven to be nearly impossible to usurp. Figure 1-2 shows the popularity
of programming languages used in embedded systems. Despite advances in computer
science and the development of object-oriented programming languages, C has
remained very popular as a general language and is heavily entrenched in embedded
software.
5
Chapter 1 Concepts for Developing Portable Firmware
2
Aspencore Embedded Systems Survey, 2017, www.embedded.com
6
Chapter 1 Concepts for Developing Portable Firmware
When it comes to product development, the single constant in the universe is that the
development either needs to be done yesterday or by some not-so-distant future date.
A few years ago, on December 1, I received a call from a prospective client I had been talking
with for the better part of the year. The client, a start-up in the small satellite industry, had
just received news that they had an opportunity to fly their new flagship spacecraft on an
upcoming launch. The problem was that they had just six weeks to finish building, testing, and
delivering their satellite!
One of the many hurdles they faced was that their smart solar panels (smart because they
contained a plethora of sensors critical to stabilizing the spacecraft) didn't have a single line
of firmware written. The solar panels’ firmware had to be completed by January 1, leaving just
four weeks over a holiday month to design, implement, test, and deploy the firmware.
To give some quantification to the project scope, the following are some of the software
components that needed to be included:
• H-bridge control
• Task scheduler
• Accelerometer
• Magnetometer
• Calibration algorithms
• Fault recovery
7
Chapter 1 Concepts for Developing Portable Firmware
I accepted the project and leveraged the very same HAL and driver techniques presented in
this book to complete the project. A day was spent pulling in existing drivers and making minor
modifications for the microcontroller derivative. The second week was spent pulling together
the application code and remaining drivers. Finally, week three was test, debug, and deliver—
just in time for Christmas and to the client’s delight.
The decision to develop portable firmware should not be taken lightly. In order
to develop truly portable and reusable firmware, there are a few characteristics that a
developer should review and make sure that the firmware will exhibit. First, the software
needs to be modular. Writing an application that exists in a single source file is not an
option (yes, I still see this done even in 2016). The software needs to be broken up into
manageable pieces with minimal dependencies between modules and similar functions
being grouped together.
Portable Firmware …
1. is modular
2. is loosely coupled
4. is ANSI-C compliant
8. is simple
8
Chapter 1 Concepts for Developing Portable Firmware
these add-ons, developers should select a safe and fully specified subset for the C
programming language. Industry-accepted standards such as MISRA-C or Secure C
might be good options to help ensure that the firmware will use safe constructs.
Developers will want to make sure that the reusable code is also well documented
and contains detailed examples. The firmware needs to have a clean interface that is
simple and easy to understand. Most important, developers will want to make sure that
a simple, scalable hardware-abstraction layer is included in the software architecture.
The hardware-abstraction layer will define how application code interacts with the
lower underlying hardware. Let’s examine in greater detail a few key characteristics that
portable firmware should exhibit before diving into hardware-abstraction layers.
Modularity
On more than one occasion over the last several years, I have worked with a client whose
entire application, 50,000-plus lines of code, was contained within a single main.c
module. Attempts to maintain the software or reuse pieces of code quickly turned into
a nightmare. These applications were still using software techniques from back in the
1970s and 1980s, which was not working out so well for my client.
Modularity emphasizes that a program’s functionality should be separated into
independent modules that may be interchangeable. Each module contains a header
and source file with the ability to execute specialized system functions that are exposed
through the module’s interface. The primary benefit of employing modularity in an
embedded system is that the program is broken up into smaller pieces that are organized
based on purpose and function.
Ignoring the preceding facts and lumping large amounts of code into a single
module, even if it is well organized or makes sense in the beginning, usually results in
a decay into a chaos and a software architecture that resembles spaghetti. Breaking
a program up into separate modules is so important when developing portable and
reusable firmware because the independence each module exhibits allows it to be easily
moved from one application to the next, or in some cases even from one platform to the
next. There are a few advantages associated with breaking a program up into modular
pieces, such as:
• Being able to find functions or code of interest very quickly and easily
9
Chapter 1 Concepts for Developing Portable Firmware
• The ability to remove modules from a program and replace them with
new functionality
Each module added to a program does come with the disadvantage that the
compiler will need to open, process, compile, and close the module. The result in the
“old days” would have been slower compilation times. Development machines today
are so fast and efficient that increased compile time is no longer an excuse for writing
bulking, clunky code.
10
Chapter 1 Concepts for Developing Portable Firmware
The software base in Figure 1-3a shows a completely different story. The modules in
Figure 1-3b are loosely coupled. A developer attempting to bring in a top-level module won’t
be fraught with continuous compiler errors of missing files or spend hours on end trying to
track down all the dependencies. Instead, the developer quickly moves the loosely coupled
module into the new code base and is on to the next task with little to no frustration. Low
coupling is the result of a well-thought-out and well-structured software design.
SOFTWARE TERMINOLOGY
Coupling refers to how closely related different modules or classes are to each other and the
degree to which they are interdependent.
Module coupling is only the story’s first part. Having low module coupling doesn’t
guarantee that the software will exhibit easily portable traits. The goal is to have a module
that has low coupling and high cohesion. Cohesion refers to the degree to which the
module elements belong together. In a microcontroller environment, a low-cohesion
example would be lumping every microcontroller peripheral function into a single module.
The module would be large and unwieldy. The microcontroller peripheral functions
could instead be broken up into separate modules, each with functions specific to one
peripheral. The results would be the benefits listed in the previous section on modularity.
11
Chapter 1 Concepts for Developing Portable Firmware
Portable and reusable software attempts to create modules that are loosely coupled
and have high cohesion. Modules with these characteristics are usually easy to reuse and
maintain. Consider what would happen in a tightly coupled system if a single module
were changed. A single change would result in changes being forced in at least one other
module, if not more, and it could be time consuming to hunt down all the necessary
changes. Failure to make the change or a simple oversight could result in a bug, which in
the worst case could cause project delays and increased costs.
Following a Standard
Creating firmware that is portable and reusable can be challenging. For example, the C
language has gone through several different standard revisions: C90, C99, and C11. In
addition to the different C versions, there also exist non-standard language extensions,
compiler additions, and even language offshoots. To develop firmware that is reusable
to the greatest extent possible, a development team needs to select a widely accepted
standard version, such as C90 or C99. The C99 version has some great additions that
make it a good choice for developers. At the time of this writing, there is limited support
for C11 in firmware development, and C11 is five years old! Adopting C99 is the best bet
for following a standard.
The long-term support for C and its general-purpose use has resulted in language
extensions and non-standard versions that need to be avoided. Using any construct
that is not in the standard will result in specialized modifications to the code base that
can obfuscate the code. Sometimes using extensions or an intrinsic is unavoidable due
to optimization needs, but we will discuss later how we can still write portable code in
these circumstances.
In addition to using the C standard, developers should also restrict their use to well-
defined constructs that are easy to understand and maintain and are fully specified. For
example, standards such as MISRA-C and Secure-C exist to provide recommendations
on a C subset and they should be used to develop firmware. MISRA-C was developed for
the automotive industry, but the recommendations have proven to be so successful at
producing quality software that other industries are adopting the recommendations.
Developers should not view a standard as a restriction but instead as a method for
improving the quality and portability of the firmware that they develop. Identifying and
following standard C dialects will take developers a long way in developing reusable
12
Chapter 1 Concepts for Developing Portable Firmware
firmware. Recognizing the need to follow the ANSI-C standard and having the discipline
to follow it will guide a development team toward creating embedded software that can
be reused for years to come.
The answer could be 65,535 or 4,294,967,295. Both answers could be correct. The
reason is that the storage size for an integer is not defined within the ANSI-C standard.
The compiler vendors have the choice to define the storage size for the variable based on
what they deem will be the most efficient and/or appropriate.
The storage size for an integer normally wouldn’t seem like a big deal. For a code
base an int will be an int, so who cares? The problem surfaces when that same code
is compiled using a different compiler. Will the other compiler store the variable as the
same size or different? What happens if it was stored as four bytes and now is only two?
Perfectly working software is now buggy!
The portability issues arising from integers, the most commonly used data type, are
solved in a relatively simplistic way. The library header file stdint.h defines fixed-width
integers. A fixed-width integer is a data type that is based on the number of bits required
to store the data. For example, a variable that needs to store unsigned data that is 32 bits
13
Chapter 1 Concepts for Developing Portable Firmware
wide doesn’t need to gamble on int being 32 bits, but instead a developer can simply
use the data type uint32_t. Fixed-width integers exist for 8, 16, 32, and in some cases
even 64 bits. Table 1-1 shows a list of the different fixed-width integer definitions that can
be found in stdint.h.
The library file stdint.h doesn’t contain just the data types found in Table 1-1 but
also a few interesting and less-known gems. Take, for example, uint_fastN_t, which
defines a variable that is the fastest to process at least N bits wide. A developer can
tell the compiler that the data must be at least 16 bits but could be 32 bits if it can be
processed faster using a larger data type. Another great example is uintmax_t, which
defines the largest fixed-width integer possible on the system. A personal favorite is
uintptr_t, which defines a type that is wide enough to store the value of a pointer.
Using stdint.h is an easy way to help ensure that embedded-software integer types
preserve their storage size no matter which compiler the code may be compiled on. It is
a simple and safe way to ensure that integer data types are properly preserved.
3
ISO/IEC 9899:1999, C Language Specification
14
Chapter 1 Concepts for Developing Portable Firmware
when a variable is declared of type Axis_t, the data members will be created in the order
x, y, and z in memory. However, the C standard does not specify how the data members
will be byte aligned. The compiler has the option to align the data members in any way
that it chooses. The result could be that x, y, and z occupy contiguous memory, or there
could be padding bytes added between the data members that space the members
by two, four, or some other byte value that would be completely unexpected by a
programmer.
The unspecified structure and union behavior makes it the developer’s job when
porting the firmware to understand how the structure is being defined in memory and
whether the structure is being used in such a way that adding padding bytes could affect
the application’s behavior or performance. The structure could include padding bytes
or even holes depending on the data type being defined and how the compiler vendor
decided to handle the byte alignment.
15
Chapter 1 Concepts for Developing Portable Firmware
The problem with bit fields is that the implementation is completely undefined by
the standard. The compiler implementers get to decide how the bit field will be stored
in memory, including byte alignment and whether the bit field can cross a memory
boundary. Another problem with bit fields is that while they may appear to save
memory, the resulting code required to access the bit field may be large and slow, which
can affect the real-time performance of accessing it. The general recommendation when
it comes to bit fields is that they are non-portable and compiler dependent and should
be avoided for use in firmware that is meant to be reusable and portable.
16
Chapter 1 Concepts for Developing Portable Firmware
The predefined macros from Figure 1-7 that identify the compiler can be used as part
of a preprocessor directive to conditionally compile code. Each compiler that may be
used can then be added to the conditional statement with the non-portable preprocessor
directive that is needed for the task at hand. Figure 1-8 shows how a developer might
take advantage of the predefined compiler macros to conditionally compile a fictitious
#pragma statement into a code base.
17
Chapter 1 Concepts for Developing Portable Firmware
Developers interested in writing portable ANSI-C code should consult the ANSI-C
standard, such as C90, C99, or C11, and check the appendices for implementation-
defined behaviors. A developer may also want to consult their compiler manuals to
determine the extensions and attributes that are available to developers.
E mbedded-Software Architecture
Firmware development in the early days used truly resource-constrained
microcontrollers. Every single bit had to be squeezed from both code and data memory
spaces. Software reusability was a minor concern, and programs were monolithically
developed. The programs would be one giant 50,000-line program, all contained within
a single module, with little to no thought given to architectural design or reuse. The only
goal was to make the software work. Thankfully, times have changed, and while many
microcontroller applications remain “resource constrained,” compiler capabilities and
decreasing memory costs now allow for a software architecture that encourages reuse.
Developing software that is complex, scalable, portable, and reusable requires a
software architecture. A software architecture is the fundamental organization a system
embodies in its components, their relationship to each other and to the environment,
and the principles guiding its design and evolution.4 In other words, a software
architecture is the blueprint from which a developer implements software. A software
architecture is literally analogous to the blueprint an architect would use to design a
building or a bridge.
4
ISO/IEC/IEEE 42010:2011, Systems and software engineering — Architecture
18
Chapter 1 Concepts for Developing Portable Firmware
The software architecture provides a developer with each component and major
software structure, supplies constraints on their performance, and identifies their
dependencies and interactions (the inputs and outputs). For our purposes, we will only
be looking at software architecture from the perspective of organizing firmware into
separate software layers that have contractually specified interfaces to improve portability
and code reuse. Each software has a specific function, such as directly controlling the
microcontroller hardware, running middleware, or containing the system’s application
code. Properly architected software can provide developers with many advantages.
First, a layered architecture can provide a functional boundary between different
components within the software. Take, for example, low-level driver code that makes the
microcontroller work. Including driver code directly within the application code tightly
couples the microcontroller to the application code. Since application code normally
contains algorithms that may be used across multiple products, mixing in low-level
microcontroller code will make it difficult and time consuming to reuse the code. Instead,
a developer who architects layered software can separate the application and low-level
code, allowing both layers to be reused in other applications or on different hardware.
Second, a layered architecture hints at the locations where interfaces within the
software need to be created. For a development team to create firmware that can be
reused, there needs to be an identifiable boundary where an interface can be created
that remains consistent and unchanging as time passes. The interface contains
declarations and function prototypes for controlling software in lower layers.
Third, a layered architecture allows information within the application to be hidden
from other areas that may not need access to it. Consider the example with the low-level
driver. Does the application code really need to know the implementation details for
how the driver works? Surely, someone working at the application level would rather
have a simple function to call, with the desired result happening behind the scenes.
This is the idea behind abstractions, which hide the implementation behavior from the
programmer and simply provide them with a black box. Developing a simple software
architecture can help developers take advantage of these benefits.
Developers looking to create portable firmware that follows a layered software-
architecture model have many different possible models that can be chosen from and
many custom hybrid models that they could undoubtedly develop. The simplest layered
architecture can be seen in Figure 1-9 and contains a driver and application layer
operating on the hardware. The driver layer includes all the code necessary to get the
microcontroller and any other associated board hardware, such as sensors, buttons, and
so forth, running. The application code contains no driver code but has access to the
19
Chapter 1 Concepts for Developing Portable Firmware
low-level hardware through a driver-layer interface that hides the hardware details from
the application developer but still allows them to perform useful functions.
The next model that a developer could choose to implement breaks the software
up into three layers, similar to Figure 1-10. In a three-layer model, the driver and
application layers still exist, but a third “middle” layer has been added. The middle layer
may contain software such as a real-time operating system (RTOS), USB and/or Ethernet
stacks, along with file systems. The middle layer contains software that isn’t directly the
end application code but also does not drive the low-level hardware. For this reason,
components in this layer are often referred to as middleware.
Beyond the three-layer model, developers may find it worthwhile to start breaking
the software up into more refined layers of operation and maybe even provide pathways
for high-level layers to circumvent layers and get direct access into lower software layers.
The architectures can become quite complex and are well beyond the scope of this
book. For now, a four-layer model will be as complex an example as we will examine.
For example, a developer may decide that the board-support package—the integrated
circuits outside of the microcontroller—should be separated from the microcontroller
driver layer. The board-support drivers are usually dependent on the microcontroller
drivers anyway, and in order to improve portability probably should be separated. Doing
this results in one possible four-layer model like the one shown in Figure 1-11.
20
Chapter 1 Concepts for Developing Portable Firmware
Many formal models exist for developing layered software architectures, including
the well-known OSI model, which contains over seven layers. A developer should
examine their requirements and their portability and reuse needs and pick the simplest
architecture that can meet their requirements. Don’t be tempted to build a 30-layer
software architecture if three layers will meet the requirements! The goal is to avoid
complex spaghetti code that is intertwined and entangled and instead develop layered
lasagna code! (Just the thought makes my stomach growl!)
• Abstracting out the details for how the underlying code works
The most interesting firmware layer that developers now have the ability to utilize is
the hardware abstraction layer (HAL). A HAL is an interface that provides the application
developer with a standard function set that can be used to access hardware functions
5
http://whatis.techtarget.com/definition/layering
21
Chapter 1 Concepts for Developing Portable Firmware
without a detailed understanding of how the hardware works. Despite being commonly
referred to as a HAL, it is not the infamous artificial intelligence from 2001: A Space
Odyssey, although sometimes they can be just as devious.
HALs are essentially APIs designed to interact with hardware, and a properly
designed HAL provides developers with many benefits, such as software that
• is portable
• is reusable
SOFTWARE TERMINOLOGY
Driver Layer refers to the software layer that contains low-level, microcontroller-specific
software. The driver layer forms the basis from which higher-level software interacts with and
controls the microcontroller.
Application Layer refers to a software layer used for system- and application-specific
purposes that is decoupled from the underlying hardware. The application code meets
product-specific features and requirements.
Configuration Layer refers to a software layer used to configure components within the layer.
22
Chapter 1 Concepts for Developing Portable Firmware
A poorly designed HAL can result in increased costs and buggy software and can
leave the developer wishing that they were dealing with the previously mentioned
infamous HAL. An example software architecture that utilizes a HAL might look
something like Figure 1-12. We will be discussing HAL design throughout the book.
SOFTWARE TERMINOLOGY
Hardware abstraction layer (HAL) refers to a firmware layer that replaces hardware-level
accesses with higher-level function calls.
Application programming interface (API) refers to functions, routines, and libraries that are
used to accelerate application software development.
6
http://whatis.techtarget.com/definition/interface
23
Chapter 1 Concepts for Developing Portable Firmware
Project Organization
Organizing a project can help improve both portability and maintainability. There are
many ways that developers can organize their software, but the easiest is to attempt to
follow the software layer stack-up. Creating a file system and project folder structure that
matches the layers makes it easy to simply replace a folder (a layer) with new software,
which would also include the components within that layer.
The project should also be organized in such a way within each layer that modules,
tasks, and other relevant code are easily locatable. Some developers like to create folders
for modules or components and keep all configuration, header, and source modules
within the folders. Organizing the software in this way makes it very easy to add and
remove software modules. Other developers prefer to break up and keep header and
source files separate. The method used is not important so much as being consistent and
following a methodology is.
24
Chapter 1 Concepts for Developing Portable Firmware
• Drivers
• Application
• Task Schedulers
• Protocol Stacks
• Configuration
• Endianness
• Processor architecture
• Bus width
• Ambiguous standards
• Modularity
• Code coupling
This is just to name a few. Getting started can be overwhelming and can lead to more
stress and confusion than simply writing very functional code that is discarded later. The
key to successfully developing portable code is to determine how well your firmware
currently meets the portable software characteristics. Once we understand where we are,
we can decide where we want to go and set in motion the steps necessary to get there.
To determine where we are today with developing portable firmware, start by
drawing a diagram like that shown in Figure 1-14. In the diagram, label each spoke
with a portable firmware characteristic and select the eight characteristics most
important to you.
25
Chapter 1 Concepts for Developing Portable Firmware
In each identified category, a developer can evaluate how well their code exhibits
these properties. For example, a developer who has been trying to transition into writing
more portable code may evaluate themselves with a diagram result like Figure 1-15.
A quick look at Figure 1-15 can tell a developer a lot of information. First, we have
strengths in documentation and modularity. That’s a great step toward developing
portable firmware, and we are just getting started. The figure also shows us where our
weaknesses are, such as code coupling and cohesion.
From this glance, we can now determine where we should focus our attention.
Which characteristic, if improved by just a couple points, will most drastically improve
our code? Let’s choose code coupling as an example. If a developer is going to improve
code coupling, they need to determine how they are going to go about making that
improvement. They might decide that the best way to do this is to do one or more of the
following:
26
Chapter 1 Concepts for Developing Portable Firmware
A developer may decide that improving in one area is good enough to start or that all
need to be done. The point is that we aren’t going to start writing perfect, reusable code
overnight. The process is iterative and may take a few years before all the rough edges are
smoothed, but that is okay.
The following is a simple process that developers can use to improve their firmware
portability:
27
Chapter 1 Concepts for Developing Portable Firmware
Going Further
Reading about portable and reusable code is one thing; actually doing it is a completely
different story. The following are some suggestions on steps you can take to start
developing firmware that is more portable:
• Select the language standard that will be used for your development
effort(s) and spend 30 minutes each day reading through the
language standard. Note areas that are not fully defined or could
become pain points.
• Develop your own coding standard on the constructs that are allowed
within an application and how compiler intrinsics and extensions
should be handled.
28
CHAPTER 2
API and HAL
Fundamentals
“Software is a great combination between artistry and engineering.”
—Bill Gates
29
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_2
Chapter 2 API and HAL Fundamentals
1
http://www.webopedia.com/TERM/A/API.html
30
Chapter 2 API and HAL Fundamentals
A HAL is a hardware abstraction layer that defines a set of routines, protocols, and
tools for interacting with the hardware. A HAL is focused on creating abstract, high-
level functions that can be used to make the hardware do something without requiring
detailed knowledge of how the hardware is doing it. A HAL can come in extremely handy
for developers who work with multiple microcontroller hardware types and need to port
applications from one platform to the next.
APIs and HALs are related. It could be argued that they do nearly the same thing.
The difference is that an API is designed to make application software easier while a HAL
is designed to make interacting with low-level hardware easier. An embedded system
that is well designed would have both a HAL to interact with the low-level hardware
and an API that interacts with the HAL to produce a set of APIs that simplify application
development.
SOFTWARE TERMINOLOGY
31
Chapter 2 API and HAL Fundamentals
The first, and probably the most famously known, are the Arduino APIs.2 Every
Arduino board can use common software components and function calls from
the Arduino software library on any Arduino-based board. Arduino provides huge
flexibility in hardware use, and most developers using Arduino know little to nothing
about microcontrollers and sometimes even programming. These libraries provide an
excellent way for non-computer-programming folks to create functional applications.
The problem is that the API is targeted toward rapid prototyping and the maker
community and lacks a professional touch that would be easy to use in a professional
development environment.
Another well-known API example is ARM’s mbed platform. Mbed is like Arduino
in that it provides a common set of software features and functions that can be used to
develop software quickly with little knowledge of the underlying hardware. Professional
developers, though, will once again struggle with the fact that this platform is not
designed to be production intent and lacks important underlying error handling
and software analysis features that would be associated with a production-intent
product. Lacking these important tools and capabilities once again make mbed a great
prototyping platform but not a production-intent system. (There have been massive
efforts under way to fill in these gaps and make mbed a fully production-intent platform
that includes an RTOS).3
Beyond Arduino and mbed, there are professional production-intent standards that
developers can leverage to develop their embedded software and improve its reusability
and portability. A great example is AUTOSAR, which is used in the automotive industry.
AUTOSAR provides a great HAL for interacting with the low-level hardware. The problem
is that AUTOSAR is a bit convoluted and expensive to use as far as processing power goes
and doesn’t play well on resource-constrained microcontroller systems running under
200 MHz.
Unfortunately, a generic, industry-wide accepted standard does not exist for
microcontroller-based systems. ARM has attempted to create standards through their
CMSIS and mbed offerings, but in most cases these can only cover a standard way for
interacting with the microcontroller core and not with the entire microcontroller. Every
microcontroller manufacturer still has their own peripherals and other intellectual
property that are designed to be key differentiators and differ from competitor offerings.
2
https://www.arduino.cc/
3
https://www.mbed.com/en/
32
Chapter 2 API and HAL Fundamentals
For this reason, in many cases these “industry standards” fail, and each vendor is now
producing their own unique and custom standard.
result in major software issues. The issues, if not considered up front, can come back to
haunt a team, causing many sleepless nights as a result of an ill-considered boogeyman.
These issues can include but are not limited to the following:
• Copyright infringement
• Execution efficiency
• Code bloat
• Readability
Microcontroller vendors have started to tie their APIs and HALs into automated
toolchains that allow a developer to select which components they need in a project and
easily configure them. For a developer using these toolchains, life is simplified and huge
time and cost savings can be realized throughout the project. For some, though, it won’t
be all blue skies. A potential issue arises when a team wants to change microcontroller
vendors. Suddenly, all their application code is tied to the vendor’s APIs and
functionality, which are tightly integrated together. Attempting to port that application
code to a new API and HAL can be time consuming and costly.
This brings us to the second issue. A development team may decide that while they
are tightly tied to the toolchain, they can easily just modify the low-level register accesses
to use a different microcontroller and maintain the same API. The problem is that if you
read the fine print for any vendor-supplied software, it is quite clear that the software,
APIs, HALs, and so on are only to be used with their microcontrollers! Using them with a
competitor’s processor is a copyright violation. The result is having to rework or rewrite
a fair amount of software or violate the copyright and nervously wait for potential legal
ramifications (which of course is never the right solution).
Beyond the potential business and legal ramifications of using the software that is
provided by microcontroller vendors, there is also the question of efficiency. Code that
is written for a very specifically defined application can be very efficient. Abstracting the
hardware and attempting to provide hooks for every possible use and application will
add layers to the software. The more layers there are, the more function calls that execute
before work is performed. This means that the system latency will begin to creep up. On
34
Chapter 2 API and HAL Fundamentals
35
Chapter 2 API and HAL Fundamentals
• Written in C99
• Easily extensible
SOFTWARE TERMINOLOGY
Coding standards contain a set of programming rules, naming conventions, and layout
specifications that provide a consistent software.4
With this preview in mind, let’s now examine the characteristics in greater detail.
4
http://www.decision-making-confidence.com/kepner-tregoe-decision-making.html
36
Chapter 2 API and HAL Fundamentals
37
Chapter 2 API and HAL Fundamentals
Ask just about any developer this question and you will get a spectrum of answers ranging
from “Commenting is a time waste” through “There are never enough comments.” The answer
is that there should be enough comments for a developer who is new to maintaining the
software to clearly understand what the code is doing and why. Sometimes a developer can
get away with no comments if the code is self-explanatory, while at other times a developer
may need to write a giant comment block.
38
Chapter 2 API and HAL Fundamentals
error if the compiler has not yet been specified. Chapter 1 showed an example of how
this could be done.
It is easy to start developing with one compiler only to discover compiler
deficiencies, develop a new partnership with a vendor, get a great deal on a new license,
or have team member preferences change. Keeping to ANSI-C and even occasionally
checking compilation against multiple compilers can help ensure that the HAL will be
easily portable to multiple compilers. Numerous teams that I’ve worked with have used
more than one compiler for different product lines or even had their own compiler that
they would periodically compare to GCC. (Maintaining your own custom compiler is
also not recommended even if the company you work for is a silicon behemoth).
APIs are the basic building blocks that applications are built upon. A good API should be small,
efficient, and easily extensible. Throughout my career, I have had the opportunity to use both
good and bad APIs. Developers can try to quantify what a good API is and what a bad API is,
but the fact of the matter is that developers will know it when they see it. A bad API will often
have the following characteristics:
39
Chapter 2 API and HAL Fundamentals
• Is not easily memorable and requires constant looks at the reference manual
40
Chapter 2 API and HAL Fundamentals
The Lord of the Rings is a great movie. I’m a huge fan, but having a single code module to rule
the entire application does not sound like fun. It’s 2017 when I’m writing this, and I still encounter
customers who write their embedded software in a single source module named main. In most
instances, these single-module applications contain at least 100,000 lines of code!
During one particular encounter, we had multiple engineers working on a new product that
was an improvement over an earlier prototype. The goal was to reuse as much code as
possible from the original product in order to save time and costs and bring it up to the latest
and greatest in organization and software architecture. The product had separate hardware
components, so we assigned one engineer to port the code for each device.
Trying to pull code from the 100 KLOC-plus code base was a nightmare since everything
was tightly coupled and hardware dependent. In frustration, I finally said the heck with it and
started from scratch. When I was finally done with my code, the other two engineers were still
frantically trying to make sense of the code they had before them. Countless time was spent
on their part searching and sifting through the code looking for things. Poor code organization
and a single module made their lives a nightmare and cost the company countless weeks if
not months in additional engineering costs.
The only time that One Module should be used to rule them ALL is if that one module is a
configuration module that is used to enable and disable features and configure the project.
41
Chapter 2 API and HAL Fundamentals
provided for one or two architectures with the test details can help an engineer infer the
behavior they can expect. Once implemented in their own design, an engineer can then
verify that assumption themselves, record the new values, and push those back to the
HAL producer to provide yet more data for engineers to make ever better decisions.
Software engineers are very optimistic creatures. If the software runs correctly one time, it
is often assumed that it will always run correctly no matter what the circumstances may be.
Unfortunately, this is not the case!
On numerous occasions, I have encountered application code that just did not seem to work
the way that was expected. After being called in to help identify the issue, I discovered that the
developers not only didn’t include any error handling or checks in their software, they also did
not check return values for functions.
After sprinkling error checking throughout the code, I discovered that one function was
returning a value that stated there was insufficient memory available. After making a slight
adjustment, the code ran fine.
42
Chapter 2 API and HAL Fundamentals
Debugging software can be time consuming and expensive, both financially and emotionally.
Don’t assume that everything will be okay; in fact, assume that nothing is going to go right!
Make sure that all return values are checked for errors. Adding in extra checks may use some
extra time and extra code space and cause a negligible performance hit, but these minor costs
will save far more time, budget, and emotional wear and tear than they cause harm.
Source Code
Unit Tests
Functional
Configuration Compiler Test Reports
Regression
Libraries Integration
Test Harness
43
Chapter 2 API and HAL Fundamentals
An integration server will pull the latest source code, configuration, and libraries and
verify that there are no problems compiling the code. Some setups will even perform
static code analysis and generate reports based on the compilation and code analysis.
Additional analytics can be performed, such as measuring the software function
complexity.
Once the compiler has successfully compiled the code, the executable can be passed
to the test harness. The test harness can use either mock hardware—that is, hardware
that is simulated in memory—or it can use real hardware and integrate into GDB or
other debugging tools. The test harness should have tests that are traceable to the
system requirements. Example tests that would be performed are unit tests on functions,
functional tests to verify that hardware performs as expected, regression tests that cover
all previous test cases and ensure that they still pass, and then perhaps even integration
testing.
44
Chapter 2 API and HAL Fundamentals
There are a few different ways that the KT matrix can be evaluated. In general, each
characteristic in a group is given a rating from 1, being the worst rank in the category,
to X, being the highest rank in that category. Every engineer involved in the decision
provides a ranking for the HAL, and then the ranking is weighted and added to the other
rankings. The rankings for all the criteria are then summed and the HAL with the highest
score is the HAL that best meets the HAL criteria.
45
Chapter 2 API and HAL Fundamentals
There are several factors that a development team needs to consider before deciding
to build their own HAL. These factors include:
• Cost
• Development time
Before deciding to start designing your own HAL, it’s critical to determine whether
you need to design one yourself or if one exists that already meets your needs and
requirements. A good starting place is to do some basic research and identify any HALs
and standards that currently exist and get familiar with them. What are their strengths?
What are their weaknesses? Having this information empowers a team to properly
evaluate whether existing HALs will fit their company’s needs.
The target microcontrollers and application can influence whether a development
team will create their own HAL or use an existing HAL. For example, if a development
team has decided that they will always use a microcontroller from a single
microcontroller supplier, the team may be able to just use the HAL provided by the
microcontroller vendor. This would save the time and cost of developing a HAL from
scratch. However, it also ties the development team into that vendor’s ecosystem
and may make it extremely costly to change microcontrollers later on down the road.
Consider the fact that in most cases those HALs have licenses or copyrights associated
with them and using them with any other manufacturer would violate those licenses.
Developing a HAL from scratch can take some additional development time to be
properly designed as well as some additional up-front costs. The costs are usually offset
and easily recouped after one or two development cycles depending on the experience
of the designing engineers. However, it is not uncommon for a HAL to require multiple
iterations and multiple projects before it is finally fleshed out and covers all the possible
permutations. The hope, however, is that developers can use the knowledge and
experiences in this book to quickly and cost effectively implement their own HALs that
will not tie them to any microcontroller toolchain.
46
Chapter 2 API and HAL Fundamentals
Figure 2-3 has a clear majority of the characteristics we previously discussed that all
HALs should have. The characteristics that are lacking can be easily added by the reader.
Throughout this book, we will go into the details of the HAL listed and discuss the design
decisions and steps to put it together, not just for GPIO but for any microcontroller
peripheral. Consider Figure 2-3 your sneak peek!
47
Chapter 2 API and HAL Fundamentals
48
Chapter 2 API and HAL Fundamentals
• AUTOSAR
A good API will declare many parameters as const because it is just using the data to
perform useful work and wants to protect the data that it is using. APIs that are light on
using const aren’t necessarily bad, but they do open themselves up to the opportunity
for something to go wrong and behave unexpectedly.
50
Chapter 2 API and HAL Fundamentals
5
http://www.freertos.org/a00106.html
51
Chapter 2 API and HAL Fundamentals
6
https://doc.micrium.com/pages/viewpage.action?pageId=10753180
52
Chapter 2 API and HAL Fundamentals
53
Chapter 2 API and HAL Fundamentals
54
Chapter 2 API and HAL Fundamentals
Now, I am not knocking any RTOS, but from a quick look it is obvious that there
is no standard that is being followed either in naming convention or for features and
functionality. Each RTOS will fill a need and a niche, and some will meet the API
characteristics more than others. I’m not advocating one RTOS over another, but rather
simply sharing the API for three very popular and successful RTOSes. Starting with one
RTOS and then trying to switch to another obviously will require rework since the APIs
are not standardized.
W
rapping APIs
As I mentioned earlier, there may be certain components in an embedded system that
meet a common design challenge, such as real-time scheduling, but the components
available on the market do not have a standard interface. When this occurs, developers
can take the matter into their own hands and add an API wrapper to the components to
make them fit a standard interface.
9
http://rtos.com/images/uploads/programmersguide_threadx.pdf
55
Chapter 2 API and HAL Fundamentals
For example, I might have three different RTOSes I want to use in a design, and the
product or process will determine which one I use. As a developer, I can look at the
commonalities between the operating systems and create my own API functions that
will call the desired function in the target RTOS. I could create an API for creating a task,
using mutexes, semaphores, or even message queues. The API would then be a generic
and standard call, which is replaced by my call into the specific RTOS function. Figure 2-9
shows an example of what the wrapper would look like. The application code would use
this function, and then within that function call would be the RTOS-specific task-create
function call.
Using a general wrapper in this way has many advantages, and there are quite a few
places where a developer may want to use a wrapper API, such as:
• RTOS calls
• Memory accesses
• File systems
• Circular buffers
• External devices
Using a wrapper is not all blue skies though. Every function call does incur a little bit
of overhead on the processor, and passing parameters into the function does use some
stack space. In most applications, the overhead performance hit and extra code will be
negligible. Developers should still be careful and aware that the wrapper does affect
performance and code size.
56
Chapter 2 API and HAL Fundamentals
• API quality
• Coding standards
• Robustness
• Code size
• Quality
• Testability
57
Chapter 2 API and HAL Fundamentals
HALs APIs
Abstractions
Hide Implementation Details
Encapsulate Data
Libraries, Frameworks and Components
Interacts with Hardware Interacts with Software
Simplify Design and Implementation
Extendable
Low-Level Software High-Level Software
Configurable
Improve Reusability
Increase Portability
Must be Real -time
G
oing Further
Understanding HALs and APIs requires more than just reading about them in this book.
Practical experience and knowledge is crucial, especially for developers interested in
developing their own portable HALs and APIs. The following are a few thoughts on what
58
Chapter 2 API and HAL Fundamentals
the reader can do to strengthen their understanding and start applying the concepts
we’ve just discussed immediately in their own development efforts:
• Microchip Harmony
• ST Microelectronics STM32CubeMx
59
Chapter 2 API and HAL Fundamentals
• Create a KT Matrix that can be used to evaluate APIs and HALs from
third-party sources. Pick a few external APIs, such as ones for an
RTOS or HAL, and with a close group of engineers walk through the
process for selecting the API.
60
CHAPTER 3
Device Driver
Fundamentals in C
“Software is like entropy. It is difficult to grasp, weighs nothing, and obeys
the second law of thermodynamics; i.e., it always increases.”
—Norman Ralph Augustine
61
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_3
Chapter 3 Device Driver Fundamentals in C
ROM GPIO
62
Chapter 3 Device Driver Fundamentals in C
The CPU region contains control registers for the CPU itself, sometimes related to
interrupts, faults, exceptions, and clock control. CPU registers are typically initialized by
the start-up code, with vendors providing their own interfaces into the memory region.
CPU regions are typically abstracted to hide the inner workings of the microcontroller
from the developer.
On numerous occasions, I have seen clients who get the ingenious idea to save memory and
application time by using RAM to store data between power cycles. The assumption is that by
writing data to RAM, performing a reset, and then powering up, the memory location can then
be read with the previous application’s values and state. I have seen developers most tempted
to do this when creating a bootloader. The data stored is meant to tell the application whether
the application or the bootloader should be loaded.
The problem with using RAM to store data between resets is that the data stored in memory
is NOT guaranteed to persist between the power cycle or reset. The data may be preserved
in most circumstances but undoubtedly is occasionally cleared out or corrupted, resulting in
unexpected behavior. Assuming that the RAM data persists between power cycles will result in
a software bug that is elusive and difficult to consistently repeat.
EEPROM regions are the rarest and typically will not be found on most
microcontrollers. EEPROM provides a developer with a working region for calibration
data that is separate from flash and provides a safe means for updating the data without
the risk of accidentally erasing application code. Microcontrollers that don’t include
EEPROM will typically provide flash libraries that can be used to simulate EEPROM
behavior but risk application code corruption.
The peripheral memory region is the most interesting to driver developers. The
peripheral memory region contains the registers that control the microcontroller’s
peripherals, such as general-purpose input and output, analog-to-digital converters,
serial peripheral interface, and many others. In order to create a driver, a developer must
map the driver code to the memory region that the peripheral registers exist in. Once
again, these regions will vary from one microcontroller to the next. In this chapter, we
will discuss general techniques and strategies for driver development, and then in the
next chapter we will dive into the nitty-gritty details.
63
Chapter 3 Device Driver Fundamentals in C
The memory regions for a microcontroller are not required to be contiguous in any
shape or form. A memory map may start with memory locations for application code,
switch to RAM, then peripherals, and then back to application code. There can even
be large spaces between usable memory locations that are commonly referred to as
memory-map holes. An example of a memory map with holes can be seen in Figure 3-2.
0x600000
ADC
Timer
UART
GPIO
0x200000
unmapped
0x100000
Flash
0x000000
64
Chapter 3 Device Driver Fundamentals in C
allowing for reuse and ease in maintainability. (At times, I have been tempted to swap
out lasagna layers, but I’ve always found it’s just better to eat it!) Remember—we want
to write lasagna code, not spaghetti code! Figure 3-3 is an example software architecture
a developer might choose that decouples the different software layers. We discussed
software architectures back in Chapter 1.
65
Chapter 3 Device Driver Fundamentals in C
The component interfaces occur where one layer touches another. The interface
will consist of functions that result in some action being taken by the component,
such as toggling a pin state, setting a register, or simply reading data. In order for
those functions—the interface—to behave as expected, it can be extremely useful for
developers to create a contract relationship between the interface and the developers
who use it.
Design by Contract
Software interfaces can get complicated very quickly. A modern API and HAL may have
over a hundred interfaces that are used to get the system to behave in the desired way.
One method that can be used to ensure that developers have a clear understanding
of how to use the interface is to use design-by-contract.1 Design-by-contract is a
methodology developers can use to specify pre-conditions, post-conditions, side
effects, and invariants that are associated with the interface. Every component then
has a contract that must be adhered to in order for the component to integrate into the
application successfully. Figure 3-5 demonstrates how design by contract works.
1
https://en.wikipedia.org/wiki/Design_by_contract
66
Chapter 3 Device Driver Fundamentals in C
Errors /
Inputs Exceptions Outputs
Side Effects
As developers, we must examine a component’s inputs, outputs, and the work (the
side effects) that will be performed. The pre-conditions describe what conditions must
already exist within the system prior to performing an operation with the component.
For example, a GPIO pin state cannot be toggled unless it first has the GPIO clock
enabled. Enabling the clock would be a pre-condition or a pre-requisite for the GPIO
component. Failing to meet this condition would result in nothing happening when a
call to perform a GPIO operation occurs.
Once the pre-conditions for a function have been met, there may be inputs that are
provided to the component so that it can carry out its function. An example would be
toggling the state for a GPIO pin. The interface may have a function designed to elicit this
behavior that requires the pin number to be passed in to properly identify the pin that will
be toggled. Some interfaces may require no additional inputs other than making a call to
the interface, while others may require a dozen or more inputs to get the desired behavior.
If the pre-conditions are met and the input data are valid, a developer would expect
there to be a resulting side effect. A side effect is basically just that something in the
system changes. Maybe a memory region is written or read, an i/o state is altered, or
data is simply returned. Something useful happens by interacting with the component’s
interface. The resulting side effect then produces post-conditions that a developer can
expect. The system state has changed into a desired state.
Finally, the outputs for the component are extracted. Perhaps the interface returns
a success or a failure flag—maybe even an error code. Something is returned to let the
caller know that everything proceeded as expected and the resulting side effect should
now be observable.
67
Chapter 3 Device Driver Fundamentals in C
DEFINITIONS
Pre-conditions are conditions required to be met prior to the function being called. Pre-
conditions are specified in the component contract, which frees the function from having to
check the conditions internally.
Post-conditions are conditions guaranteed to be met when the component has completed
execution provided that all the pre-conditions have been met.
Side effects are the effects that the called function has on the system when it is executed.
The side effect is the useful work that is performed by the function.
Invariants are conditions that are specified across the application that must be met to use the
component. For example, when the restrict keyword is being used with a pointer, which tells
the compiler the input will not be used anywhere else within the program.
A
ssertion Fundamentals
Before moving on to discuss driver models and the different methods embedded-
software developers use to create drivers, it is important that we take a brief moment
to review an important construct within the C programming language that is usually
neglected or abused. The construct is the assert macro, which allows a developer to test
assertions about the software.
The best definition for an assertion that I have ever encountered is as follows: “An
assertion is a Boolean expression at a specific point in a program that will be true unless
there is a bug in the program.”2 Assertions can be used to make sure that the program
state is exactly what we expect it to be. If the state is something else, an assertion will
stop execution and provide debug information, such as the file and line number where
the assertion went wrong. A developer can then dive in and understand what happened
before the application has the chance to change states.
The assert macro is defined in the assert.h header file. The assert macro
generally takes the form shown in Figure 3-6. If the assertion is false, a developer-defined
function is called to notify the user about the failed condition. In this case, when the
assertion is false, a message will be printed over the UART that lists the file and line
number of the failed assertion.
2
http://wiki.c2.com/?WhatAreAssertions
68
Chapter 3 Device Driver Fundamentals in C
The reason that I bring up assertions at this point, even though they are really
beyond the scope of this book, is to point out that assertions are a great way to check
inputs, outputs, pre-conditions, and post-conditions for interfaces and functions that are
using design-by-contract interface definitions. A developer can use assert to verify that
the conditions and inputs are met, and if not then there is a bug in the application code
and the developer can be instantly notified that they did something wrong.
Using assertions is straightforward. A developer determines what the precondition
is to the function and then develops an expression to test that condition. For example,
if function x requires that the input be less than 150, a developer would check the pre-
condition in the function using code like that found here:
Every input and pre-condition should be checked at the start of a function. This is
the developers’ way to verify that the contract has been fulfilled by the component user.
The same technique can also be used to verify that the post-conditions, output, and even
the side effect are correct.
Now, some readers may be thinking to themselves that given enough assertions
in the code, the overhead and the code space could quite quickly become too much.
Assertions are meant to catch bugs in the program, and in many cases they are only
enabled during development. Disabling assertions will reclaim code space and a few
instruction cycles. Defining the macro NDEBUG will change the assert macro to an empty
macro, essentially disabling the assertions.
69
Chapter 3 Device Driver Fundamentals in C
Pay attention! This is critical! If assertions are going to be disabled for production,
the final testing and validation needs to be performed with the assertions disabled. The
reason for disabling them is that evaluating the expressions does affect the real-time
performance, even if it is only a few clock cycles. Changing the execution time after
testing could have completely unexpected consequences.
70
Chapter 3 Device Driver Fundamentals in C
(a) (b)
Figure 3-7. (a) blocking driver model (b) non-blocking driver model
On the one hand, blocking drivers can be very simple since they don’t need to return
to the main application and perform monitoring functions. The problem is that the real-
time performance can be severely affected. Alternatively, non-blocking implementations
can be used, which will preserve the real-time performance but will potentially increase
the complexity for the application. The application must now in some way monitor for
when the next character is ready to be placed into the buffer. The two primary ways that
the buffer can be monitored are polling- or interrupt-driven behavior.
71
Chapter 3 Device Driver Fundamentals in C
The standard implementation for printf can get even worse! Printing a fixed
string doesn’t help when debugging a system. The data that is transmitted often
includes variables and data that will change from one iteration to the next and require
substitution. Figure 3-9 shows the same blocking implementation that is now printing
out the system state using printf(“The system state is %d”, State). The result is
that, on average, the transmission takes 21 milliseconds!
Figure 3-9. Blocking printf timing to print “The system state is %d”, State
72
Chapter 3 Device Driver Fundamentals in C
error-prone endeavor. A developer needs to carefully weigh their options and select the
method that is most appropriate for the situation.
If a developer were to go back to their printf implementation and decide to
implement a non-blocking solution that uses the UART transmission complete interrupt
to load a new character into the transmit buffer, they would see a drastic change in their
application’s performance. First, the new implementation would process the string and
prepare it to be transmitted, which, depending on the string’s complexity, could take
anywhere from 0.5 to 2.0 milliseconds for the strings used in the blocking example. Once
the first character was transmitted, the remaining characters would be transmitted in an
interrupt that executed approximately every 1.2 milliseconds, as shown in Figure 3-10.
The major concern then becomes how much CPU time the interrupt is using.
Interrupting the application every 1.2 milliseconds could potentially affect the
application. A developer will want to understand how long the interrupt will be
executing every 1.2 milliseconds. Figure 3-11 shows the average UART transmit
execution time for this example. The interrupt requires approximately 35 microseconds
to clear the transmit-complete flag and then copy the next character into the transmit
buffer.
73
Chapter 3 Device Driver Fundamentals in C
That is a stark difference in performance for printf between blocking and non-
blocking methods! No application code executes for 12 to 21 milliseconds in the blocking
implementation, while non-blocking blocks for 1 to 2 milliseconds up front and then
interrupts for 35 microseconds every 1.2 milliseconds.
For debuggers and microcontrollers that do not include these capabilities or similar
capabilities, developers will still need to be very careful with their printf statements.
Interrupts are not the only method that can be used to minimize how long a
driver blocks the main application for. Developers can also use the direct memory
access (DMA) controller. In a DMA implementation, a developer configures the DMA
74
Chapter 3 Device Driver Fundamentals in C
controller to interrupt and handle data movement from memory into a peripheral or
from a peripheral to memory. The advantage to a DMA is that it is very fast and does
not require the CPU. The CPU can be in a low-power state or executing other code
while the DMA controller is moving data around the system. Considering the printf
example, a developer could set up a memory buffer, then configure the DMA to transmit
x characters from the buffer and into the UART transmit buffer. This implementation
would then remove the periodic interrupt and allow the CPU to focus on the application
code. An example of how a DMA setup would look can be found in Figure 3-12.
RAM
DMA Peripheral
DMA is a powerful tool for developers but can be a complicated topic for a first-
time user. Using DMA can also add unnecessary complexity to the software or result
in abstractions and data movement that is not obvious from a quick look at the
system. The efficiency can be well worth the trouble, however. Just don’t forget: most
microcontrollers have a limited number of DMA channels, so use them wisely!
75
Chapter 3 Device Driver Fundamentals in C
Back when I was working on my master’s degree at the University of Michigan, I was involved
in the embedded-software design and implementation for a small satellite. My primary focus
was the main flight-computer code that interacted with a half dozen or more subsystems
and orchestrated the behavior for the entire satellite. One such subsystem was an Attitude
Determination and Controls (ADACs), and it was experiencing issues retrieving and analyzing
its data. Periodically, data would be lost as if the processor did not have enough throughput to
handle the data stream.
As I sat down to review the firmware with the younger and less experienced engineer, I
discovered that the implementation was flawless. The processor just could not keep up with
the data rate, analysis, and communication simultaneously. Changing the processor was not
an option. The alternative and only chance was to use the DMA to handle the data acquisition
and memory storage and relieve the CPU of the responsibility. After a few short discussions
and what could not have been more than an hour of updates to the drivers and software, the
ADACs subsystem was operating flawlessly.
All that was needed was offloading some data handling from the CPU to the DMA controller.
76
Chapter 3 Device Driver Fundamentals in C
• The interface
• The source code
• A configuration module
How these three pieces are organized is completely up to the developer. In some
cases, a developer may choose to create a folder for the entire component and include
all these pieces together at the component’s top level. In other cases, a developer may
decide to create separate folders, one for each piece. There are many possibilities for
how a component can be organized. A few examples can be seen in Figure 3-13.
77
Chapter 3 Device Driver Fundamentals in C
DEFINITIONS
Module is part of a program that contains one or more routines. One or more independently
developed modules make up a program.3
Component is an identifiable part of a larger program or construction. A component provides a
specific function for the application. An application is divided into components that in turn are
made up of modules.4
Interface is a boundary across which two independent systems meet and act on or
communicate with each other.5
3
h ttps://www.techopedia.com/definition/3843/module
4
http://whatis.techtarget.com/definition/component
5
www.webopedia.com/TERM/I/interface.html
6
http://www.ganssle.com/articles/namingconventions.htm
78
Chapter 3 Device Driver Fundamentals in C
Another convention that I highly recommend is to start with the subsystem and then
work toward the specific. For example, an interface that is going to provide a read of the
digital input/output peripheral would be named:
Dio_Read
The first three letters specify the subsystem followed by an underscore and then the
purpose. This convention flows naturally and makes it very easy for a developer to first
see the main actor and then the purpose for the interface.
• Abstraction
• Encapsulation
• Objects
• Classes
• Inheritance
• Polymorphism
Despite C not being object-oriented, developers can still implement some concepts
in their application that will dramatically improve their software. While there are ways
to create classes, inheritance, and polymorphism in C, if these features are required,
developers would be better off just using C++. Applications can benefit greatly from
using abstractions and encapsulation. Let’s explore these concepts in detail.
7
https://www.techopedia.com/definition/8982/procedural-language
79
Chapter 3 Device Driver Fundamentals in C
DEFINITIONS8
Abstraction is revealing functionality and software features while hiding the implementation
details.
Encapsulation is wrapping related data and code together into a single unit.
8
http://www.javatpoint.com/java-oops-concepts
80
Chapter 3 Device Driver Fundamentals in C
The second step to creating an ADT is to define the operations that can be performed
on the data. The operations that may be performed on an ADT are completely
dependent on the purpose of the ADT. For example, an ADT for a stack might include the
following operations:
• initialization
• pushing data
• popping data
Don’t forget that using an ADT is quite different from the way a developer would
normally manipulate data. Typically, a developer would define the data and write
code that directly manipulates the data. With an abstract data type, developers create
an interface where the data is indirectly modified behind the scenes, leaving the
implementation to the ADT implementer and letting the application developer simply
use the data type.
Next, the ADT interface specification needs to be completed. The interface
specification includes the function prototypes for all the public operations that can
be performed on the ADT. The interface specification will be in the ADT header file.
Considering the stack example, a developer might find that the interface specification
looks something like the code shown in Figure 3-15.
Next, the ADT developer would either create the ADT implementation or a template
for the implementation that would be filled in later. The ADT implementation could
change from one application to the next. In fact, the ADT implementation could
change during project development, and one major benefit to using an ADT is that
the application that uses the ADT doesn’t need to change. The implementation details
are in the source module and “hidden” from the higher-level application developer.
The use of an ADT provides a developer with a high degree of flexibility. An example
implementation for the stack ADT can be found in Figures 3-16 through 3-19.
82
Chapter 3 Device Driver Fundamentals in C
The example implementation doesn’t even allocate the memory for the stack until
runtime. The Stack_Init function is used to dynamically allocate memory for the
ADT. The user has no clue what the implementation does or how it does it and truthfully
doesn’t need to know or care! (Unless it could affect the real-time performance.) All the
application code needs to do is create a pointer that will be used to store the location for
the stack. That pointer should never even be used by the developer directly but only be
used as the data object that is going to be manipulated by the operation functions.
The initialization function for the stack in this implementation is providing a robust
implementation. First, it is checking the malloc return value, which will return zero if the
memory could not be allocated. If everything goes as expected, the implementation will
initialize the stack location member and set the return value.
83
Chapter 3 Device Driver Fundamentals in C
The final step to creating the ADT is to put the ADT to the test. The ADT can be tested
by writing some application code. The application code should declare an ADT and
then manipulate the data through the interface specification. An example initialization
and test for the stack ADT is shown in Figure 3-20. In the example, the stack.h header
file is included in the application. The ADT from the user application’s point of view is
nothing more than a pointer. The Stack_Init function is called, which then performs
the operation on the stack data to allocate memory and prepare it for use.
84
Chapter 3 Device Driver Fundamentals in C
Finally, some data is pushed onto the stack by calling Stack_Push. Note that in the
example application we are not checking the return values. This is something that a
developer should do but that the author decided to not show at this point in time.
Creating an ADT is as simple as that! Using them in your software will hide the
implementation details of a data structure, thus improving software maintenance, reuse,
and portability. Developers who use ADTs will find that they are able to quickly adapt to
changing requirements and save time by not having to dig through code searching for
obscure data references.
85
Chapter 3 Device Driver Fundamentals in C
C
allback Functions
Callback functions are an essential and often critical concept that developers need
in order to create drivers or custom libraries. A callback function is a reference to
executable code that is passed as an argument to other code that allows a lower-level
software layer to call a function defined in a higher-level layer.9 A callback allows a driver
or library developer to specify a behavior at a lower layer but leave the implementation
definition to the application layer.
DEFINITIONS
Callback is a reference to executable code that is passed as an argument to other code that
allows a lower-level software layer to call a function defined in a higher-level layer.
A callback function at its simplest is just a function pointer that is passed to another
function as a parameter. In most instances, a callback will contain three pieces:
• A callback registration
• Callback execution
9
https://en.wikipedia.org/wiki/Callback_(computer_programming)
86
Chapter 3 Device Driver Fundamentals in C
Figure 3-21 shows how these three pieces work together in a typical callback
implementation.
Application
Main Callback
Driver Invoke
Library Callback
Kernel
Callback_Register
Signal Handler
First, a developer creates the library or module that will have an implementation
element that is determined by the application developer. An example might be that
a developer creates a GPIO driver that has an interrupt service routine whose code is
specified by the application developer. The interrupt could handle a button press or
some other functionality. The driver doesn’t care about the functionality, only that at
runtime it knows what function should be called when the interrupt fires. The code that
will invoke the callback function within the module is often called the signal handler.
Next, there needs to be some way to tell the lower-level code what function should
be executed. There are many ways that this can be done, but for a driver module, a
recommended practice is to create a function within the module that is specifically
designed to register a function as a callback. Having a separate function to register the
callback function makes it very clear to the developer that the callback function is being
registered to a specific signal handler. When the register function is called, the desired
function that will be called is passed as a parameter into the module, and that function’s
address is stored.
Finally, the application developer writes their application, which includes creating
the implementation for the callback and initialization code that registers that function
with the library or module. When the application is executed, the low-level code has the
87
Chapter 3 Device Driver Fundamentals in C
callback function address stored, and when the feature needs to execute, it dereferences
the callback function and executes it.
There are two primary examples that a developer can consider for using callbacks.
First, in drivers, a developer will not know how any interrupt service routine might
need to be used by the end application. If the developer is creating a library for some
microcontroller’s peripherals, a callback could be used to specify all the interrupts’
behaviors. Using the callback would allow the developer to make sure that every
interrupt had a default service routine in the event that the application developer did not
register a custom callback function. When callbacks are used with interrupts, developers
need to keep in mind that the best practices for interrupts need to be followed.
Second, callbacks can be used whenever there is common behavior in an application
that might have implementation-specific behaviors. For example, initializing an array is
a very common task that needs to be performed within an application. What if, for some
applications, a developer wants to initialize array elements to all zeroes, while in another
application they want the array elements initialized to random numbers? In this case,
they could use a callback to initialize the arrays.
Examine Figure 3-22. The ArrayInit functiontakes a pointer to an array with
element’s size and then it also takes a pointer to a function that returns integers. The
function at this point is not defined but can be defined by the application code. When
ArrayInit is called, the developer passes in whatever function they choose to initialize
the array elements. A few example functions that could be passed into ArrayInit can be
seen in Figures 3-23 and 3-24.
88
Chapter 3 Device Driver Fundamentals in C
The functions Zeros or Random are passed into ArrayInit depending on how the
application developer wants to initialize the array.
Error Handling
One of the biggest problems with the C programming language is that there really is
not a great way to do error handling or error trapping. Object-oriented languages have
the ability to try a code block and if an error occurs to catch the error. C has no such
capability. The best that C offers is the ability to check a function’s return value.
The problem with checking a function’s return value is that developers are really
really bad at checking return values. It is not mandatory that return values are checked,
so many developers will just ignore them. Ignoring return values is of course just bad
discipline. In many circumstances, error handling in C is done by returning error codes
or that the function completed successfully.
So, how can a developer handle errors in their drivers? The best approach that
developers can take is to create a list of all the possible errors that can occur in the driver
that they are creating. From that list, create an enumeration that contains all the error
codes. Review the list and identify errors that the driver needs to actively manage. These
errors might include transmit flag complete never sets, receive flag complete never sets,
89
Chapter 3 Device Driver Fundamentals in C
10
https://en.wikipedia.org/wiki/Software_design_pattern
90
Chapter 3 Device Driver Fundamentals in C
Uart Rx ISR
Read Character Serial Bus
Store In Buffer
Process Data
Design patterns are the puzzle pieces that can be used to quickly build an embedded
system. The more that an application can leverage design patterns, the faster the
software can be developed. Many drivers will adhere to very common design patterns
that we’ve already discussed in this chapter, such as blocking and non-blocking
architectures. Later in the book, as we dive into specific examples for developing
different peripheral drivers, these design patterns will become clearer.
91
Chapter 3 Device Driver Fundamentals in C
In general, these are minor issues, and developers should not let them get in their
way when developing organized drivers. It’s just important to recognize that it isn’t all
red roses and green grass.
Second, assertions are great for verifying that an assumption for inputs, pre-
conditions, post-conditions, and so forth are correct, but they aren’t exactly free.
Every expression that is evaluated in the assertion uses up some processing time to be
evaluated. While this may only be a few dozen instructions and execute very quickly, it
can influence the real-time system performance. Even worse than the performance, the
assertion takes up a little bit of code space on the microcontroller. Over time, a project
can easily contain an assertion density approaching 3 to 5 percent, which may make
the code look significantly bloated. These are reasons why assertions are often disabled
before testing and production release.
Third, developers need to make sure that they are careful when they use callbacks. In
many cases, callbacks register a function to an interrupt service routine. Since callbacks
execute in an interrupt, they need to be short, fast, and to the point. Developers need to
make sure that they follow best practices for using callbacks, which were discussed in the
callback section.
Finally, developers need to be careful how far they carry the “object-oriented C”
concept. It’s a great idea to encapsulate data, use a few abstract data types, and so forth,
but eventually a point will be reached where it may just make sense to upgrade to C++.
I’ve had the pleasure of teaching a session once on how to create a class using the C
language (not by choice). If you need full object-oriented behavior, just use an object-
oriented language.
Going Further
There are several activities that readers can perform in order to consolidate the driver
concepts that we have just discussed. Drivers are an important foundation in embedded
systems, and it is critical to have a clear understanding of these basic concepts. Some
additional activities that are recommended include:
• Find the memory map for your favorite processor. What memory
regions do the following occupy?
• Flash
• RAM
92
Chapter 3 Device Driver Fundamentals in C
• GPIO
• SPI
• Are there any memory-map holes that you can find? Are there any
memory regions where the memory can be expanded?
• Identify any other conventions that you will use when developing
software going forward.
• Test your skills by creating an abstract data type. Follow the stack
example and implement the stack ADT. Developers interested in the
Stack example source can download it here.11
• Create a simple callback function application that initializes an array.
Create a callback to initialize an array to all zeroes and another to
initialize the array to random numbers.
11
http://www.beningo.com/wp-content/uploads/Downloads/ATP.zip
93
CHAPTER 4
—Norman Ralph
R
eusable Drivers
Writing a driver that can be used from one application to the next can be very helpful to
embedded-software developers. Once a driver is written, developers can focus on the
application code and not worry about the bits and the bytes. Driver design patterns can
be reused not only on the same hardware, but also across multiple platforms with only
minor changes required to adjust the driver to access the different memory regions.
In this chapter, we will examine the different methodologies that developers can use
to map into peripheral memory, and then we will demonstrate how each technique can
be used.
95
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_4
Chapter 4 Writing Reusable Drivers
a bug. These bugs are usually difficult to find and reproduce, which makes them time-
consuming to fix.
One programming language best practice is to limit the scope of all variables and
functions. Keep data and functions need-to-know. Keeping the scope limited will
prevent another application component, or a developer, from accidentally misusing or
trampling over data that they are not supposed to be using.
Junior-level embedded-software developers will often be aware that using global
variables is a frowned-upon practice and will avoid using the extern keyword. The
problem is that by default the extern keyword is implicitly placed before functions and
variables at a file-scope level. This means that if you don’t specify the linkage type, the C
language toolchain will make everything global!
For example, look at the simple module shown in Figure 4-1. The module looks
completely valid. The module would compile without errors or any warnings. However,
to the compiler and linker, the application shown in Figure 4-1 looks like the program
shown in Figure 4-2.
96
Chapter 4 Writing Reusable Drivers
In C, the best way to control the default external linkage in a component is to employ
the static keyword. This is a storage-class specifier that tells the compiler to limit the
variable’s or function’s scope while at the same time telling it to allocate storage for the
variable that will persist throughout the application’s lifetime.1 Static overrides those
implicit extern keywords that are automatically put in front of functions and variables
and instead makes those variables and functions internally linked. The result: variables
and functions that are only available within a single module. Figure 4-3 shows how
static would work in the program that previously had external linkage.
1
C in a Nutshell, pages 156, 165
97
Chapter 4 Writing Reusable Drivers
The problem with the code in Figure 4-4 is that the compiler will look at the code and
realize that in the while loop, UART_REGISTER & UART_TX_FLAG is a constant expression.
Nowhere in the software does that value ever change! So, the compiler will do what it is
designed to do and optimize the code to something like Figure 4-5.
The resulting application that is shown in Figure 4-5 is obviously not what the
developer had intended, but it does teach an important lesson. When accessing
hardware, developers need to reach into the C programming toolbox and pull out the
volatile keyword. This instructs the compiler to reread the object’s value each time it is
used, even if the program itself has not changed it since the previous access.2 A developer
2
C in a Nutshell, pages 53, 127
98
Chapter 4 Writing Reusable Drivers
can prevent the optimized code generation shown in Figure 4-5 by declaring the value
being pointed to by UART_REGISTER as volatile. By doing this, the compiler will recognize
that the expression in the while loop could change at any moment and the value should
be reread to see if it has changed. The updated application can be found in Figure 4-6.
Note where the volatile keyword is located in the updated code. The C statement
is declaring UART_REGISTER as a pointer to a volatile uint8_t. The data is volatile, not
the pointer. The code shown in Figure 4-7 is an example of the wrong place to put the
volatile keyword. The example is showing a volatile pointer to a uint8_t. In general,
having a pointer to a hardware register change is not something that we would want to
have happen in an embedded system.
3
C in a Nutshell, page 57
99
Chapter 4 Writing Reusable Drivers
variable that is being defined as const exists in RAM, a developer could conceivably
create a pointer to the constant variable, typecast off the const, and then change the
value. In many cases, variables declared const in an embedded system will not be stored
in RAM but instead will be in flash. This prevents the constant data from being modified
and really does make const data constant.
A best practice for developing embedded software is to use the const keyword as
often as possible.4 The const keyword does provide a developer some protection through
the compiler if an attempt is made to change the value of an identifier. The primary
places that developers should look to use the const keyword are:
4
Barr Group Best Practices (Embedded C Coding Standard, page 23)
100
Chapter 4 Writing Reusable Drivers
M
emory-Mapping Methodologies
There are several options available to developers to map their code into the
microcontroller’s memory regions. The technique used is going to be dependent upon
an engineer’s need to control:
• Code size
• Execution speed
• Efficiency
• Portability
• Configurability
The simplest techniques tend to not be reusable or portable, while the more complex
techniques are. There are several memory-mapping techniques that are commonly used
in driver design. These methods include the following:
• Using pointers
• Using structures
Let’s examine the different methods that can be used to map a driver to memory.
101
Chapter 4 Writing Reusable Drivers
Writing code in this manner is very manual and labor intensive. The code is written
for a single and very specific setup. The code can be ported, but there are opportunities
for the wrong values to be written, which can lead to a bug and then time spent
debugging. Very simple applications that won’t be reused often use this direct register
write method for setting up and controlling peripherals. Directly writing to registers in
this manner is also fast and efficient, and it doesn’t require a lot of flash space.
Now, the code in Figure 4-10 has a problem! There is a real possibility that if we try
to write code to read the port or a bit on the port the compiler will optimize out the read!
The compiler will see a while loop that is checking a bit state in the register, as shown
in Figure 4-11, and decide that since there is no place in the while loop that changes
the values stored in the location pointed to by Gpio_PortC, there is no reason to keep
reading the value, and that reading the memory location can be optimized out.
102
Chapter 4 Writing Reusable Drivers
In order to resolve this issue, developers need to use the volatile keyword.
Volatile essentially tells the compiler that the data being read can change out of
sequence at any time without any code changing the value. There are three places that
volatile is typically used:
Volatile basically tells the compiler to not optimize out the read but instead make
sure that the data stored in the memory location is read every time the variable is
encountered.
The location that volatile appears in the declaration is critical to properly mapping
a peripheral register. Declaring a pointer to a register using the following statement
tells the compiler that the pointer is volatile, not the data being pointed to. The code in
Figure 4-12 is saying the pointer could change at any time when in fact it’s the data in the
register being pointed to that can change.
Figure 4-12. Incorrectly using the volatile keyword for pointer data
The correct declaration would place the volatile keyword immediately following
the data pointer and not immediately after the pointer, as shown in Figure 4-13.
103
Chapter 4 Writing Reusable Drivers
Figure 4-13. Correctly using the volatile keyword for pointer data
This code tells the compiler that Gpio_PortC is a pointer to a volatile uint32_t.
Remember, when reading a declaration like this, start reading just to the left of
the identifier and read from right to left. This will help provide clarity to the actual
declaration. (I highly recommend reading the section “Complex Declarators” from the
book Expert C Programmers,5 which provides general advice for figuring out what a
declaration means).
With the volatile keyword in the correct place, we now know the compiler won’t
optimize out reading the variable. However, there still is a problem with the declaration
the way it has been written. Take a moment to examine the code shown in Figure 4-14.
5
Expert C Programming: Deep C Secrets, Peter Linden (Prentice Hall, 1994)
104
Chapter 4 Writing Reusable Drivers
Adding the const keyword now makes it so that Port C is a constant pointer to
a volatile uint32_t, and any attempts to increment or decrement the pointer in the
source code will result in a compiler error. Using const in this way is critical to writing
robust code, and yet if you peruse example code or the register definitions provided by
microcontroller suppliers, you will find that the majority ignore this fact and allow their
memory-mapped pointers to be modified within the source.
The structure needs to have each member match the order in order for the
peripheral registers to map properly. Also notice in the declaration that the structure is
abstracting the details for creating a pointer to the structure. With the structure declared
in this manner, a developer could access the peripheral by using the code in Figure 4-17.
105
Chapter 4 Writing Reusable Drivers
I’m not really a big fan of using macros in this way, although when searching through
microcontroller-supplied code you will find that it is quite rampant. An alternative would
be declaring PORTC_BASE_PTR as a standard identifier using the code shown in Figure 4-18.
Using structures to map memory can be efficient and provide developers with a
way to start creating reusable mapped drivers. Using standards such as ARM® Cortex®
Software Interface Standard (CMSIS) can provide a common and reusable method for
accessing peripheral registers that improves portability. Unfortunately, as of this writing,
many vendors will still use their own naming conventions, which still requires a fair
amount of work to adapt to different microcontrollers.
106
Chapter 4 Writing Reusable Drivers
Pointer arrays also help to abstract out the hardware and convert registers into
something more readable and understandable by human programmers. Developers can
create easy-to-understand function names that access the pointer arrays and handle the
details behind the scenes. Initialization structures can even be created that allow a table
to be passed into a driver to initialize the peripheral, once again creating a common,
standard framework that can be reused and easily ported.
Despite the powerful capabilities and portability that pointer arrays bring to the
programming table, there are a few drawbacks that developers need to be aware of. First,
creating pointer arrays will increase the program size when compared with structure
or direct-access memory-mapping methods. The reason for the program increase is
that there are now additional arrays that are storing pointers, and above that there is a
configuration table that will be stored in flash that contains the initialization information
for every peripheral and channel. The program size increase isn’t terribly significant, but
if a developer is limited to a microcontroller with a few thousand kilobytes of flash space
then it will quickly fill with initialization data.
Second, since the peripherals are being accessed through a pointer array, there
can be a performance hit a few clock cycles long when accessing low-level drivers.
If a developer is using an old 8-bit microcontroller running at 8 MHz, there could be
a big problem. Using a modern-day processor such as a 32-bit ARM Cortex-M, the
performance difference is not noticeable in most applications. That said, a developer still
needs to make sure that they monitor their system’s performance.
When comparing the cost and development times to using structures or direct
memory-mapping methods, pointer arrays provide developers with a flexible, reusable
design pattern that is easily scalable and adaptable. Let’s examine how we could map
memory to a timer peripheral using the pointer array mapping technique.
107
Chapter 4 Writing Reusable Drivers
driver that can be reused and follows the pointer array memory-mapping methodology,
there are several steps a developer needs to follow:
• enabling
• and so on
108
Chapter 4 Writing Reusable Drivers
109
Chapter 4 Writing Reusable Drivers
Second, the channel definition will be used by the drivers to access the correct
element in the pointer array. It is therefore critical to make sure that the channel naming
order matches the pointer array order. The channels are used in the driver interface and,
once again, make the code more human readable, as the timer is used throughout the
application.
The channel definition is nothing more than a simple enum. It lists all the available
peripheral channels that are available. For example, a microcontroller with three timers
would list out TIMER0, TIMER1, and TIMER2, as shown in Figure 4-20. In addition to listing
the channels, it is a good practice to create a final enum element named MAX_TIMER or
NUMBER_OF_TIMERS that can then be used as a boundary-condition checker.
110
Chapter 4 Writing Reusable Drivers
Since the configuration has internal linkage, a developer will need to create a helper
function that returns a pointer to the configuration table. A simple helper function can
be seen in Figure 4-22.
111
Chapter 4 Writing Reusable Drivers
The ARRAY_NAME is simply replaced with a description for what the register type
is that the array is mapping to. CHANNELS can be omitted in the array definition, but
if a developer is trying to be as explicit as possible, which is always a great idea, then
specifying the number of elements in the array would be necessary.
It is important to also note that the placement of const and volatile is critical. Placing
them in a different location will completely change what is constant and whether the data or
the pointer will be reread at each program encounter. Const is telling the compiler that the
pointer in the array cannot be changed to point to anything else, keeping our pointers from
changing. On most compilers, this will also force the array to be stored in flash. Volatile
is telling the compiler that the data in the register may change unexpectedly, so reread
the data. A developer may want to go even further by limiting the pointer-array linkage to
internal by declaring the array static, which is a very good programming practice.
Using the generic definition shown in Figure 4-23, a developer will then need to
use the definition pattern to create and populate an array with a pointer to the register
for each peripheral channel. The register definitions are usually already created by
the microcontroller manufacturer and are sometimes already in a pointer form. In
most cases, just the addresses for the registers are defined, and the developer must
typecast the address into a pointer when initializing the array. An example for the timer
peripheral that shows a few pointer-array definitions can be seen in Figure 4-24.
The first step to creating the initialization function is to create a function stub for
Timer_Init that takes a pointer to TimerConfig_t. Don’t forget that TimerConfig_t is a
structure that contains all the initialization information for the different timer channels.
Developers should declare the pointer as const so that the initialization code can’t
accidentally manipulate the pointer. The configuration code is probably stored in flash
anyway, so it can’t easily be changed without active assistance from the flash controller,
but it’s a safe programming practice to declare the pointer const anyway.
Before a single line of code is written, it is wise to take a few minutes to develop an
architectural diagram and a flowchart depicting how the initialization function is going
to behave. A simple activity diagram for initializing the timers through the configuration
table and pointer arrays can be found in Figure 4-25. Literally all that is done is that
the code loops through the configuration table, one entry at a time, and reads the
configuration setting for the peripheral. The setting is then mapped into the correct
register and bits before moving on to the next parameter.
Index < N
Channels?
Return from Init()
Enable Clock
Reset Timer
Clear Count
Calculate Period
Set Prescalers
Configure Interrupts
113
Chapter 4 Writing Reusable Drivers
The result is a simple initialization that just loops through the configuration table
and then writes to the pointer array. A shortened initialization function example can be
seen in Figure 4-26. Notice that every pointer-array access requires us to dereference the
pointer in the array element. Don’t forget that the full source is available with the book
materials.
The initialization can be written to simplify the application developers’ software as
much as possible. For example, a timer module could have the desired baud rate passed
into the initialization, and the driver could calculate the necessary register values based
on the input configuration clock settings. The configuration table then becomes a very
high-level register abstraction that allows a developer not familiar with the hardware to
easily make changes to the timer without having to pull out the datasheet.
114
Chapter 4 Writing Reusable Drivers
115
Chapter 4 Writing Reusable Drivers
• Timer_Init
• Timer_Control (Enable/Disable)
• Timer_IntervalSet
• Timer_ModeSet
116
Chapter 4 Writing Reusable Drivers
• Code size
• Execution speed
• Efficiency
• Portability
• Configurability
Table 4-1 compares the different memory-mapping methods and where they are
best deployed. Keep in mind that the table is doing a direct comparison, and while
one method may be mentioned as being least efficient, a developer should take into
consideration what that really means. It could be that there are a few extra instructions
generated to access the register by indexing an array and dereferencing a pointer.
In most applications, the additional instructions won’t really affect the application
performance, but performing a few experiments can be useful to wrap your mind around
the best and worst cases.
In general, the direct register access technique is best used for very resource-
constrained systems with less than 16 kB of code space. These systems typically are 8-bit
and have clock speeds less than 48 MHz. Pointer-structure mapping is a good general
117
Chapter 4 Writing Reusable Drivers
Going Further
Let’s examine what you can do to take the concepts we’ve discussed in this chapter and
start to apply them to your embedded software.
• Using structures
• Using pointer arrays
118
Chapter 4 Writing Reusable Drivers
119
CHAPTER 5
Documenting Firmware
with Doxygen
“Just because you don’t like something doesn’t mean that it isn’t
helping you.”
—Tim Harford
121
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_5
Chapter 5 Documenting Firmware with Doxygen
Documentation can mean the difference between getting to market quickly or never getting
to market at all. I had a client who was working on a medical device that was inherited from
another engineering company. I was called in to review the code that was available and try to
make heads or tails of what features were completed and where the code stood.
The code existed as a single main.c file of over 100,000 lines of code, with no comments,
cryptic variable names, and no documentation. After months of analysis, we finally scrapped
the entire code base and started from scratch. More than six months of previously developed
effort was lost because the original engineers never bothered to document their work (let
alone follow any recommended coding practice).
122
Chapter 5 Documenting Firmware with Doxygen
There are two approaches that developers must follow in order to generate
documentation that is useful and doesn’t require unrealistic amounts of time. First,
developers need to automatically generate their documentation. There are many tools
available at freemium or even premium costs that can generate documentation based
on the organization of the code and the comments associated with it. One such tool,
Doxygen, will be examined in great detail in this chapter.
Second, developers need to generate all their documentation from a single source.
While there is a need for requirements, design, and reference manuals, these all need to
be maintained in a single source that can be used to generate the individual documents.
Otherwise, if separate sources are used, developers will need to change multiple
sources every time something changes in the software or in their requirements. Using
a single source allows the generation tool to scan for changes and make updates to all
documentation at once.
Even if developers use an automated tool to generate documentation, there is no
guarantee of success without discipline. Developers must be diligent in making sure
that the single source is updated as project and code changes are made. There are two
factors that determine the level of quality one can expect from software documentation:
whether the team is disciplined and whether they use an automated tool. Figure 5-1
demonstrates a way that we can think about documentation.
Disciplined
Sparse Functional /
Accurate
Manual Automated
Non-existent Minimal /
inaccurate
Not Disciplined
123
Chapter 5 Documenting Firmware with Doxygen
In the lower left quadrant, we have a team that is not disciplined and generates
documentation manually, which will result in no documentation at all. These are teams
that either are set up for failure or will require far more time and money to get their
product to market and maintain it. Teams in this quadrant are not capable of creating
portable and reusable firmware, but are instead functional rapid prototypers who can
make something work on a bench but struggle to get anything production ready.
The lower right-hand quadrant, where we have a team that is not disciplined but
has an automated tool available, we create minimal documentation that tends to be
inaccurate. In this circumstance, automated tools are able to parse the general structure
and flow of the code and identify variables. Something is better than nothing, but the
documentation tends to be inaccurate due to developers’ not updating code comments
or adding any comments at all. Developers will still struggle to maintain these systems
and may be frustrated by incorrect information.
The upper left-hand quadrant, where we have a disciplined team manually
generating documentation, will result in accurate documentation, but it will generally
be incomplete and sparse. The reason for this is that such teams need to invest
large amounts of time, money, and effort to generate their documentation, which
very few development teams have. The result is that we end up with great high-level
documentation, but the details tend to be lacking. Many government organizations tend
to fall into this category, although they happily invest the time and money.
The final quadrant, the upper right, is where developers interested in developing
high-quality, reliable, portable, and reusable code should aim to find themselves. These
teams are disciplined, updating code comments and design diagrams as they change.
They use automated tools to scan their code base and comments to generate their
documentation. They focus on the end result and generate functional and accurate
documentation.
124
Chapter 5 Documenting Firmware with Doxygen
A few years before I became a consultant, I was working within the defense industry for a
small business that had been quite successful but had poor software processes. Despite their
success, they had nearly no documentation for any of their software and had a fairly high
turnover rate. One of my primary missions was to help them get their software-development
process under control and develop documentation.
1
Doxygen, August 2015, www.doxygen.org.
125
Chapter 5 Documenting Firmware with Doxygen
Doxygen fit these criteria, while another tool favored by a more senior engineer did not. At the
time, management decided to go with the more senior engineer’s recommendation, and all of
the software was commented using a proprietary format. The tool was buggy and hadn’t had
any updates in over two years. Within a year, the tool was officially abandoned and obsolete.
An expensive and time-consuming effort began to convert the comments to Doxygen.
Doxygen allows just about any kind of data to be added to the documentation,
including images and equations. All the source code is available and hosted on GitHub,
which allows a team to dig through the guts of the tool and modify it as needed. More
important, Doxygen is widely used and supported through various software disciplines,
and for more than ten years has been providing feature improvements and updates at
least three times a year. There is no fear of the tool suddenly disappearing or losing its
place as the standard documentation tool.
I nstalling Doxygen
Doxygen is a fairly simple but very configurable and powerful documentation generation
tool. As developers, we can take advantage of tools such as Doxygen to generate reusable
code modules that are already documented. We can use Doxygen to create templates
of software for APIs or HALs that have the interface already predefined and are simply
waiting for the code for the specific target to be added in order to bring it to life. Since
Doxygen can be so useful for creating reusable code and interfaces, I believe it is critical
to walk through the installation process and cover some of its more interesting features.
You will discover that many of the HAL examples in this book were designed first by
writing Doxygen comments in header and source files. The implementation of those
interfaces was then filled in as needed for target applications.
The first and most important step when installing Doxygen is to locate its
installation file, documentation, and any dependencies. All of the Doxygen installation
and documentation can be found at www.doxygen.org. The installation files can be
acquired from the download link located on the top left-hand side of the Doxygen
website. Doxygen can be downloaded in pure source form from a GitHub repository, or
individual binaries can be downloaded for one’s platform of choice. While many readers
may cringe, I mostly use Doxygen on Windows, but there is support for Mac OS and
Linux, among others. Since I typically use Windows for my development environment,
there are several additional packages required to generate PDF documents and fancy
126
Chapter 5 Documenting Firmware with Doxygen
graphics for call graphs and the like. Before we get into those juicy details, download and
install Doxygen for your operating system of choice.
Next, download and install Graphviz from http://www.graphviz.org/. Graphviz
is an open source graph visualization resource provided by AT&T research. Later, we
will use this package by enabling the HAVE_DOT function in our configuration file to
allow Graphviz to generate our graphs. This results in a more visually appealing and
professional result. Finally, in order to convert documentation into a PDF, install LaTex
(for a Windows user, I highly recommend the use of MikTex) and Ghost Script. Together,
these two packages will allow for PDF generation.
• An images folder to store any visual aids that will be included in the
documentation
I am a big fan of only inventing the wheel once, so as soon as a directory structure
that works for you is determined, copy that folder structure (even any file starters) and
save it somewhere safe for the start of each project.
127
Chapter 5 Documenting Firmware with Doxygen
One of the advantages of using Windows is that the old humdrum of command
prompts and command options are a thing of the past (fine, I admit I still use the
command prompt for things like ipconfig or Python scripts, but I can pretend like the
old terminal days are over). Doxygen for Windows comes with a user interface called
DoxyWizard that can be used to set up a Doxygen configuration file. The configuration
file should be stored in the config folder of the documentation folder that was just
discussed.
DoxyWizard is broken up into a tabbed user interface where each tab acts as a
stepping stone for setting up the project, as can be seen in Figure 5-2. First, we have a
Wizard tab that is extremely useful for configuring the initial project settings, such as
project name, logo, source location, and where to store the documentation. Next, with
the basics entered, the Expert tab allows the fine-tuning of Doxygen for parameters such
as file extensions, messages, HTML, and many other options. Finally, the Run tab is
where a developer can execute Doxygen based on the configuration-file parameters and
build the documentation.
128
Chapter 5 Documenting Firmware with Doxygen
Using the Doxygen Wizard tab is straightforward. Under Project, enter the project
name, a brief description, and the version or ID for the software. If the project has a
logo, the logo file can be selected, and the logo will appear on the top of each HTML
documentation page in the HTML header. I usually just place my company logo, since
each individual project does not have its own logo associated with it. The primary
directory for source code and the destination for the documentation can also be entered.
An example of the Setup page can be found in Figure 5-2.
The Mode menu provides a developer with the ability to select the programming
language that is being used. An estimated 80 percent of all embedded software is
developed in C, which makes the selection of optimizing for C a good guess. Obviously,
if a developer is using C++ then the option for C++ optimization should be selected.
Figure 5-3 shows an example of how the Mode page should look when properly
configured. Note that Doxygen in this case is set to only generate documentation for
documented entities. Documented entities are areas of code that have special comment
blocks associated with them. For a code base without any comments, a developer could
select “All Entities,” and Doxygen would still parse the code and generate at least some
documentation.
129
Chapter 5 Documenting Firmware with Doxygen
The Output option provides a developer with the ability to select the types of
generated documentation that should be created. Figure 5-4 reveals that the options are
HTML, LaTeX, Man pages, RTF, and XML. But what about PDF? I’ve found that the best
way to generate a PDF is to either use the LaTeX output or, better yet, to open the RTF
and save it as a PDF. Sometimes it can be useful to add additional information to one of
the generated files prior to creating the PDF and releasing it. The RTF also has the option
of using a template so that the generated document fits a required format. Creating an
RTF template is beyond the scope of this book, but be aware that templates exist if it is an
area of interest.
No documentation is complete without some sort of diagram, and Doxygen has the
ability to generate a plethora of diagrams automatically for developers. The diagrams do
require GraphViz and the dot tool, so at any point, if graphs in the documentation show
up empty, odds are that Doxygen needs to be repointed to the GraphViz directory.
Developers can select which graphs to include within the documentation. Figure 5-5
shows the Diagrams option, which includes all of the possibilities. A few examples include
class diagrams, call graphs, and dependency graphs. These are good options to include
within automated documentation.
130
Chapter 5 Documenting Firmware with Doxygen
When Doxygen parses the source file, it would discover the /** comment block and
then associate the entire comment with the macro GRAVITY_ACC_MS. All Doxygen blocks
start with /**, but not every C construct is commented in exactly the same way. Let’s
examine some of the common declarations and how a comment block can be formatted
appropriately.
/**
* Defines two variables which specify the spacecraft structure.
*/
typedef struct
{
uint8 Acceleration; /**<Rate spacecraft is accelerating */
uint8 Mass; /**< The current mass of the spacecraft */
}SpaceCraft_t;
132
Chapter 5 Documenting Firmware with Doxygen
The most complicated code blocks to document are functions because they tend to
require more information in order to be completely explicit on their purpose and how
to use them. They have input and return parameters in addition to references to other
functions, and even sometimes example code snippets. That is why it is extremely useful
to create a function template that can be copied, pasted, and modified for each new
function that is developed. Just be warned: copying and pasting a template can result in
the documentation not being up to date if a developer forgets to update the pasted code.
Documenting Functions
When documenting a function, there are several important factors that a developer
needs to ensure are documented to get maximum benefit. The factors include the
following:
• Function name
• A change history documenting all the changes that have been made
to the function with the date, version number, developer who made
the changes, and a description of the change that has been made
The preceding list might at first seem overwhelming. There is a lot of information
that needs to be included. But consider what would happen if any of this information
were omitted. Take, for example, omitting whether the parameters are inputs or outputs
to the function. A developer looking at the function will need to take extra time to
133
Chapter 5 Documenting Firmware with Doxygen
determine what the parameters are doing, and might even need to experiment to get it
right. Or worse, they could just implement what they think is right and hope for the best.
Hello, new bug! Such a simple piece of documentation will make it very clear what the
parameters are doing. Remember, sometimes the code isn’t readily available
(in binary format), which means the documentation and the function prototype are the
only information a developer has to go on.
A developer looking to properly document their function will need to create a
function comment block that contains all this information. The first step is to provide
the function name in the comment block. Documenting all the features in the preceding
list will take up quite a few lines of code, and since the comment block should be above
the function definition, we want to make sure that we can easily find the function name,
which will follow dozens of lines later. The comment block will start with the text shown
in Listing 5-1.
/******************************************************************
* Function : Dio_Init()
*//**
In the preceding case, we don’t want the function name to be included multiple
times in a row within the generated documentation, so we leave the function name
outside the Doxygen comment block. The comment block doesn’t start until the /**
sequence. Doxygen will automatically associate this comment block with the function
that immediately follows it and associate the comment block with the function name.
Including the function name in the Doxygen block would duplicate the function name in
the documentation, which would make the resulting documentation confusing.
The next step is to provide a brief description of the function’s purpose. Since the
Doxygen comment block has already been started, we can simply start entering the text
that we want for the description. An example for the description block can be seen in
Listing 5-2. In this case, we want to create a heading within the comment block with
the text “Description” in bold face. We can do this by placing \b before the text. The
remainder of the comment should simply state the purpose of the function.
134
Chapter 5 Documenting Firmware with Doxygen
* \b Description:
*
* This function is used to initialize the Dio based on the
* configuration table defined in dio_cfg module.
Next, a developer should take the time to carefully think through any of the function
pre-conditions that need to be documented. For example, before making a call to a
peripheral transmit function, an application should have already called the peripheral
initialization function and configured the peripheral clocks. Documenting the
pre-conditions is essentially a checklist for developers on what they need to make sure
happens before ever using the function. An example of a pre-condition/post-condition
block can be seen in Listing 5-3.
The function parameter list and return data should be the next information listed
inside the comment block. In order to document parameters in Doxygen, a developer
should use the specialized Doxygen tag @param. Doxygen has several specialized tags
that provide the tool with information on how to process the comment block. Refer to
the latest documentation for a complete tag list. For parameters, @param can be used by
itself, but it is recommended that developers follow the tag with square brackets [], then
specify the parameter direction, such as an input [in], output [out], or both [in/out].
An example can be seen in Listing 5-4. The return parameter for the function is specified
by using the @return tag followed by the type of data being returned and a description.
Developers should include at least a short example of how the function can be used.
There is a mechanism within Doxygen that allows a developer to insert code snippets
into the documentation. In order to show code within the documentation, two special
tags are required, the @code and @endcode tags. As one might guess, the @code tag is used
to tell Doxygen that the following comment block contains code, while @endcode tells
Doxygen that the code block is complete. The code example can be inserted in between
the tags. Doxygen will parse the code and generate a special documentation block that
shows the code. An example of how to use the tags can be seen in Listing 5-5.
* \b Example:
* @code
* const DioConfig_t *DioConfig = Dio_ConfigGet();
*
* Dio_Init(DioConfig);
* @endcode
The next critical puzzle piece is to tell Doxgyen what other functions are related to
this function so that links to those functions can be generated in the documentation.
The code-block format is to use the @see tag followed by the name of the function. If
the function exists within the documentation, Doxygen will create a hyperlink in the
HTML documentation that allows a developer to easily navigate to related functions to
understand how they work. Listing 5-6 shows how to use the @see tag.
Finally, our function block could contain a change history for the function. A change
history isn’t necessarily required, but in safety-critical systems developers may want
to note at the function level the changes that were made and when they were made.
Change information could be kept in a general log or at the beginning of a module, but it
is up to the developer to decide how they want to track changes.
136
Chapter 5 Documenting Firmware with Doxygen
The change-history block is going to look a bit crazy at first because there is HTML
formatting included so that the change list looks presentable in the final documentation.
Without the HTML tags, the generated documentation would not align or look nice,
which would undoubtedly drive management crazy. A developer can insert HTML tags
into the documentation, such as <br> for a line break and <b> to start bold-faced text
and </b> to end bold-faced text. In the generated documentation, a change history
looks most presentable when using a table that has an 800-pixel width. An example
change-history block can be seen in Figure 5-7.
Each documentation block that we have discussed can be pulled together into a
single block that results in a nice, legible, and reusable function template that can be
used to quickly generate adequate function documentation with minimal effort and
time input. A template that is fully assembled and ready to be used can be found at
www.beningo.com.
Documenting Modules
Application code is going to contain a series of header and source modules that contain
code comments. To generate the most consistent documentation possible, there are two
additional pieces of information that developers need to add to their modules to ensure
full documentation. The first is a module header. The header is something that nearly
every developer already adds to their code, except in this case they are replacing general
text with specialized Doxygen tags. Typical information that is included in a module
header is the following:
• Module name
• Filename
• File description
• Module author
137
Chapter 5 Documenting Firmware with Doxygen
• Module version
• Compiler used
• Target
• Copyright
• Licensing information
Listing 5-7 demonstrates what a typical module header would look like. Notice that
the information that we would normally put in the module header simply gets an @
symbol added before it so that Doxygen can place the information in the documentation.
A module header of this type would go into both header files (*.h) and source files (*.c).
/******************************************************************
* @Title : Digital Input / Output (DIO)
* @Filename : dio.c
* @Author : Jacob W. Beningo
* @Origin Date : 09/01/2015
* @Version : 1.0.0
* @Compiler : TBD
* @Target : TBD
* @Notes : None
*
* THIS SOFTWARE IS PROVIDED BY BENINGO EMBEDDED GROUP
* "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BENINGO
* EMBEDDED GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
2
Legal wording is modified from Freescale source example code and provided as an example.
138
Chapter 5 Documenting Firmware with Doxygen
At this point, a developer might think that is all they need to know about Doxygen
to start, but there is still one more interesting feature that can be used to organize
the resulting documentation. Doxygen contains an @addtogroup tag that allows the
documentation to be organized by group. For example, a developer may be developing a
hardware abstraction layer and wants all the modules contained within it to be shown in
the documentation together under the group HAL. In this case, the developer would add
the @addtogroup tag near the beginning of the module along with a curly bracket { (I like
to call them squirrelly brackets). At the bottom of the module, a developer would then
add one final closing squirrelly bracket. Don’t forget that the squirrelly brackets must be
within a comment block, otherwise the compiler will try to process them. An example of
adding the contents of dio.c into a HAL group can be seen in Listing 5-8.
/** @}*/
139
Chapter 5 Documenting Firmware with Doxygen
ported and reused, so there must be some way to decrease the labor intensity required
to document source code. The easiest way to document code is to create a header and
source file template that contains generic starter information and formatting so that
every time a new module is created, the template is used and contains all the Doxygen
formatting and tags. The template will provide a consistent look for every module within
the code base.
Figuring out all the little nuances Doxygen requires can take some time and some
trial and error. I’ve been using Doxygen for almost a decade (if not longer), and I still
periodically make adjustments and tweaks to my template. A developer could start from
scratch with a blank header and source module, or they could download the templates
that accompany this book and modify those templates for their own use. The resources
at the end of this chapter identify where the templates can be downloaded.
Once the template has been downloaded, a developer should review each
documentation section. First, review how each tag is used and the way each C language
construct is documented. If the documentation does not make sense, navigate to the
Doxygen website and review the user-manual entries on that tag. Run Doxygen and
review what the generated HTML documentation looks like. At this point, a developer
can start to make modifications to the template and then rapidly observe how the
changes affect the final output.
140
Chapter 5 Documenting Firmware with Doxygen
contents with a series of web links that could be used to navigate to pages with important
developer information. Examples for main page information include the following:
• Version log (which version is this and how have things changed from
version to version?)
• Acronyms (what do all these funny terms mean? i.e., ADT, A2D, SPI,
CAN, PWM, etc.)
• HALs
• Middleware
• OS information
• Testing and validation (how did we prove that this version actually
works?)
Anything that a developer needs to know should be included as part of the main
page. Creating the main page starts out relatively simple. There are two primary methods
that can be used to populate the main page. First, a single file can be used in which the
entire table of contents is added. For small projects, a single file can make a lot of sense
since there probably isn’t a lot of information that needs to be recorded. However, as
projects grow, a single main page file can become rather large and difficult to maintain.
A better approach would be to create a file for every element of the table of contents
and then have Doxygen merge them into a main page. For now, we will only examine
the first method, and the reader can at their own leisure investigate the more advanced
technique.
141
Chapter 5 Documenting Firmware with Doxygen
Doxygen recognizes a file to be the main page by identifying the @mainpage tag at the
top of the file. After the @mainpage tag has been added to the file, a developer needs to use
HTML tags to create the layout and the information flow for their page. Being an expert at
HTML is not required. There are a few HTML commands that a developer will find useful,
which can be found in Figure 5-8. The easiest way to create links for the table is to use
the HTML anchor tag. When a link is clicked that has an associated anchor, the page will
jump to the anchor point, allowing the main page documentation to be easily navigated.
Each entry in the table of contents section of the main page can be considered its
own separate section. Doxygen has a built-in section command that can be used to
separate the content. Doxygen even provides a subsection command for the event that
we need to break up our information into even smaller pieces. Sections will allow a
developer to organize their main page and properly control the flow of information.
As with any document, a picture is worth a thousand words, and Doxygen even has a tag
to include them. The image tag consists of the command image, a type such as html, rtf, or
latex, and then the filename, such as image.jpg. Due to the way Doxygen handles images, a
developer does need to include multiple image tags if more than one type of documentation
is going to be created. For example, if a developer wants to create HTML, RTF, and LaTex
files, an image tag needs to be added that includes the command for all three formats.
electronics-blogs/embedded-basics/4422388/10-Tricks-for-Documenting-Embedded-Software
142
Chapter 5 Documenting Firmware with Doxygen
commented code base. Source code that is well documented can decrease the cost and
time to market by providing insights into the software that would otherwise require
time and experimentation to jog the developers’ memory on the what and why of the
code’s behavior. These insights, if lost, can increase costs and delay time to market by
introducing bugs into the code base. Here are ten simple tips that can be followed to help
ensure that not only does the software get documented but also that it is documented
with useful information.
The comment itself leaves quite a bit to be desired. Anyone with a basic
understanding of the C language knows by observation what the line of code is doing,
but why are we shifting by 8? Why are we storing the shifted bit pattern in PortB?
A developer who reads this line of code six months or a year after writing it will have
little idea without investigation as to what this line is really doing. Something more
appropriate might look something like the following:
// Port B bit 8 controls the motor relay that needs to be turned off
// during the emergency stop procedure. Setting bit 8 high will
// disengage the motor through a relay.
*Gpio_PortB |= (1UL<<8);
This comment may not be perfect, but it explains why the developer is shifting a
bitwise ORing into PortB.
wait until after the software is written, but the pressure of getting to market and other
priorities often make it highly unlikely that the comments will convey the original intent.
An alternative to writing comments during or after the code is to instead write the
comments before the software is written. This has the unique advantage of allowing the
developer to think through what they are about to code and the why before ever writing
a single line of code. It can be thought of as a translation of the software architecture and
design phase of development into source code. This keeps the software design at the
forefront of the developers’ minds and allows them to think clearly about what it is they
are about to write code for.
144
Chapter 5 Documenting Firmware with Doxygen
to stylistic differences, the result being that code reviews are easier because the code
style is uniform and the actual code can be the focus rather than superficial details about
comment locations.
• File name
• Author
• Origin date
• Copyright information
• Miscellaneous notes
• Revision information
145
Chapter 5 Documenting Firmware with Doxygen
header and source file template that can be used to develop embedded software and that
uses Doxygen tags can be found at http://www.beningo.com/162-code-templates/.
146
Chapter 5 Documenting Firmware with Doxygen
Starting an identifier in this way has many advantages. First, there is no need to
reference the variable declaration to get the type. This can save time otherwise spent
continually having to refresh on the type and size of the variable and whether it needs
to be cast in the calculation. Second, it makes it easier to spot casting errors, such as
multiplying two 8-bit numbers without a cast.
Starting an identifier with the type is a trend that seems to come and go over time.
Personally, the author has bounced back and forth on this naming convention, but it
seems to prove very useful for identifiers used in mathematical calculations and can
make mathematical errors much more obvious.
G
oing Further
Reading about automatic documentation generation is one thing, but actually doing
it is a completely different story. The following are some suggestions on next steps to
improve the way your software is documented:
• Identify three improvements that can be started over the next three
months that can take your documentation effort from its current
place on the spectrum toward where you want to be.
• Review each template and become familiar with the different tags used.
• Update the template and main page for your own purposes and needs.
• Separate the main page file into separate files for each of the
table of contents items. Separating the files will make them more
maintainable and modular.
• Add the formatting and style of the Doxygen comment blocks to your
own C style guide.
• Experiment with the advanced tabs within the DoxyWizard and learn
what each feature does and how it affects the generated output.
148
CHAPTER 6
1
roundhog Day, the 1993 comedy starring Bill Murray. If you don’t understand this reference
G
then stop now, go on Netflix, Hulu, etc., and watch the movie. An all-time classic.
149
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_6
Chapter 6 The Hardware Abstraction Layer Design Process
the HAL creation process, but before we do, let’s take a look at the characteristics that
every HAL needs to have. Keep in mind that this book examines a HAL that jumpstarts
a developer’s HAL needs. Rather than taking years to tweak, the readers of this book will
be able develop a HAL very quickly based on the processes and accompanying materials.
• Human readable
• Abstracted complexities
• Well documented
• Portable
• Encapsulates data
• Reusable
• Maintainable
The hardware abstraction layer should contain a basic set of functions to control
the underlying peripherals that are human readable and generic. The interface should
be simple and contain fewer than a dozen functions. The more complex the interface
becomes, the more difficult the interface will be to understand, port, and just simply use.
Developers should only expose the need-to-know information of the interface and allow
all the details to be hidden behind the interface. Developers who use the HAL don’t need
to be an expert in the underlying hardware and complexities, just an expert in how to use
the interface!
150
Chapter 6 The Hardware Abstraction Layer Design Process
A well-designed and -executed HAL should simplify application development along with many
other value-added benefits, such as faster development and decreased costs. However, when
the HAL interface is designed, developers need to make sure that they provide verbose error
codes and documentation that specifies what causes those errors. On numerous occasions,
I’ve encountered vendor code that has all the dressings and appearance of being great only
to discover later that when an issue occurred behind the interface, it was nearly impossible to
troubleshoot and figure out what was wrong. When this happens, debugging the black box can
be challenging and time consuming. Test and validate any vendor code before committing to it!
The process, while apparently simple, can require a few executions before becoming
completely clear. In this chapter, we will walk through this generic process for designing
a hardware abstraction layer, and then in subsequent chapters we will walk through the
process again for specific peripherals and external components.
151
Chapter 6 The Hardware Abstraction Layer Design Process
Feature #1 x x x x x
Feature #2 x x x x x
Feature #3 x x x
…
152
Chapter 6 The Hardware Abstraction Layer Design Process
One of the best areas of the datasheet to review is the register map. The registers
reveal what configuration settings are available for the peripheral. Reading the
peripheral’s general description can be helpful, but the details are in the registers.
For example, a developer creating a HAL for a GPIO device would find the ability to
multiplex the pins, set pins as inputs or outputs, and control the output of the pins. The
general description may not mention these since they appear obvious to a seasoned
developer. Reviewing the register map makes these capabilities obvious.
Once the feature matrix is completed, a developer should review the matrix and
identify the features that are common to every microcontroller and which are attempts
to differentiate the microcontroller. The common features, such as setting the pin
multiplexer for a GPIO pin, will be added to the HAL interface, while non-common
features such as input validation will be included through a generic interface. The
common features will be the features that every single microcontroller vendor peripheral
has, and those are the features to design the interface around.
• A common interface
• An uncommon interface
• Callback registration
153
Chapter 6 The Hardware Abstraction Layer Design Process
The uncommon interfaces into the peripheral have the potential to clutter up the
interface and make it unwieldy. In order to handle any custom features built into the
peripheral, a very simple interface can be created that allows an application developer to
have full control and access to the peripheral to set up and configure those features. By
keeping the HAL interface generic, the application code can extend the HAL to include
those custom features. As far as the HAL is concerned, the interface is nothing more than
presenting a method for reading and writing hardware registers.
Take a moment to look at the generic definition listed in Figure 6-2. Notice that even
though these two interfaces are designed for uncommon peripheral features, we’ve
managed to create a generic and reusable interface. That is a huge plus. The downside
is that if a developer wants to use these customized features they need to dig into the
datasheet, learn how the extended features work and how to set them up, and then
extend the interface into their application code. In most circumstances though, the
common interfaces are what will be used, so the downside to this technique is actually
quite minimal.
The final piece to the HAL design puzzle is the callback registration interface. Every
single peripheral has interrupts, and if we are designing a clean, reusable interface, the
callback interface will provide developers with a clean way of customizing the interrupt
needs without having to continually rewrite the driver when it is used in different
applications. Interrupt service handlers can be written at the application level and then
registered as callbacks with the specified interrupt through the callback interface.
154
Chapter 6 The Hardware Abstraction Layer Design Process
Developers may be wondering, why is there only a register function and no way to
unregister a callback? The best practice for using interrupt callbacks would be to assign
callbacks during the system initialization. Once registered, there shouldn’t be any need
to unregister or change the behavior of the system. If for some reason there is, simply
register a new function with the driver. The new registration will override the old. If the
developer wants nothing to be associated with the callback, simply register a default or
exception handler.
155
Chapter 6 The Hardware Abstraction Layer Design Process
they are about to perform. There is a simple process that developers can follow to create
their documentation, which can be found here:
5) Repeat the preceding steps until all the features for the peripheral
have been documented.
Once the documentation has been developed, filling in the stubs is trivial. The
documentation literally serves as our design document, and we simply read the
documentation and then implement what we read. For example, take a look at the
function block found in Listing 6-1, which shows the initial documentation for the
Pwm_Init interface. Notice that the developer has now had time to think through the
interface and identify pre-conditions and post-conditions along with the data that needs
to be passed into and out of the function. At this stage, a developer can fill in the stub.
/**********************************************************************
* Function : Pwm_Init()
*//**
* \b Description:
*
* This function is used to initialize the pwm based on the configuration
table defined in pwm_cfg module.
*
* PRE-CONDITION: Configuration table needs to populated (sizeof > 0)
* PRE-CONDITION: The MCU clocks must be configured and enabled.
*
156
Chapter 6 The Hardware Abstraction Layer Design Process
Filling in the stub is super easy. The function documentation is already completed,
and all the developer needs to do is read the text and convert it into code. The developer
can read through the documentation and simply execute these next steps:
1) Read the feature name; create a function with the same name.
3) Select appropriate types for the parameters if they have not been
specified in the documentation (some interface data types may
change based on the target architecture).
6) Copy the function implementation and add it to the header file for
the prototype declaration.
Before moving on to the implementation phase, developers should make sure that
they save the completed template in their revision-control system. Developers will
find that as they implement the HAL on multiple architectures and use it on different
projects, the HAL may change slightly with time. This is perfectly normal but needs to
be documented. A strict control process should be followed so that applications using
different HAL versions don’t run into long-term maintenance issues.
157
Chapter 6 The Hardware Abstraction Layer Design Process
158
Chapter 6 The Hardware Abstraction Layer Design Process
When testing a HAL, there are a few tips and tricks that developers should keep in
mind to minimize the stress and pain. These include the following:
Figure 6-4 shows an example setup for testing a HAL. A developer could use a code
test harness, but to really test an embedded system the tests should be run on live
hardware. Figure 6-4 shows the use of an external test bench that stimulates the HAL and
peripheral to perform its different functions. Developers can make testing as simple or as
complicated as is needed. A very robust check of the implementation would transmit the
different possible configuration tables to the HAL and then verify that all registers are set
up as expected.
159
Chapter 6 The Hardware Abstraction Layer Design Process
Programmer
Bus Monitor /
Test PC Target MCU
Communications
Logic Analyzer
I/O Monitor
Testing can be a very time-consuming process, especially in the early stages of the
HAL design. Keep in mind that once the tests and the interface are created, they are
designed once and used forever. The investment in most cases is well worth it, especially
when one considers the typical cost to resolve a software bug.
2
riginally published on June 2, 2015 @ EDN.com: http://www.edn.com/electronics-blogs/
O
embedded-basics/4439613/10-Tips-for-designing-a-HAL
161
Chapter 6 The Hardware Abstraction Layer Design Process
162
Chapter 6 The Hardware Abstraction Layer Design Process
No one gets a perfect HAL on the first try. The HAL that I use in my own development efforts
and with my clients is a HAL that I developed over the course of five to seven years. The first
iteration worked with a single microcontroller, a PIC24. After the first project, the HAL was
ported to a Freescale Kinetis-L component, which revealed numerous flaws and holes in the
HAL. The next port proved to require only minor cosmetic changes.
Every iteration afterward didn’t change the existing HAL at all but instead added additional
features, such as handling callbacks and the ability to extend the interface easily. The most
important aspect was that with each iteration, the documentation became clearer and
included more examples. Eventually, the HAL matured to the point where porting it to a
new microcontroller requires nearly no changes whatsoever! Start simple and use the time
available wisely, and before you know it you will have a robust and portable HAL.
163
Chapter 6 The Hardware Abstraction Layer Design Process
their requirements and application needs. The API or HAL should allow for different
low-level implementation strategies to be implemented and supported.
164
Chapter 6 The Hardware Abstraction Layer Design Process
G
oing Further
We’ve examined a fair amount of information on how to create a HAL from a generic
point of view. In the next chapters, we will walk through the process again for a number
of microcontroller peripherals. The following are ideas on how you can take the concepts
in this chapter a bit further:
• Walk through the process in this chapter and design a HAL for the
GPIO peripheral.
• Review any existing HALs and list updates and changes that need to
be made to them.
165
CHAPTER 7
167
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_7
Chapter 7 HAL Design for GPIO
the developer will quickly learn which features are common and which are meant to be
product differentiators.
Before jumping right into the datasheet and getting to work, a team should identify
at least three different microcontrollers that will be used for comparison. Since each
microcontroller vendor and architecture can vary drastically in capabilities, selecting
from the broadest parts range will help ensure that the largest possible combinations
are examined. For the examples in this book, we will examine the following
microcontrollers:
• NXP Kinetis-L KL25Z family (32-bit ARM Cortex-M0+)1
• STMicroelectronics STM32F4 family (32-bit ARM Cortex-M4)2
From reviewing the preceding list, the reader can see that we have a sampling of 8-,
16-, and 32-bit cores along with selections from different silicon vendors that contain
ARM cores. While we will not see every possible permutation for the peripherals, using
just these four microcontrollers will allow for a complete HAL to be developed.
During the initial datasheet review, developers should be attempting to get a general
feel for how the peripherals work and its general capabilities. Lower-level details such as
the register mappings will be examined in depth during the feature-identification step.
Most microcontroller datasheets are thousands of pages of technical details. In this step,
just finding the right datasheet and identifying the correct pages and sections in those
manuals will prepare developers for the real work that follows.
1
XP KL25Z Sub-Family Reference Manual
N
2
ST Microelectronics STM32F427xx Datasheet
3
Microchip PIC24FJ128GA010 Family Datasheet
4
Microchip PIC18F2455 Datasheet
168
Chapter 7 HAL Design for GPIO
features. The easiest way to record the different features is to use an Excel spreadsheet.
By using a spreadsheet, a developer can list each microcontroller along the spreadsheet’s
top row, the features down the first column as they are discovered, and then also provide
a mark to indicate whether the microcontroller under review supports the feature.
Examining each microcontroller’s GPIO datasheet results in a table like Table 7-1.
Pin Output X X X X
Pin Input X X X X
Pin Toggle X X X X
Port Output X X X X
Port Input X X X X
Port Data Direction X X X X
Multiplexing X X X X
Pull-up/down Resistors X X
The table is very useful because at a quick glance developers can see what features
for the peripheral are common across any microcontroller and which ones are
specialized. They can also see where the differences are. Take, for example, the STM32F4
and the PIC18F. Both microcontrollers have internal pull-up resistors, while the other
microcontrollers don’t have this feature. These minor differences will potentially come
into play when the HAL is designed or could be critical when the configuration table for
the peripheral is developed. For GPIO, the differences seem minor, but as we will see
with other peripherals, the differences can become quite large.
169
Chapter 7 HAL Design for GPIO
Table 7-1 provides a developer with some functional details that the HAL is going to need
to exhibit in order to give the application developer enough control over the hardware.
After all, we want our HAL to abstract the low-level hardware and make it easier for the
application developer to interact with the microcontroller.
Every HAL interface is going to require, at a minimum, the following:
• Initialization
• Input/output
• Low-level register access
• Callbacks
The easiest place to start designing is the initialization. Every peripheral initialization
will follow a simple design. The initialization will start with a peripheral identifier, such
as Dio or Gpio, followed by an underscore (_), and then the function that the interface
will provide. When creating your first HAL, the initialization should return void until the
interface has become mature enough to return error codes. The choice is completely up
to the implementer though, if you want to leave the hooks in for errors from the start.
The initialization function should take a pointer to a configuration table that will
tell the initialization function how to initialize all the Gpio registers. The configuration
table in systems that are small could contain nearly no information at all, whereas
sophisticated systems could contain hundreds of entries. Just keep in mind, the
larger the table is, the larger the amount of flash space is that will be used for that
configuration. The benefit is that using a configuration table will ease firmware
maintenance and improve readability and reusability. On very resource-constrained
systems where a configuration table would use too much flash space, the initialization
can be hard coded behind the interface, and the interface can be left the same. An
example for the Dio_Init function can be seen here:
The next critical interface for the GPIO HAL is to determine the necessary inputs
and outputs required by the peripheral. For GPIO, the number of input and output
interfaces has the potential to get out of control very quickly. A developer could act on
individual pins, entire ports, adjust modes, and validate inputs, just to name a few. When
developing an interface, a developer should attempt to minimize it so that it doesn’t
become too large and unwieldy.
170
Chapter 7 HAL Design for GPIO
My personal preference is to operate on the GPIO interface at only the pin level.
I view every single pin as an individual channel for the peripheral interface and
design my HAL accordingly. For example, I include ChannelRead, ChannelWrite, and
ChannelToggle functions within my HAL. ChannelRead is used to read the input state for
a pin. ChannelWrite is used to write a desired state to an individual pin. ChannelToggle
will simply toggle the state for the desired pin. I keep each function separate, but if the
interface were to get too large, these three could be combined into a single function that
takes a parameter for the pin operation that will be performed on the peripheral.
The input/output interface might not just contain read and write functions.
There could be times when the pin mode or direction need to be changed during
program execution. During such a circumstance, a developer may decide that having
ChannelModeSet and ChannelDirectionSet functions as part of the interface would be
appropriate.
The next major functions that should be included in the HAL are generic register-
access functions. These functions are designed to handle “extra” peripheral features
that are NOT common in all microcontrollers. The RegisterWrite and RegisterRead
functions are meant to allow a developer to access the peripheral functions and then
extend the HAL into the board support package (BSP) or the application code. By
extending the HAL in this manner, a HAL can remain constant no matter what special
features a microcontroller feature may have.
Keep any module interface to a dozen or fewer functions. The more functions there are, the
more difficult it can be for developers to remember and even find the function call they are
looking for.
Finally, a developer needs to consider functionality that may need to be set at the
application layer but that is hidden behind the veil of the HAL. An example might
be to have generic interrupt service routines that are defined in the driver but whose
functionality is determined during runtime or at compile time. Once a driver is
developed, we don’t want to have to change the code from one application to the next.
Instead, we prefer to use a callback function.
171
Chapter 7 HAL Design for GPIO
template must contain a few simple components, fully defined interface stubs, and
documentation.
The interface stubs are the declarations contained in a header file and the definitions
for the interface found in the source file. One recommendation for the template file
is to use the word TYPE where a developer would normally put the C language type.
The reason for doing this is that a team may be working with an 8-bit, 16-bit, or 32-
bit microcontroller whose registers will vary in size based on the architecture. When
the template is used to create real code, the template is copied and then each TYPE is
updated to the appropriate architecture bus width.
Each interface designed into the HAL should be documented. In an earlier chapter,
we examined how Doxygen can be used to document a header and source file along with
how to document functions and declarations. These skills will be essential to properly
documenting the HAL. In fact, the example templates that were developed earlier will be
directly applied to create the HAL template.
The template is designed to contain common interfaces and documentation but
can also contain common code! For example, earlier we examined how to create
configuration tables, and since the HAL is designed for common peripheral features, a
configuration template file can also be created that contains the default configuration
for any microcontroller. We can add any other code, such as the ability to read and write
GPIO pins, that will not change with the architecture. The ability to leverage code in this
manner can be very powerful and allows a developer to create drivers based on the HAL
template in a few hours rather than days or weeks.
During the template-development stage, a team should also examine each interface
and document all the pre-conditions and post-conditions that are expected for the
interface. For example, calling the Dio_Init function on an ARM-based microcontroller
before enabling the GPIO clock will result in a failed initialization. Somewhere within the
interface template the documentation needs to state that a pre-condition for executing
the Dio_Init HAL is that the GPIO peripheral clock has been enabled. A simple problem
could occur if the configuration table has not been fully populated. For that reason,
another pre-condition would be that the configuration table has a size greater than zero.
The idea of defining pre-conditions and post-conditions is not new to us, since we
have already discussed the concept for design-by-contract. In this case, a developer
uses Doxygen to document the contract between any user for the interface and what
the interface will do for the caller. The assert macro can even be used in the template
to ensure that the pre-conditions and post-conditions are adhered to in any subsequent
software.
173
Chapter 7 HAL Design for GPIO
For the GPIO HAL, Figure 7-2 shows an overview of the interface and how it
is organized into different files. The HAL contains header and source modules for
configuration data that is used to initialize the peripheral on startup and then header
and source modules that contain the behavior functions for the HAL.
So far, we have discussed every aspect required to develop our template and
application stubs. Let’s now examine the documentation for each interface in the GPIO
HAL. Listings 7-1 to 7-4 provide the documentation for each HAL GPIO interface. The
documentation is detailed and fully self-explanatory, so I leave it up to the reader to
examine each figure before catching back up with me in Step #5.
/**********************************************************************
* Includes
**********************************************************************/
/**********************************************************************
* Preprocessor Constants
**********************************************************************/
/**
174
Chapter 7 HAL Design for GPIO
/**
* Defines the number of ports on the processor.
*/
#define NUMBER_OF_PORTS 8U
/**********************************************************************
* Typedefs
**********************************************************************/
/**
* Defines the possible states for a digital output pin.
*/
typedef enum
{
DIO_LOW, /** Defines digital state
ground */
DIO_HIGH, /** Defines digital state
power */
DIO_PIN_STATE_MAX /** Defines the maximum
digital state */
}DioPinState_t;
/**
* Defines an enumerated list of all the channels (pins) on the MCU
* device. The last element is used to specify the maximum number of
* enumerated labels.
*/
typedef enum
{
/* TODO: Populate this list based on available MCU pins */
FCPU_HB, /**< PORT1_0 */
PORT1_1, /**< PORT1_1 */
175
Chapter 7 HAL Design for GPIO
/**
* Defines the possible DIO pin multiplexing values. The datasheet
* should be reviewed for proper muxing options.
*/
typedef enum
{
/* TODO: Populate with possible mode options */
DIO_MAX_MODE
}DioMode_t;
/**
* Defines the possible states of the channel pull-ups
*/
typedef enum
{
DIO_PULLUP_DISABLED, /*< Used to disable the internal pull-ups */
DIO_PULLUP_ENABLED, /*< Used to enable the internal pull-ups */
DIO_MAX_RESISTOR /*< Resistor states should be below this value
*/
}DioResistor_t;
/**
* Defines the digital input/output configuration table’s elements that are
used
* by Dio_Init to configure the Dio peripheral.
*/
176
Chapter 7 HAL Design for GPIO
typedef struct
{
/* TODO: Add additional members for the MCU peripheral */
DioChannel_t Channel; /**< The I/O pin */
DioResistor_t Resistor; /**< ENABLED or DISABLED */
DioDirection_t Direction; /**< OUTPUT or INPUT */
DioPinState_t Data; /**<HIGH or LOW */
DioMode_t Function; /**< Mux Function - Dio_Peri_Select*/
}DioConfig_t;
/**
* Defines the slew rate settings available
*/
typedef enum
{
FAST, /**< Fast slew rate is configured on the corresponding pin, */
SLOW /**< Slow slew rate is configured on the corresponding pin, */
}DioSlew_t;
/**********************************************************************
* Function Prototypes
**********************************************************************/
#ifdef __cplusplus
extern "C"{
#endif
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_H_*/
/***End of File****************************************************/
177
Chapter 7 HAL Design for GPIO
/**********************************************************************
* Module Preprocessor Constants
**********************************************************************/
/**********************************************************************
* Module Preprocessor Macros
**********************************************************************/
/**********************************************************************
* Module Typedefs
**********************************************************************/
/*********************************************************************
* Module Variable Definitions
**********************************************************************/
/**
* The following array contains the configuration data for each
* digital input/output peripheral channel (pin). Each row represents a *
single pin. Each column is representing a member of the DioConfig_t
* structure. This table is read in by Dio_Init, where each channel is then
* set up based on this table.
*/
178
Chapter 7 HAL Design for GPIO
/**********************************************************************
* Function Prototypes
**********************************************************************/
/**********************************************************************
* Function Definitions
**********************************************************************/
/**********************************************************************
* Function : Dio_Init()
*//**
* \b Description:
*
* This function is used to initialize the Dio based on the configuration
* table defined in dio_cfg module.
*
* PRE-CONDITION: Configuration table needs to populated (sizeof > 0)
*
* POST-CONDITION: A constant pointer to the first member of the
* configuration table will be returned.
*
179
Chapter 7 HAL Design for GPIO
180
Chapter 7 HAL Design for GPIO
#ifndef DIO_H_
#define DIO_H_
/**********************************************************************
* Includes
**********************************************************************/
#include <stdint.h> /* For standard type definitions */
#include "dio_cfg.h" /* For dio configuration */
#include "constants.h" /* For HIGH, LOW, etc */
/**********************************************************************
* Preprocessor Constants
**********************************************************************/
/**********************************************************************
* Configuration Constants
**********************************************************************/
/**********************************************************************
* Macros
**********************************************************************/
/**********************************************************************
* Typedefs
**********************************************************************/
/**********************************************************************
* Variables
**********************************************************************/
/**********************************************************************
* Function Prototypes
**********************************************************************/
#ifdef __cplusplus
extern "C"{
#endif
181
Chapter 7 HAL Design for GPIO
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_H_*/
/*********************************************************************
* Module Preprocessor Macros
**********************************************************************/
182
Chapter 7 HAL Design for GPIO
/**********************************************************************
* Module Typedefs
**********************************************************************/
/**********************************************************************
* Module Variable Definitions
**********************************************************************/
/**
* Defines a table of pointers to the peripheral input register on the
* microcontroller.
*/
static TYPE volatile * const DataIn[NUM_PORTS] =
{
(TYPE*)®ISTER1, (TYPE*)®ISTER2,
};
/**
* Defines a table of pointers to the peripheral data direction register
on
* the microcontroller.
*/
static TYPE volatile * const DataDirectin[NUM_PORTS] =
{
(TYPE*)®ISTER1, (TYPE*)®ISTER2,
};
/**
* Defines a table of pointers to the peripheral latch register on the
* microcontroller
*/
static TYPE volatile * const DataOut[NUM_PORTS] =
{
(TYPE*)®ISTER1, (TYPE*)®ISTER2,
};
/**
183
Chapter 7 HAL Design for GPIO
/**
* Defines a table of pointers to the port’s function select register
* on the microcontroller
*/
static TYPE volatile * const Function[NUM_PORTS] =
{
(TYPE*)®ISTER1, (TYPE*)®ISTER2,
};
/**********************************************************************
* Function Prototypes
**********************************************************************/
/**********************************************************************
* Function Definitions
**********************************************************************/
/*********************************************************************
* Function : Dio_Init()
*//**
* \b Description:
*
* This function is used to initialize the Dio based on the configuration
* table defined in dio_cfg module.
*
* PRE-CONDITION: Configuration table needs to populated (sizeof > 0) <br>
* PRE-CONDITION: NUMBER_OF_CHANNELS_PER_PORT > 0 <br>
* PRE-CONDITION: NUMBER_OF_PORTS > 0 <br>
* PRE-CONDITION: The MCU clocks must be configured and enabled.
*
184
Chapter 7 HAL Design for GPIO
/**********************************************************************
* Function : Dio_ChannelRead()
*//**
* \b Description:
*
185
Chapter 7 HAL Design for GPIO
/**********************************************************************
* Function : Dio_ChannelWrite()
*//**
186
Chapter 7 HAL Design for GPIO
* \b Description:
*
* This function is used to write the state of a channel (pin) as either
* logic high or low through the use of the DioChannel_t enum to select
* the channel and the DioPinState_t to define the desired state.
*
* PRE-CONDITION: The channel is configured as OUTPUT <br>
* PRE-CONDITION: The channel is configured as GPIO <br>
* PRE-CONDITION: The channel is within the maximum DioChannel_t definition
*
* POST-CONDITION: The channel state will be State
*
* @param Channel is the pin to write using the DioChannel_t
* enum definition
* @param State is HIGH or LOW as defined in the
* DioPinState_t enum
*
* @return void
*
* \b Example:
* @code
* Dio_WriteChannel(PORT1_0, LOW); // Set the PORT1_0 pin low
* Dio_WriteChannel(PORT1_0, HIGH); // Set the PORT1_0 pin high
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
187
Chapter 7 HAL Design for GPIO
**********************************************************************/
void Dio_ChannelWrite(DioChannel_t Channel, DioPinState_t State)
{
/**************************************************************************
* Function : Dio_ChannelToggle()
*//**
* \b Description:
*
* This function is used to toggle the current state of a channel (pin).
*
* PRE-CONDITION: The channel is configured as OUTPUT <br>
* PRE-CONDITION: The channel is configured as GPIO <br>
* PRE-CONDITION: The channel is within the maximum DioChannel_t definition
*
* POST-CONDITION:
*
* @param Channel is the pin from the DioChannel_t that is
* to be modified.
*
* @return void
*
* \b Example:
* @code
* Dio_ChannelToggle(PORTA_1);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
188
Chapter 7 HAL Design for GPIO
*
* <br><b> - HISTORY OF CHANGES - </b>
*
**********************************************************************/
void Dio_ChannelToggle(DioChannel_t Channel)
{
/**************************************************************************
* Function : Dio_RegisterWrite()
*//**
* \b Description:
*
* This function is used to directly address and modify a Dio register.
* The function should be used to access specialied functionality in the
* Dio peripheral that is not exposed by any other function of the
* interface.
*
* PRE-CONDITION: Address is within the boundaries of the Dio register
* addresss space
*
* POST-CONDITION: The register located at Address with be updated
* with Value
*
* @param Address is a register address within the Dio
* peripheral map
* @param Value is the value to set the Dio register to
*
* @return void
*
* \b Example:
* @code
* Dio_RegisterWrite(0x1000, 0x15);
* @endcode
*
189
Chapter 7 HAL Design for GPIO
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
**********************************************************************/
void Dio_RegisterWrite(uint32_t Address, TYPE Value)
{
/**********************************************************************
* Function : Dio_RegisterRead()
*//**
* \b Description:
*
* This function is used to directly address a Dio register. The function
* should be used to access specialied functionality in the Dio peripheral
* that is not exposed by any other function of the interface.
*
* PRE-CONDITION: Address is within the boundaries of the Dio register
* addresss space
*
* POST-CONDITION: The value stored in the register is returned to the
* caller
*
* @param Address is the address of the Dio register to read
*
* @return The current value of the Dio register.
*
* \b Example:
* @code
190
Chapter 7 HAL Design for GPIO
* DioValue = Dio_RegisterRead(0x1000);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
*
**********************************************************************/
TYPE Dio_RegisterRead(uint32_t Address)
{
/**********************************************************************
* Function : Dio_CallbackRegister()
*//**
* \b Description:
*
* This function is used to set the callback functions of the dio driver. By
* default, the callbacks are initialized to a NULL pointer. The driver may
* contain more than one possible callback, so the function will take a
* parameter to configure the specified callback.
*
* PRE-CONDITION: The DioCallback_t has been populated
* PRE-CONDITION: The callback function exists within memory.
*
* POST-CONDITION: The specified callback function will be registered
* with the driver.
*
191
Chapter 7 HAL Design for GPIO
192
Chapter 7 HAL Design for GPIO
In the examples that follow, I’ve stripped out the function documentation and focused
just on the executable code.
Let’s start by examining the pointer arrays. Listing 7-5 shows how the GPIO registers
can be organized into similar groupings and mapped to memory. A pointer array is
created for each register type within the GPIO peripherals. A pointer to the register is
then added to the array, which will later allow the initialization and application code to
simply loop through the array to access the register.
Listing 7-5. Pointer Array Memory Map Example for Kinetis-L KL25Z
/**
* Defines a table of pointers to the Port Data Input Register
*/
uint32 volatile * const portsin[NUM_PORTS] =
{
(uint32*)&GPIOA_PDIR, (uint32*)&GPIOB_PDIR,
};
/**
* Defines a table of pointers to the port’s data-direction register
*/
uint32 volatile * const portsddr[NUM_PORTS] =
{
(uint32*)&GPIOA_PDDR, (uint32*)&GPIOB_PDDR
};
/**
* Defines a table of pointers to the Port Data Output Register
*/
uint32 volatile * const ports[NUM_PORTS] =
{
(uint32*)&GPIOA_PDOR, (uint32*)&GPIOB_PDOR,
};
193
Chapter 7 HAL Design for GPIO
/**
* Defines a table of pointers to the Port Data Toggle Register
*/
uint32 volatile * const ptoggle[NUM_PORTS] =
{
(uint32*)&GPIOA_PTOR, (uint32*)&GPIOB_PTOR
};
/**
* Defines a table of pointers to the Pin Control Registers
*/
uint32 volatile * const pinctl[NUM_PORTS] =
{
(uint32*)&PORTA_PCR0, (uint32*)&PORTB_PCR0
};
Let’s start examining the Dio_Init code, which can be found in Listing 7-6. The
initialization is straightforward. A pointer to the configuration table is passed into the
interface, and a for loop is used to read each element one row at a time. Based on the
information stored in the configuration register, the appropriate register is accessed
through the pointer array and the correct bits within the register are set based on the
configuration.
// Loop through all pins, set the data register bit and the data-direction
// register bit according to the dio configuration table values
for (i = 0; i < NUM_DIGITAL_PINS; i++)
{
number = Config[i].Channel / NUM_PINS_PER_PORT;
position = Config[i].Channel % NUM_PINS_PER_PORT;
194
Chapter 7 HAL Design for GPIO
Once the initialization code is created, the remaining HAL functions are relatively
simple to implement. They simply access the pointer array and either set or retrieve
register data. For example, the Dio_ChannelRead code, which can be seen in Listing 7-7,
reads in the state for the input register, shifts the data, and determines whether the bit is
set high or low.
195
Chapter 7 HAL Design for GPIO
/* Mask the port state with the pin and return the DioPinState */
return (( PortState & PinMask) ? DIO_HIGH : DIO_LOW);
}
196
Chapter 7 HAL Design for GPIO
In earlier chapters, we discussed the need to extend the HAL interface. The extension
for the interface is to handle custom peripheral behaviors that are not common to every
peripheral on every processor. In these applications, the ability to write to and read from
a generic register is very useful. The great part about implementing generic register read
and write functions is that once written they can be used repeatedly with only minor
modifications needed. The recommendation is that good programming practices are
followed by verifying the address and data that you are trying to access. Listings 7-10 and
7-11 show an example of what these functions might look like, excluding the defensive
checks.
*RegisterPointer = Value;
}
return *RegisterPointer;
}
197
Chapter 7 HAL Design for GPIO
198
Chapter 7 HAL Design for GPIO
move on to the next peripheral and begin designing the next HAL. In the next chapter,
we will examine the SPI peripheral and how we can design a basic HAL for it using the
techniques that we have been discussing in this book.
G
oing Further
The GPIO peripheral is a foundational module that developers need to take the utmost
care when developing to ensure that their software scales. The following are some ideas
on how a developer can take the concepts discussed in this chapter and immediately
apply them to their own development cycle.
• Review the table and identify the features that belong in a standard
HAL interface. Create an initial HAL interface list and identify the
input and output features for the interfaces.
• Identify the development board that the first port will be performed
on. Use the examples in this chapter to fill in the implementation
for the target. If the reader is interested in a working example that
can be used for educational purposes, examples for the NXP KL25Z
development board are available on www.beningo.com under Insights
➤ Toolkits.
199
Chapter 7 HAL Design for GPIO
• Develop basic test cases based on the configuration table and HAL
input and output features. Verify that the ported code behaves as
expected.
200
CHAPTER 8
Every slave device that communicates with the master, typically the microcontroller,
has a slave select line that asserts which slave device is being communicated with.
The SPI bus can support as many slave devices as there are GPIO pins available to
communicate with them. The fact that a slave select pin is required for every device is
one disadvantage to using the SPI peripheral.
There are many advantages though. First, SPI is a very simple serial interface.
For every clock pulse, a master output bit and a slave output bit are clocked out
simultaneously on the bus. This behavior makes it so that bi-directional communication
can occur very quickly. Second, the SPI bus typically can communicate at 1 Mbps to
16 Mbps, which makes it an extremely fast communication channel. There are many
other advantages to using the SPI bus, but the last one that I will mention is that the SPI
peripheral is very easy to set up and use.
201
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_8
Chapter 8 HAL Design for SPI
MOSI
MISO
CLK Device 1
SS1
Microcontroller Device 2
SS2
Device 3
SS3
202
Chapter 8 HAL Design for SPI
Master/Slave X X X X
Tx/Rx X X X X
Wait mode X X
Bi-directional X X
High-speed X X
dual output
MSB/LSB X X X X
DMA X X
CRC X
The table can be used by developers to quickly determine the common and
uncommon features in the peripheral that later either will be placed into the HAL or will
require a HAL extension. When the reader walks through their own SPI peripherals, they
may find that they have significantly more features available than I’ve listed. The goal
here is to provide an example and leave some work to the reader.
1
XP KL25Z Sub-Family Reference Manual
N
2
ST Microelectronics STM32F427xx Datasheet
3
Microchip PIC24FJ128GA010 Family Datasheet
4
Microchip PIC18F2455 Datasheet
203
Chapter 8 HAL Design for SPI
• Initialization
• Callbacks
You will notice a similarity between these interface needs and the GPIO. The only
difference is that instead of an Input/Output feature there is a Data Transmit and
Receive, which could still be considered Input/Output. Most peripherals will have a very
similar outline for their interface.
A developer will want to decide what the major inputs and outputs required
to configure and run the SPI bus are and decide on the operations that need to be
performed on the bus. The operations go in the interface, and the inputs and outputs will
be used by the operations in some way. For example, a configuration table that is used to
initialize the SPI peripheral will contain all the data required to set up the peripheral and
will be passed into the Spi_Init function.
The resulting interface for the SPI HAL would look something along the lines of
Figure 8-2. Notice how the interface follows a very similar pattern to the GPIO HAL and
that it is easily readable and extendable.
204
Chapter 8 HAL Design for SPI
Design patterns are a solution to a common problem that exists in software engineering. There
are many different design patterns, such as using a circular buffer for receiving UART data.
As you develop your own interfaces, drivers, and application code, keep an eye open for
repeating patterns. These patterns should be captured and saved into a template so that they
can be reapplied to future applications.
For example, transmitting and receiving data on the SPI bus will use a design pattern for how
the HAL is designed, but a design pattern can also be used also behind the scenes in the
implementation,. Design patterns save time by avoiding your having to reinvent the wheel.
Instead, a better wheel can be made.
205
Chapter 8 HAL Design for SPI
The SPI HAL will require several files in order to contain all the operations necessary
to communicate with an external device on the SPI bus. The modules that are necessary
can be found in Figure 8-3.
Once each file has been created, the generic Doxygen template can be used to fill
in the modules. A quick pass through to update for SPI would then be necessary. There
are several functions that will need to be added to the modules. In order to save the
reader time and effort, Listings 8-1 and 8-2 show an example of what is needed. Don’t
forget that each function should have its inputs and outputs documented as well as
provide a detailed example of how to use the interface. It also wouldn’t hurt to set up the
assertions at this point to validate the preconditions and post-conditions.
/*************************************************************
* Function : Spi_Init()
*//**
* \b Description:
*
* This function is used to initialize the Spi based on the configuration
table
* defined in spi_cfg module.
*
* PRE-CONDITION: Configuration table needs to populated (sizeof > 0)
* PRE-CONDITION: The MCU clocks must be configured and enabled.
*
* POST-CONDITION: The peripheral is set up with the configuration
*
206
Chapter 8 HAL Design for SPI
/**********************************************************************
* Function : Spi_Transfer()
*//**
* \b Description:
*
* This function is used to initialize a data transfer on the SPI bus.
*
207
Chapter 8 HAL Design for SPI
In order to save the reader time and also muscle fatigue from having to carry
around a giant and heavy book, the templates for the helper functions and the common
RegisterRead, RegisterWrite, and callback functions have been left out. They are
208
Chapter 8 HAL Design for SPI
included in the example templates that go with this book. If needed, refer to Chapter 7 on
GPIO Hals and review how these function stubs are set up. The only difference between
the SPI and DIO setups is that the functions are preceded with Spi instead of Dio.
/**
* Defines a pointer table to the spi control 0 registers.
*/
uint8_t volatile * const spicon1[NUM_SPI_CHANNELS] =
{
(uint8_t*)&SPI0_C1, (uint8_t*)&SPI1_C1
};
209
Chapter 8 HAL Design for SPI
/**
* Defines a pointer table to the spi control 1 registers.
*/
uint8_t volatile * const spicon2[NUM_SPI_CHANNELS] =
{
(uint8_t*)&SPI0_C2, (uint8_t*)&SPI1_C2
};
/**
* Defines a pointer table to the spi status registers.
*/
uint8_t volatile * const spistat[NUM_SPI_CHANNELS] =
{
(uint8_t*)&SPI0_S, (uint8_t*)&SPI1_S
};
/**
* Defines a pointer table to the spi bit-rate control registers.
*/
uint8_t volatile * const spibr[NUM_SPI_CHANNELS] =
{
(uint8_t*)&SPI0_BR, (uint8_t*)&SPI1_BR
};
Just like before, setting up these pointers is a great way to access memory and set up
initialization functions that easily loop through a configuration table and then set the bit
values in the registers. An example initialization function can be found in Listing 8-4.
210
Chapter 8 HAL Design for SPI
{
// Enable clock gate for spi channel
*spigate |= spipins[Index];
211
Chapter 8 HAL Design for SPI
else
{
*spicon2[Index] &= ~(REGBIT0 + REGBIT3);
}
In order to save space, the configuration structure is not shown, but from reviewing
the initialization function, you can easily see the information that is being stored there.
The SPI bus is a unique communication interface in that it receives data while it
transmits data. This makes the SPI bus very efficient. We can use the transmit buffer to
store the receive data, which limits how much RAM we need to allocate to communicate
with slave devices.
Creating a robust Spi_Transfer function isn’t trivial or something that should be
attempted without first thinking through the design and process. The SPI bus, while
simple, does require that certain steps be followed in order to successfully handle all the
possible cases. Figure 8-4 shows the steps the driver must go through to transfer data.
In many cases, each step can be placed into a separate helper function to keep the code
readable and maintainable. The function overhead will slightly affect the performance
unless the functions are in-lined.
212
Chapter 8 HAL Design for SPI
The flow chart looks simple, but there is an important consideration that developers
need to look at that I often see overlooked. If something goes wrong, the driver needs
to be able to detect that the communication timed out. Most drivers I review assume
that everything will always work as expected and end up hanging up because a device
at some point fails to respond or something happens that prevents the “transmission
complete” flag from being set. Make sure that you think through the potential failure
points and how the higher-level application will be notified that a device is not
responding.
The Spi_Transfer implementation can be found in Listing 8-5.
// Setup the spi registers with the spi device settings
Spi_Setup(Config);
213
Chapter 8 HAL Design for SPI
/***************************************************************
* Transmit (and receive) the data one byte at a time.
***************************************************************/
for(i = 0; i < Config->NumBytes; i++)
{
/***************************************************************
* Check the shift direction. If it is LSBit first, reverse the order
* in which we transmit each byte (last byte first) as well.
***************************************************************/
if (Config->Direction == LSB_FIRST)
{
j = Config->NumBytes - i - 1;
}
else
{
j=i;
}
Mcu_TimeoutStart(INTERVAL_10MS);
break;
}
}
Mcu_TimeoutStart(INTERVAL_10MS);
214
Chapter 8 HAL Design for SPI
break;
}
}
*(Config->TxRxData + j) = *spibuf[Config->SpiChannel];
/***************************************************************
* Latch the data into the slave by de-selecting the chip select.
***************************************************************/
// In some cases the chip select will de-select the device
// before the last bit is transmitted. This is due to the flag
// options of this peripheral. In order to transmit properly, a
// slight delay is included before deselection.
for(x = 0; x < TransferDelay[Config->SpiChannel]; x++);
Spi_ClearCs(Config);
}
215
Chapter 8 HAL Design for SPI
Take the time to implement a test harness at this early development stage and reap
the rewards for the entire development cycle.
G
oing Further
The SPI peripheral is a foundational module that developers need to take the utmost
care developing to ensure that their software scales. The following are some ideas on
how a developer can take the concepts discussed in this chapter and immediately apply
them to their own development cycle.
• Review the table and identify the features that belong in a standard
HAL interface. Create an initial HAL interface list and identify the
input and output features for the interfaces
216
Chapter 8 HAL Design for SPI
• Identify the development board that the first port will be performed
on. Use the examples in this chapter to fill in the implementation
for the target. If the reader is interested in a working example that
can be used for educational purposes, examples for the NXP KL25Z
development board are available on www.beningo.com.
• Develop basic test cases based on the configuration table and HAL
input and output features. Verify that the ported code behaves as
expected.
• Create automated test cases that can be executed daily to verify that
the HAL is working as expected. Don’t forget to inject errors to verify
that the regression tests are correct.
217
CHAPTER 9
• Internal flash
• Internal EEPROM
• External EEPROM
• Externa flash
Using internal flash and EEPROM devices can be useful when you want to limit
external devices, product size, complexity, and cost. There can be several potential
issues with using internal memory storage, however. First, internal flash and EEPROM
devices tend to be more complex to set up and use than external devices. Developers
must grapple with setting internal clocks perfectly to ensure that the internal memory
devices are not damaged. Second, application code is stored either in or near the
219
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_9
Chapter 9 HAL Design for EEPROM and Memory Devices
• How much drift will there be in the clock at various voltages and
temperature ranges? Is it enough to cause an erase or write cycle
to fail?
The worst-case analysis on what can go wrong always seems to bring up more
potential issues than if you were using an external device. This doesn’t mean that
developers should avoid internal memory but simply that they need to be careful with
how they implement it.
In this section, we are going to develop a hardware abstraction layer that can be used
to govern both internal and external memory devices, with our primary focus being on
external EEPROM devices. The nice thing about the HAL is that it abstracts out these
devices so that the underlying details are completely hidden. The device could be an
internal or external device, on a SPI or I2C bus, or even be for different memories, such
as EEPROM, Flash, or some other architecture. A properly designed HAL doesn’t care
about the underlying implementation or architecture, which means that if we do our job
right, we can design an interface for memory devices once and use it for any memory
device in any project indefinitely.
Just as we did in the last several chapters, we will continue to follow our simple
seven-step process to design our memory HAL. In this case, we are going to focus
primarily on external EEPROM devices, but the HAL can easily be used with any
memory device on nearly any interface, as we mentioned before.
220
Chapter 9 HAL Design for EEPROM and Memory Devices
1
icrochip 25AA160D, 16 kb EEPROM, https://www.digikey.com/product-detail/en/
M
microchip-technology/25AA160D-I-ST/25AA160D-I-ST-ND/2125495
2
Microchip 25AA1024, https://www.digikey.com/product-detail/en/
microchip-technology/25AA1024T-I-SM/25AA1024T-I-SMTR-ND/1228443
3
Rohm BR25L640-W, 64 kb EEPROM
4
STMicroelectronics M95512-DR, 512 kB EEPROM, https://www.digikey.com/product-detail/
en/stmicroelectronics/M95512-DRDW3TP-K/497-14457-1-ND/4729165
5
ONSemi CAT25128, 128 kb EEPROM, https://www.digikey.com/product-detail/en/
on-semiconductor/CAT25128VI-GT3/CAT25128VI-GT3OSTR-ND/2063309
221
Chapter 9 HAL Design for EEPROM and Memory Devices
These devices provide a basic sampling that we can use to develop our HAL, but
nearly any device could be selected.
I was creating a driver for another external memory device when I started to get the feeling
that I had written the exact same code before. The memory device that I was working with
was completely different from the one we had used on the last project, yet I kept getting a
feeling of déjà vu.
Finally, I couldn’t take it any longer and went back to review the code from the previous
project. Sure enough, despite being a completely different memory device, the basic
commands were identical! Further investigation revealed that there was a standard that the
devices were following.
The moral of the story is that we need to always be on the lookout for repeating patterns in the
work that we do and leverage anything that already exists that we can. After this realization, I
created a reusable interface that I still use to this day.
222
Chapter 9 HAL Design for EEPROM and Memory Devices
Write Enable X X X X
Write Disable X X X X
Write X X X X
Read X X X X
Read Status X X X X
Write Status X X X X
Page Erase X
Sector Erase X
Power Down X
Read ID X
Write ID X
Lock Status X
The JEDEC standard can be easily seen in Table 9-1. These are the features that are
supported by every device, such as the Write Enable and Disable features. Just like with
a microcontroller peripheral, many memory manufacturers will include the JEDEC
standard features but also attempt to differentiate themselves by adding additional
features that developers might find useful. For example, the Microchip 25AA1024
includes a Page Erase feature, which would typically be present in a flash controller
rather than an EEPROM controller. The feature gives developers an easy method for
quickly erasing large amounts of data. Such a feature could be very useful but also very
dangerous if not properly used and protected in source code.
223
Chapter 9 HAL Design for EEPROM and Memory Devices
• Initialization
• Writing data
• Reading data
Creating a HAL for EEPROM is just like any other peripheral except in this example
we are not going to include a callback function. A callback might exist if the EEPROM or
memory device is internal to the microcontroller. In this example though, the EEPROM
device is external to the microcontroller, which does not have any way to trigger an
internal interrupt on the microcontroller. For this reason, a callback is not included.
If a developer wanted to create an all-encompassing HAL that covered both internal
and external devices, they could include the callback and then just populate the code
depending on the circumstances.
An interface example can be seen in Figure 9-1. Notice that this HAL still follows
the standard pattern we have seen with microcontroller peripherals. There is still an
initialization function, a read/write function, and then register-access functions. The
primary difference here is that we have added an additional WriteStateSet function
that is used to control the write state of the memory. This easily could have been pulled
into the RegisterWrite capability, but in this example we want to explicitly create it in
the interface so that application users see that there may be extra steps necessary to work
with the memory device. If that detail were abstracted into the general RegisterWrite
capability, it might be easily overlooked. How a developer chooses to handle these types of
issues is dependent on their needs and preferences. There is not a right or wrong answer.
224
Chapter 9 HAL Design for EEPROM and Memory Devices
The HAL for the memory interface doesn’t look too bad. It could be much worse. The
first HAL version I created originally had more than a dozen different interfaces! I had
created the following:
• StatusRegisterWriteEnable
• StatusRegisterWriteDisable
• DataWriteEnable
• DataWriteDisable
Then, I had even extended the interface in the original HAL to include custom
features, such as the following:
• EraseChip
• EraseSector
• ErasePage
• PowerDown
• ReadID
The result was a HAL that had more than a dozen functions and was very difficult
to navigate and understand. In time, as I realized that the interface was too large, I
refactored the HAL so that it represented a much smaller and more manageable function
set. Everything related to custom features is now extended into a separate module that
is specific to the device, including all the erase functionality, identification, and energy-
savings modes. The main HAL was also refactored into the final version, shown in
Figure 9-1.
225
Chapter 9 HAL Design for EEPROM and Memory Devices
The HAL does include some custom datatypes. The primary HAL includes an
EepromWriteState_t. This allowed the original WriteEnable and WriteDisable
functions to be refactored from two separate functions to a single function that is
controlled by its parameters. The control is created by declaring a typedef enum with the
possible states, as shown in Figure 9-2.
Developers also will need to consider the different registers that can be accessed
through the interface and will make up the EepromRegister_t. In general, this won’t be
done until the coding stage simply because the registers available will vary from one part
to the next. Just for fun though, we will get ahead of ourselves and show an example of
what the EepromRegister_t might look like in Figure 9-3.
At this point, the base HAL is in place and we are ready to start building the
documentation and software stubs.
226
Chapter 9 HAL Design for EEPROM and Memory Devices
• Eeprom_Init
• Eeprom_Write
• Eeprom_Read
227
Chapter 9 HAL Design for EEPROM and Memory Devices
/**********************************************************************
* Function : Eeprom_Init()
*//**
* \b Description:
*
* This function is used to initialize the eeprom. There are several
* operations that this function performs. First, it configures the
* communication channel that is used to interface with the EEPROM.
* Second, it enables write protection and disables the HOLD hardware
* feature.
*
* PRE-CONDITION: Dio driver initialized
* PRE-CONDITION: Communication driver initialized
*
* POST-CONDITION: The EEPROM device is initialized and write
* protected.
*
* @param Config is a pointer to a CommBus_t that contains the
* communication bus configuration information for interfacing to the
* EEPROM.
*
* @return void
*
* \b Example:
* @code
* const DioConfig_t *DioConfig = Dio_ConfigGet();
* const SpiConfig_t *SpiConfig = Spi_ConfigGet();
* const EepromConfig_t *EepromConfig = Eeprom_ConfigGet();
*
* Dio_Init(DioConfig);
* Spi_Init(SpiConfig);
* Eeprom_Init(EepromConfig);
* @endcode
*
228
Chapter 9 HAL Design for EEPROM and Memory Devices
* @see Eeprom_ConfigGet
* @see Eeprom_Init
* @see Eeprom_Read
* @see Eeprom_Write
* @see Eeprom_RegisterWrite
* @see Eeprom_RegisterRead
**********************************************************************/
void Eeprom_Init(const EepromConfig_t * Config)
{
// Initialization code goes here!
}
/**********************************************************************
* Function : Eeprom_Read()
*//**
* \b Description:
*
* This function is used to initialize the eeprom. It currents enables write
* protection and disables the HOLD hardware feature.
*
* PRE-CONDITION: Dio driver initialized
* PRE-CONDITION: Spi driver initialized
* PRE-CONDITION: Eep_Init called
*
* POST-CONDITION: Size bytes are read from location Src into Dest.
*
* @param Dest - pointer to the location where data will be stored.
* @param Src - the starting address that is to be read
* @param Size - the number of bytes that are going to be read.
*
* @return void
*
* \b Example:
* @code
229
Chapter 9 HAL Design for EEPROM and Memory Devices
/**********************************************************************
* Function : Eeprom_Write()
*//**
* \b Description:
*
* This function is used to write data to the eeprom device. There is a
limit
* of being able to only write 256 bytes of data to the eeprom at a time!
*
* PRE-CONDITION: Dio driver initialized
* PRE-CONDITION: Spi driver initialized
* PRE-CONDITION: Eep_Init called
230
Chapter 9 HAL Design for EEPROM and Memory Devices
*
* POST-CONDITION: Size bytes are written from location Src into Dest.
*
* @param Dest - Address where the data will be stored in eeprom.
* @param Src - pointer to the data to be stored
* @param Size - the size of the data that is going to be written.
*
* @return void
*
* \b Example:
* @code
* const DioConfig_t *DioConfig = Dio_ConfigGet();
* const SpiConfig_t *SpiConfig = Spi_ConfigGet();
* const EepromConfig_t *EepromConfig = Eeprom_ConfigGet();
*
* Dio_Init(DioConfig);
* Spi_Init(SpiConfig);
* Eeprom_Init(EepromConfig);
* Eeprom_Write(0x0, Buffer, 8);
* @endcode
*
* @see Eeprom_ConfigGet
* @see Eeprom_Init
* @see Eeprom_Read
* @see Eeprom_Write
* @see Eeprom_RegisterWrite
* @see Eeprom_RegisterRead
**********************************************************************/
microcontroller, which means once we implement the base HAL we can literally reuse
the implementation without having to make any modifications. The only changes
that need to be made will be in the configuration files for the EEPROM setup or in the
extended HAL if we want to implement a non-standard feature.
This is exciting because we are finally at a point where we are writing code once
and reaping the benefits for every project thereafter. The other HALs certainly can be
reused, but if a team is moving from one microcontroller to the next, a little more work is
required, whereas with the external devices this code can be completely reused.
In this section, we are going to look through the implementation for the EEPROM
HAL, but we are only going to examine a minimum feature set. The EEPROM device will
also be an external SPI device. We will examine the following functions:
• Eeprom_Init
• Eeprom_Write
• Eeprom_Read
From these implementation details, readers should be able to create and fill in the
remaining HAL features on their own. Let’s start by examining the Eeprom_Init function
in Listing 9-4.
// Disable HOLD pin in hardware. We will not be using this function.
Dio_ChannelWrite(EEPROM_HOLD, HIGH);
232
Chapter 9 HAL Design for EEPROM and Memory Devices
// Bits 2 and 3 of the status register are the block write protection, so
// if (Value & 0x0C) is not zero, block write protection is enabled.
if((Value & 0x0C))
{
// Disable write protection
Eeprom_WriteProtection(EEPROM_WP_DISABLE);
Notice that the initialization function is simple and could be used with any standard
EEPROM device. The function starts by assigning the external EEPROM configuration
pointer to a local, module-defined variable. The hardware write protection is configured,
followed by the internal write protection. This initialization by default disables the write
protection, but a developer could create their own initialization that makes this feature
configuration defined. That would allow the default values to change based on the
application needs. (I leave that as an exercise for the reader to perform).
The next function that a developer would create is the Eeprom_Write function. An
example for this function can be seen in Listing 9-5.
233
Chapter 9 HAL Design for EEPROM and Memory Devices
{
EepromConfig.TxRxData[Index + 4] = Src[Index];
}
status = Eeprom_RegisterRead(EEPROM_READ_STATUS_REG);
// Poll the busy bit in status register
while(status & 0x01)
{
status = Eeprom_RegisterRead(EEPROM_READ_STATUS_REG);
}
The code shown in Listing 9-5 is basic example code that does not perform any safety
checks on the data size that is coming in or performing any checks to verify that the data
written was done so successfully. However, it does demonstrate how this code could
be used with any EEPROM device. In a production-intent implementation, a developer
would make sure that at least the following cases are considered and handled:
234
Chapter 9 HAL Design for EEPROM and Memory Devices
• Verify the written data by reading it back out and comparing it.
The write function starts out by defining the first four bytes in the data stream as
the command and the address that the data will be written to. Following this setup, the
data is copied into the transmit buffer. Once again, for production, there should be some
safety checks to make sure that the transmit buffer does not overflow. If the data cannot
fit within a single transaction then the code would need to set up multiple write actions.
To keep things simple, I’ve removed all these details.
With the transmit buffer set up, a developer updates the number of bytes to transmit
and then initiates the communication transfer. This example shows an explicit call to
the Spi_Transfer function, but a developer could implement this in such a way that
the transaction could occur on any bus. To do this, the function call would dereference
a function pointer to the desired transmit function. Before transmitting and writing the
data, the function also disables any write protection that might be enabled on the chip.
The write function will not be instantaneous. This driver uses a polled monitoring
technique to watch the status register for the “write complete” flag to be set. Once the
write has completed successfully, the write protection is enabled and the local variables
are reset to their default values.
The EEPROM read function turns out to be just as simple if not more so than the
write function. The read function can be found in Listing 9-6.
235
Chapter 9 HAL Design for EEPROM and Memory Devices
{
EepromConfig.TxRxData[Index] = 0xAA;
}
Just like with the write function, the read function starts by configuring the
command and the address that will be read from. Once this is done, the Spi_Transfer
function is called to perform the transaction. When all the data has been read into the
buffer, the function copies the received data into the desired destination. Copying the
data could cause a slight performance hit on the EEPROM functionality. A developer
could also create their function so that the data is placed directly into the destination
location rather than in an intermediary buffer or use a pointer to directly access the data.
Don’t forget that the read function is just an example! Production code should
include assertion and runtime checks to make sure that the buffers do not overflow and
that all error conditions and use cases are covered appropriately. It should also take into
account the efficiency, performance, and memory usage.
236
Chapter 9 HAL Design for EEPROM and Memory Devices
237
Chapter 9 HAL Design for EEPROM and Memory Devices
Going back to Table 9-1, there are several custom features that do not belong in the
primary HAL. These features include the following:
• Erase modes
• Low-power modes
While these are all useful features that a developer probably wants to implement,
they are not supported in every device. They are instead a manufacturer’s custom
implementation designed to differentiate their product from the competition.
A developer would add a separate module that would handle these customizations.
The module name could be anything, but the following are a few suggestions:
• hal_device_ext
• device_ext
• device_hal_ext
As the reader can see, my personal preference is to indicate in some way that the
HAL is an extension. Some HALs include the word hal in their naming conventions, but
I typically do not do this. My preference is to specify the device with the assumption that
the device module contains the HAL functions to control the device. If a developer were
working with the 25AA1024, they would end up with the following files:
• eeprom_25aaxxxx.h
• eeprom_25aaxxxx.c
• eeprom_25aaxxxx _ext.h
• eeprom_25aaxxxx _ext.c
• eeprom_25aaxxxx _cfg.h
• eeprom_25aaxxxx _cfg.c
Everything required to use a 25AA device would be included in these files. Notice
that in this example I am putting EEPROM in front of the part number. I do this because
without it a developer could easily get confused as to the purpose or function that part
number is associated with. They may find themselves wasting time trying to remember
which of these ten different part numbers was EEPROM.
238
Chapter 9 HAL Design for EEPROM and Memory Devices
The HAL extension functions will vary depending on the extra features that are
available on the device. For example, Figure 9-4 shows several new HAL functions that
are added to the EEPROM module through the _ext file.
The HAL extensions may require additional type definitions in order to constrain
and define the possible parameters that can be used to control the interface. Earlier, we
discussed how in my earliest HALs I had a separate function for every erase function on
the EEPROM device. Having multiple functions to control this behavior can complicate
the interface and make readability and maintainability worse. For that reason, a single
function that is then controlled by the parameter is preferred. Figure 9-5 shows an
example enumeration that would be used to control the erase functions on an EEPROM
chip. Keep in mind that this is specific to a single chip since most EEPROM devices do
not require a mass erase function.
239
Chapter 9 HAL Design for EEPROM and Memory Devices
G
oing Further
Developing a HAL for an external device such as an EEPROM device is no different
than creating a HAL for an internal device. The implementation will require accessing
a communication peripheral such as I2C or SPI, but the HAL design is the same. Now is
a great time to apply these techniques yourself. The following are some ideas of how a
developer can take the concepts discussed in this chapter and immediately apply them
their own development cycle.
• Review the table and identify the features that belong in a standard
HAL interface. Create an initial HAL interface list and identify the
input and output features for the interfaces.
• Identify the development board that the first port will be performed
on. Use the examples in this chapter to fill in the implementation for
the target.
240
Chapter 9 HAL Design for EEPROM and Memory Devices
• Develop basic test cases based on the configuration table and HAL
input and output features. Verify that the ported code behaves as
expected.
• Create automated test cases that can be executed daily to verify that
the HAL is working as expected. Don’t forget to inject errors to verify
that the regression tests are correct.
241
CHAPTER 10
243
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_10
Chapter 10 API Design for Embedded Applications
the SD card to get the desired result. APIs provide developers with several advantages,
such as the following:
• Creates a black box that performs the desired operation with little to
no knowledge of how it does it
• Speeds up development
Creating and using APIs for embedded software in today’s environment really is a
no-brainer. Developers should be creating APIs to produce more modular and reusable
code. The benefits have been proven time and time again. However, as developers go
about creating their APIs, there are several disadvantages that should be kept in mind.
These include the following:
• Each API level will have a minor performance hit when storing the
function return address on the stack unless the functions are in-lined
by the compiler.
SOFTWARE TERMINOLOGY
In most cases, the benefits far outweigh the disadvantages, and if developers are
aware of the disadvantages, they can mitigate any potential issues that might arise
from them.
244
Chapter 10 API Design for Embedded Applications
Designing APIs
Creating an API for an embedded application is not much different than the process
that we have been using throughout this book to create a HAL. The major differences
are that we are working at a higher abstraction level, removed from the hardware.
This makes life easier on the developer. We no longer need to compare datasheets for
multiple microcontroller devices and carefully craft an interface that supports them all.
The same process used to design a HAL can be used to make an API, with a few minor
modifications. The modified process for designing an API is as follows:
1) Identify the features and operations that the API will perform.
That’s it! The process is shortened by two steps since we don’t have to review a bunch
of datasheets. The nice part about the API level is that we implement once and only need
to maintain the interface. The APIs should be usable across platforms, and only HAL
dependencies would ever need to be updated.
Every best practice that we have discussed related to HALs in this book also applies
to the API level. For example, developers should try to keep their APIs manageable and
limited to no more than a dozen per component. A developer should break up and
organize their component so that it contains four different modules, as follows:
Keeping a component organized in this fashion will help maximize reuse and will
also help keep the APIs associated with it organized and easily navigable.
245
Chapter 10 API Design for Embedded Applications
A
pplication Frameworks
An application framework is a collection of different components, or set of APIs, that
are interrelated and assist a developer in rapidly developing an application. Application
frameworks have been around for PC developers for decades, but embedded-software
developers really haven’t had application frameworks available to them until recently.
The reason why is that embedded developers only focused on one-off applications and
had no reason to create reusable code and application frameworks to help them speed
up development.
Developers have started to move to 32-bit ARM-based microcontrollers. With this
transition, the hardware has become so complicated that microcontroller manufacturers
such as Microchip, Renesas, and ST Microelectronics have started to develop application
frameworks for their parts. Application frameworks help their customers speed up
development and abstract out the hardware. Developers therefore don’t need to become
experts on every register in the microcontroller and how each works. These frameworks
include not only a HAL but often high-level APIs to implement features such as SD card,
RTOS, command consoles, and much more. An example application framework from
Renesas can be found in Figure 10-1. Notice how it includes everything from the board
support package and HAL to several different application-level functions.
When you are thinking about creating your own APIs and collecting them into a
framework, take some time to review what has already been done in the industry. You
might find that you are able to use something that already exists or at least leverage the
best practices from other teams that have already made progress in developing useful
reusable firmware.
246
Chapter 10 API Design for Embedded Applications
1
https://www.renesas.com/en-us/products/synergy/features.html
247
Chapter 10 API Design for Embedded Applications
If the answer to most of these questions is “yes,” then the component should
probably be written to have a nice API so that it can be easily reused. The real question
in many developers’ minds might be what embedded-software components lend
themselves to being reused and are deserving of the time and attention required to
create a robust API around them?
For every team, the answer will be dependent upon their end application and
their core intellectual property. However, there are several general examples that are
necessary in almost every embedded application that we can use as a starting point to
provide an example for how a developer should design and create their own APIs.2 The
examples that I am about to provide are major components in an embedded system that
can be reused. Undoubtedly, you will find that there are many more smaller components.
2
http://www.webopedia.com/TERM/L/library.html
248
Chapter 10 API Design for Embedded Applications
earlier chapter, we discussed how a developer might need to create a wrapper layer in
their software so that an RTOS can easily be swapped in and out. The wrapper layer,
shown in Figure 10-2, provides a well-known interface through which to access the
features available in any RTOS and allows the RTOS to easily be swapped out.
Using a wrapper for the RTOS layer has immediate advantages, such as the following:
The only real downside to having a wrapper layer around the RTOS is that there is a
slight performance hit due to making a function call to get into the wrapper, which then
must call the associated RTOS function. This disadvantage can be overcome by function
in-lining and enabling compiler optimizations.
Application
RTOS Wrapper
Other Component Layers
RTOS
HAL
• ThreadX;
• FreeRTOS;
• Micrium OS2 or 3;
3
http://www.microchip.com/mplab/mplab-harmony
249
Chapter 10 API Design for Embedded Applications
to be swapped into and out of their software platform. The application code makes calls
to the same API calls, but the API is populated with the specific RTOS feature’s API call.
That is definitely something that a developer who is working on a reusable software
framework or trying to maximize firmware reuse should take into consideration.
Embedded-software developers love to get down into the bits and bytes and work at the
hardware level. Real-time developers especially take pride in being able to fine-tune and
control not just the hardware but also the deterministic timed behavior of the system. These
developers have always loved to write scheduling algorithms. The problem with writing your
own scheduler or RTOS is that it has been done a million times by a million engineers.
There are currently over a hundred different real-time operating systems and scheduling
algorithms commercially available. Designing and getting a basic scheduler up and running
isn’t a big deal, but creating one that is robust and correct all the time and that is designed
under a certified development cycle starts to push the time and budgetary constraints
available on projects today.
The advice I can give is to use a proven scheduling algorithm and only write your own on your
own free time if it is something that you are passionate about. Writing a scheduler can provide
great insights into how a real-time scheduler works. Examining and modifying one that already
exists can be far more efficient, however, and you can learn just as much.
250
Chapter 10 API Design for Embedded Applications
Console Application
Console Output Console Input
Communication Interface
HAL
The Renesas Synergy Platform does something very similar to this. While their
platform offers a wide variety of components, one component that I have found to
be very useful is their console-application module. This module can be added to any
Synergy project and be connected to USB, a UART, or any communication channel that
is available on the microcontroller! Once in the project, a developer creates a command
list and the function that should be executed if the command is received.
These components aren’t just reusable; they also drastically decrease the time required
to create a console feature on an embedded system. Once again, why reinvent the wheel
when one already exists? It’s far better to instead invent something that builds upon it!
Since so many systems have a need to transmit a command, after the third or fourth time
having to implement one, I designed a configurable and reusable command parser that
became a necessary element in my bag of tricks. The parser I designed contains several
elements:
• A configuration table that lists each available command and has a function pointer
to the command function
• A search algorithm that can find the matching command and execute the
associated function
This may seem complicated, but I’ve found that once a switch statement grows to more than
a dozen or so cases, it becomes difficult to manage and maintain. I’ve worked on applications
that had hundreds of commands and have seen this implemented in a massive, nearly
unsearchable switch statement.
Using a command parser with the elements I just described can improve
• readability;
• maintainability; and
• portability.
The best part is that it is simple to copy the template into a new application, list the new
commands, and in a few minutes have a command parser up and running in the system.
These were big problems for the clients, especially the timing and robustness
requirements. So, how do you solve a problem that is common to nearly every
embedded system and can be time-consuming to build? You create a reusable firmware
framework that can be ported to multiple hardware platforms!
That is exactly what I did. I took many of the lessons and discussions that we have
had in this book and applied them to creating a software framework that could be used to
easily adapt bootloaders to any microcontroller. The framework was not done in a single
shot, but rather started out with basic capabilities and APIs and then, over the course
of a half-dozen or so bootloaders, took full shape. This required creating low-level HAL
drivers and higher-level APIs. The basic, simplified results can be seen in Figure 10-4.
Bootloader Application
Communication Memory Scheduling Watchdog
HAL
253
Chapter 10 API Design for Embedded Applications
made, additional features were added, and the next one took two weeks. With time and
a reusable framework, bootloader implementation has become extremely easy and fast.
Table 10-1 shows the progression and the effect that the bootloader framework had on
the development activity. Obviously, the development effort has greatly benefited from
the availability of a reusable framework.
1 No 8 Weeks
2 No 8 Weeks
3 Yes 4 Weeks Framework 0.5
4 Yes 3 Weeks Framework 0.8
5 Yes 2 Weeks Framework 1.0
6 Yes 2 Weeks Framework 1.1
7 Yes 2 Weeks Framework 1.2
After examining the data, keep in mind that this is the time necessary to get the
bootloader up and running. Integrating a user application and updating it to work with
the bootloader can sometimes be considerable work, depending on how they designed
their application and the tools that they used.
4
http://elm-chan.org/fsw/ff/00index_e.html
254
Chapter 10 API Design for Embedded Applications
FatFs has a great API set. The APIs are all easy to remember and very simple. A short
listing can be seen in Listing 10-1. You might notice that all the APIs start with the same
prefix so as to identify that it is a file API, and then the function immediately follows. The
API is clean and easy to read and remember. One could complain that there are more
than a dozen functions in the API, but the APIs are so simple and straightforward that
it wouldn’t make any sense to reduce their number! The dozen functions are a rule of
thumb, not a law.
What is great about FatFs is that even the file organization is clean and has been well
thought out. The framework is layered so that a developer only needs to provide some
low-level access into the hardware, and the higher-level API calls will function on the
hardware as expected. This is a great example of how to architect software that has a
clean API and is modular enough to be used on multiple platforms.
Open source software, though, doesn’t always have the greatest implementation.
A quick analysis shows that there are many functions with a cyclomatic complexity
greater than 10. In fact, there are several with values greater than 20, and even a few in
the 30s and 40s. These functions obviously have probably never been fully tested and
255
Chapter 10 API Design for Embedded Applications
could potentially be harboring unknown bugs just waiting to strike. That doesn’t stop
engineers from using them. In all honesty, I’ve never had any obvious issues that I’ve
found when I use them, but still, “buyer” beware.
Going Further
APIs are the foundation that most modern software is built upon. They nicely abstract out
and hide the implementation details, allowing developers to focus on their application
rather than on common software features. The following are several thoughts on where
you can go from here to improve and get up to speed on creating your own APIs:
• Review the best practices for HALs. These best practices also
apply to APIs.
• Appropriate APIs
• Software architecture
• Software-development process
• Testing procedures
• Review the APIs from different RTOS suppliers. Which APIs seem to
be the easiest to use and remember?
Testing Portable
Embedded Software
“Program testing can be used to show the presence of bugs, but never to
show their absence!”
—Edsger W. Dijkstra
“Defect-free software does not exist.”
—Wietse Venema
SOFTWARE TERMINOLOGY
Regression testing is the ability to automatically run test cases that were previously executed
to verify that they still pass after the software has been modified.
257
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_11
Chapter 11 Testing Portable Embedded Software
The worst testing strategy that a development team can have, and unfortunately one
that I have seen implemented on numerous occasions, is the “cross your fingers and pray”
strategy. In this implementation, developers spot-check their code and the system to make
sure that they don’t notice any major system defects. The spot-checking has minimal code
coverage and is a highly manual procedure. When the product ships, developers mostly
just cross their fingers and pray that they don’t run into any major issues.
In order to have a consistent test strategy, developers need two key features in their
tests: automation and regression. Automated tests are necessary because there is no way
that a developer or a team can dedicate the time and effort necessary to manually check
that every line of code is executed and behaves as expected. The only way to perform
these checks is to automate testing so that it can be executed without human interaction.
Once tests are automated, developers can employ regression testing, which is
the ability to rerun tests that were previously executed to verify that they still pass.
Regression testing is an amazing tool that, if executed periodically, can show developers
where feature additions or changes in the code base may have broken the application
code. Debugging is far more efficient if a developer can be alerted immediately when the
problem arises in the system rather than weeks or months later.
Development teams that want to reuse their firmware and port it from one hardware
platform to the next need the ability to automatically test that their ported code is
working as expected—without requiring significant time. To do so, there are several key
test areas that need to be developed, as follows:
• Unit tests
• Functional tests
• Regression tests
• Integration tests
In this chapter, we will review best practices and considerations that developers
should look at when developing a test strategy for their reusable firmware.
Unit Testing
The most basic testing that every developer should be performing on their embedded
systems is unit testing. Unit testing is a software-development process in which the
smallest testable parts of an application are individually and independently scrutinized
258
Chapter 11 Testing Portable Embedded Software
First, a test harness would be set up that could automatically run the function under
all the input conditions that are required to test the function. Next, these inputs should
allow the function to follow all possible branches through the function, which can be
seen as the connected circles in the “Function Under Test” block. We will discuss how
we can ensure we have the minimum number of test cases required in the next section.
Finally, the function will produce an output that results in the work that it performed,
which can then be recorded in a test report.
SOFTWARE TERMINOLOGY
1
http://searchsoftwarequality.techtarget.com/definition/unit-testing
259
Chapter 11 Testing Portable Embedded Software
Unit tests can be performed manually, but they are far more effective if they can be
automated. Running any test case manually is a very time-consuming process. Always
try to find a way to automate the process. I don’t enjoy spending my time testing or
debugging, so the more automated these processes are, the better!
Embedded-software developers often struggle with determining the correct number
of test cases that they should have in order to fully test a function. Developers can easily
define the inputs to enter a function, but they also need to make sure that every line
of code is executed and that every code branch is traversed. Thankfully, there is a tool
developers can utilize that will save them from having to manually determine how many
test cases they need to create. That tool is cyclomatic complexity.
2
cCabe, Thomas Jr. “Software Quality Metrics to Identify Risk.” Presentation to the Department
M
of Homeland Security Software Assurance Working Group, 2008. (http://www.mccabe.com/ppt/
SoftwareQualityMetricsToIdentifyRisk.ppt#36); and Laird, Linda, and M. Carol Brennan
(2006). Software Measurement and Estimation: A Practical Approach. Los Alamitos, CA: IEEE
Computer Society.
260
Chapter 11 Testing Portable Embedded Software
The second benefit that developers can leverage from the cyclomatic complexity
measurement is that it provides a value for the minimum number of test cases that need
to be defined and executed in order to fully test a function. This is because cyclomatic
complexity measures the number of linearly independent paths through the function.
A linearly independent path is any path through a program that introduces at least one
new edge that is not included in any other linearly independent path.3 Let’s look at a few
quick examples.
The first example will be a function that takes two parameters and contains a simple
if/else statement. The code can be seen in Listing 11-1. In this example, we have two
linearly independent paths. The first path is where var1 is equal to var2. The second
path is if var1 and var2 are not equal. Using the M-squared RSM tool on this code, the
cyclomatic complexity result is two, which is what we would expect. We have two linearly
independent paths through the function.
In this example, we know that we should have two test cases to ensure that each
linearly independent path gets tested. A developer would also want to test the possible
values for var1 and var2 if it would impact the behavior of the function. There would be
no point in testing every possible combination if it would not impact how the function
behaves.
3
http://www.ironiacorp.com/
261
Chapter 11 Testing Portable Embedded Software
An interesting example is one where a developer has two if/else statements that
occur one after the other. Each if/else statement calls a function. The code can be seen
in Listing 11-2. If a developer were counting possible paths through the code, they would
notice the following function combinations would be executed and count the following:
1) Foo() Bar()
2) Foo() Code()
3) Dead() Bar()
4) Dead() Code()
What is interesting is that the cyclomatic complexity measurement is three for this
function despite there being four possible paths! Was the cyclomatic complexity wrong?
No, it wasn’t! Cyclomatic complexity measures linearly independent paths. The last path
is not linearly independent of the first three paths because it does not introduce any new
nodes (program statements) that were not included in the first three paths.4 This is a
great example of how cyclomatic complexity provides the minimum number and not the
actual number of test cases required to test a function.
if(var2)
{
Bar();
}
4
https://stackoverflow.com/questions/24191174/cyclomatic-complexity-1-if-statements
262
Chapter 11 Testing Portable Embedded Software
else
{
Code();
}
}
There are several different tools that developers can use to measure cyclomatic
complexity. A few that I have used in the past include the following:
• GMetrics5
• M-squared’s RSM6
• LDRA7
• Decreased costs
5
http://gmetrics.sourceforge.net/gmetrics-CyclomaticComplexityMetric.html
6
https://msquaredtechnologies.com/
7
http://www.ldra.com/en/
263
Chapter 11 Testing Portable Embedded Software
There are also several disadvantages that developers need to be aware of concerning
standard tests. These concerns include:
• periodically reviewing the standard tests to make sure that they still
completely cover the code;
By doing these three things, developers can make sure that they always have
standard tests that can be executed on their standard APIs.
F unctional Testing
Functional testing is a testing process that is used to verify that the software conforms
with all its requirements.8 In most instances, it’s a testing method that is used to verify
that the business needs or the end-user needs are being met. Functional testing is
most often executed at the application level to verify that the end users’ inputs provide
expected outputs.
Functional testing often follows black-box or white-box testing methods. In black-
box testing, the tests are created with little to no knowledge of how the system’s inner
workings were created. The test simply knows that pressing button A should result in
output A.
When the developer who designed the system gets involved in creating the tests, the
testing is known as white-box testing. Since the developer has intricate knowledge of the
inner workings of the device, they can devise tests that not only verify the inputs/outputs
for the system but also test corner cases and specific internal actions.
8
Grenning, James (2011). Test-Driven Development for Embedded C, The Pragmatic Programmers.
264
Chapter 11 Testing Portable Embedded Software
Functional testing can go beyond simply verifying the inputs and outputs
for the system. They can also include unit testing. For embedded developers, we
have an interesting problem in that most of our code touches hardware. Registers
get manipulated that affect the output on a physical pin. There can be multiple
configurations, and it can be difficult and time consuming to verify that all the
combinations are correct and function as expected. This is where two different tools
come into play to help embedded-systems developers: test-driven development and
hardware in-loop testing.
T est-Driven Development
In James Grenning’s book Test-Driven Development for Embedded C, he defines test-
driven development as “a technique for building software incrementally where no
production code is written without first writing a failing unit test.”8,9 The idea behind
TDD is that a developer first writes their test case, makes it fail, and then writes the code
necessary to make the test case pass. Once the test case passes, they write another test
case that fails, and then they write the code that resolves that test case. It then continues
in this manner until the entire software is completed.
There are several obvious advantages to using TDD, including the following:
• Test cases are created incrementally for every piece of code that is
written.
When one considers the advantages of thinking through the tests first and then
writing the code, it’s quite brilliant and counterintuitive to the way that embedded-
software developers write software, so much so that if you read the book and try it out,
you may find yourself struggling to accept a TDD mindset.
9
https://www.techopedia.com/definition/19509/functional-testing
265
Chapter 11 Testing Portable Embedded Software
TDD is not without its own headaches and issues. There are several disadvantages to
TDD that can affect embedded-software developers, such as the following:
Despite these disadvantages, developers may still want to investigate TDD and
determine which pieces could work best for their reusable firmware.
Debugger
Microcontroller
Comms
Host PC
Logic
Analyzer
Product Specific
DAC / ADC Hardware
266
Chapter 11 Testing Portable Embedded Software
HIL testing can contain several different components. First, there is the device under
test, which is commonly referred to as the DUT. The DUT will have information that it is
critical to access in order to verify the system is working, such as the following:
• Communication channels
Finally, this brings us to the host computer that runs the test suite and must monitor
and control the entire testing process. There are several different test harnesses from
companies such as LDRA, but it is also possible for developers to write their own Python
scripts to test and validate their system. In many cases, the direction a team will go will
depend upon several factors, such as the following:
• Available budget
• Available time
• Team members available for the project
The one thing that I’ve tried to convey throughout this book is that reusable software
saves time and money in the long run. It often does require more time and budget up
front, but once everything is in place, the speed at which a team can move and the
money that can be saved pays for itself multiple times over.
R
egression Testing
Developers who are creating reusable software absolutely need to make sure that
they can perform regression tests in a timely and automated manner. According to
Wikipedia, regression testing is “a type of software testing which verifies that software
which was previously developed and tested still performs the same way after it was
changed.”10 In summary, regression testing helps a developer ensure that when they
modify their software by fixing bugs, adding new features, or porting it to a new target
microcontroller, they can verify that the software behaves as expected without any new
bugs being created. If bugs have been created, the regression tests would catch them and
developers could deal with them.
The idea behind regression testing is that there is a test set that exists that can
be rerun on the system periodically to ensure that all the tests are still able to pass. If
regression testing is run often, any tests that fail should be easily traceable to the code
that changed and is causing the issue.
10
https://en.wikipedia.org/wiki/Regression_testing
268
Chapter 11 Testing Portable Embedded Software
Automating Tests
Any team or developer that is creating reusable software should be creating automated
tests. Even the simplest embedded system could require a hundred or more test cases to
ensure that the software behaves as expected. Attempting to manually run through these
tests will consume a lot of time and could be prone to errors. Therefore, automating test
cases is really the best solution for developers.
There are several different methods that teams can use to create automated test
cases. The most popular that I have encountered include
There are several example C/C++ test harnesses that developers can leverage, such
as Unity or Cpputest. Both C/C++ test harnesses are open source and can be found by
searching for them in your favorite web browser. The advantages to using a C/C++ test
harness is that
269
Chapter 11 Testing Portable Embedded Software
Target MCU
(Running Event Recording Library) Host PC
In the setup, a developer runs a small and efficient event-recording library that can
communicate with the debugging probe to store the event data on a host PC. The sample
rate for the event data will depend on the throughput to the PC along with the buffer
size given to the event-recording library. The larger the buffer, the more event data that
can be stored locally before it needs to be transmitted upstream. Even on resource-
constrained microcontrollers, the event-tracing library uses no more than 1 percent of
the CPU and usually has a few kilobytes of RAM allocated to it.
Once a developer has set up tracing and recorded a trace to their PC, they can use
their capture software to get statistical information about the system. This information
can be viewed in many ways, from simple tables and graphs to task-tracing diagrams. An
example trace that monitored a system that had three tasks to control LEDs can be seen
in Figure 11-4. This table shows useful information, such as CPU usage and minimum,
maximum, and average execution times, along with the task periodicity. A developer can
easily use this information to monitor and track not only changes to their application but
also whether their code is behaving as expected after porting it to a new microcontroller
or product.
270
Chapter 11 Testing Portable Embedded Software
Another interesting feature that developers can use to test and verify their software is
a visual inspection of the trace data. Figure 11-5 shows an example visualization where
a developer has discovered that there is a deadlock in their application code. The active
task is shown as a solid lifeline, while the task waiting to execute is shown as a hashed
line. The highest-priority task is on the right-hand side. Examining the trace reveals
when different events occur, such as:
• Task delays
• Context switches
271
Chapter 11 Testing Portable Embedded Software
272
Chapter 11 Testing Portable Embedded Software
portable manner across the entire microcontroller family! What is so surprising is that
Renesas doesn’t just supply example code but has also gone through a rigorous software-
development cycle that has strict quality-assurance requirements that include many of
the testing methodologies that we have been discussing in this chapter.
For example, Figure 11-6 shows the general process that Renesas uses every single
night to test that its framework works as expected!
The reader can easily see that the test setup is a combination of running tests both on
the software alone and on the hardware. By quickly surveying the diagram, a developer
can see that their software framework is the following:
• Statically analyzed
11
Renesas Synergy Software Quality Handbook, page 17.
273
Chapter 11 Testing Portable Embedded Software
• Unit tests
• Functional tests
• Regression tests
• Unit tests
• Functional tests
• Regression tests
• Integration tests
• Performance tests
The way that Renesas has built and tested its reusable firmware is a perfect example
of how to apply many of the concepts that we have been discussing throughout this book
and in some circumstances going well beyond those topics. The techniques that it is
applying are ones that every developer interested in reusable code should be leveraging,
examining, and using as a case study for how they build and design their own embedded
systems.
G
oing Further
Testing is critical in any embedded system but especially for developers who are
planning to reuse their software. This chapter has covered some basic fundamentals, but
once again, an entire book could be spent on the topic. The following are some ideas on
how you can put this chapter to use, along with where you can go to learn more:
12
http://www.mccabe.com/pdf/mccabe-nist235r.pdf
274
Chapter 11 Testing Portable Embedded Software
• Review each function and identify the test cases that need to be run
in order to cover all paths, inputs, and outputs.
• Select a test harness and implement the tests for each function.
• Record how long it takes to implement the tests initially. The next
time you port your code, record and compare the development times.
275
CHAPTER 12
A Practical Approach
to Code Reuse
“Make everything as simple as possible but not simpler.”
—Albert Einstein
• budgets and resources are scarce but the end results still need to be
delivered;
• any situation where developers are pressured in such a way that they
don’t develop software the way they know they should.
277
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_12
Chapter 12 A Practical Approach to Code Reuse
• Limited budget
• Delivery timelines
278
Chapter 12 A Practical Approach to Code Reuse
First, it is important to recognize that reusability and portability in the long run
will help decrease the total cost of ownership. Second, given how chaotic firmware
development can be when developers don’t follow a strict process or best practices or
continually jump through management hoops, the chances are that building some reuse
into the code up front will still be cheaper and faster in the short term. The trick is to
not go overboard and overdesign the reuse, but rather to identify where the maximum
benefit will be and aim to achieve it.
When time is short or the pressure is on, take a first pass at creating reusable
firmware. Design a HAL with the expectation that it will need to be updated in future
releases. Create configuration tables so that drivers and application modules are easily
configurable rather than hard coded. Add enough flexibility so that at a later time the
software can be improved without bringing down a house of cards.
We have discussed many times in this book that a HAL design, for example, will
require multiple iterations to get right. Implementing code reuse will also require several
iterations and phases before it is completely in place and being utilized successfully.
In general, developers can follow a very simple process that over time will allow them
to implement the practices that we have been discussing throughout this book. This
process contains five steps:
279
Chapter 12 A Practical Approach to Code Reuse
Identify
Where are we
now? Desired Results and Outcomes
Assess Evaluate
Execute Define
Roadmap
This process can follow a very formalized and strict implementation, or it can be
done by a single programmer who simply realizes that things need to change. The
best results are achieved by focusing on one to three improvements until the desired
outcome has been achieved. Let’s discuss in more detail how the reader should go about
following this simple process and how they can get the most from it.
280
Chapter 12 A Practical Approach to Code Reuse
On the surface, these desired results might not look like they overlap at all, but in
many cases the things that a developer wants to improve will have an impact on the
business results. In some situations, though, getting management to see and understand
the benefits can be difficult, and sometimes vice versa. I’ve worked with clients where
management saw the benefits and were failing to get the developers to buy in on how
important reuse and portability are.
The trick is to identify desired engineering results that also mesh with the results
management is looking for. If developers want to decrease bugs and rewrite modules,
while management is looking to decrease costs, the two are going to clash. Developers
need to understand a business need or result first and then translate what can be done
at the engineering level to get that result while simultaneously achieving their own
desired goals. Sometimes this requires that the individual developer get a read on their
management team and forge forward on their own without support in the hope that the
end results justify the means.
When all is said and done, there are three primary outcomes or results that a
business is looking to get out of reusable firmware. In many cases, a developer should
justify their activities to see if their reusable code will improve the odds of achieving their
goals. Always choose the low-hanging fruit that will make the biggest impact with the
smallest amount of effort. Let’s examine these outcomes and a few engineering activities
that go with them.
281
Chapter 12 A Practical Approach to Code Reuse
Projects that run their full course—or worse, go over schedule—can cost
dramatically more, sometimes even to the point that the project is canceled or the
business goes bust. This is a primary reason why there is such a big push to decrease
development times. We live in a society that is in a hurry, and unless we work for Apple
we want to be to market first.
There are several reuse options that developers can employ and suggest in order to
decrease their time to market. Several ideas that we have discussed throughout this book
are as follows:
282
Chapter 12 A Practical Approach to Code Reuse
There are several options available to developers that can help them decrease their
development costs. Several ideas that we have discussed throughout this book are as
follows:
There are many different things that can be done to decrease costs, such as buying a
professional debugger and good development tools. Spending money on the right tools
for the job can make a huge impact on total cost.
Implementing these will help to ensure that, over time, your firmware becomes high
quality.
Let’s be honest—some of these are obvious. The key is to find metrics that adhere
to all three of these characteristics. If a developer has to occasionally stop development
when they are under pressure to document and record a metric, they aren’t going to do
it. If they must stop at the end of the day to record a metric when they want to get home
and have a beer, they aren’t going to record it.
In order to get anything from a metric, it has to be easy to measure, automatically
trackable, and meaningful. If it doesn’t meet these criteria, then the data will end up with
holes that will cloud the result and make the metric meaningless. Let’s examine a few
metrics that developers should be interested in tracking and for which it is possible to do
so automatically.
Each component should track several different metrics, which include the following:
• Code size
• RAM usage
• Cyclomatic complexity
• Development time
That’s it! One might want to track the time spent debugging because so much time
is spent debugging. Identifying and using proper debug techniques could dramatically
improve development times and costs. That is beyond this book’s goal, however, so let’s
examine a few of these metrics and understand why we should be tracking them.
First, development time for a component is obvious. We need to understand how
long it takes us to develop the component and then how long it takes during each port to
get the component up and running. Tracking the time spent on a component from one
project to the next can also give a developer a sense of how much time is saved through
reuse. For example, the first time I created an SPI peripheral HAL and implementation
design pattern it took 40 development hours. The next time I used the SPI HAL and
design pattern, it took only eight hours to port and fully test the driver. Before creating a
reusable module, it was taking on average 32 hours to implement and test the drivers.
For an extra eight hours of work, I could then save 24 hours of work on every project
that followed! That was just for that one component. When you consider that there
are at least a dozen components in most projects, it’s easy to not just see but to show
management and clients the time savings resulting from reusing software. The data
is there to justify further improvement efforts, but also can be used as a baseline for
estimating future project timelines.
Second, developers should be tracking each component’s code and RAM usage. As
we discussed earlier in this book, reusable components can potentially use more RAM
and code space. That means that when we start looking at the microcontrollers we are
using, there may be a trade-off where we need to use a more expensive microcontroller
to fit all the code. While we may be saving money and time through reuse, we might be
paying back the money portion in more expensive hardware. This doesn’t have to be the
case, but it’s a good metric to track to ensure that the code base doesn’t get out of control
and to allow developers to easily select the microcontroller they need for a given project.
286
Chapter 12 A Practical Approach to Code Reuse
Tracking these metrics doesn’t have to be a big deal. A script can easily be written
that parses the map-file output from the compiler to calculate the code and RAM usage
and log it to a database or print it in a report. For tracking development time, developers
could use online tools such as Trac, but they could also just as easily use a spreadsheet.
Figure 12-2 shows an example of how a developer can create a simple activity list for a
project containing all the different common software components and then record the
development time for each. The data is fake, but it does provide the reader with a general
idea of how, if they record this data, they can easily start to get minimum, maximum, and
average development hours for different activities.
Figure 12-2 shows just a handful of low-level microcontroller activities, but the list
can be fully expanded to include BSP and application components as well. A complete
list can be found with the download materials for this book. As a team builds more
products, they will very quickly be able to not only improve their estimation skills but
also calculate how taking the time up front to do things right can impact their project.
287
Chapter 12 A Practical Approach to Code Reuse
Assess the Results
Once the implementation is under way, developers can continually monitor their
progress and assess where they are at in relation to getting the desired results. Having
good metrics is key to being able to assess the results appropriately. Once the results
have been achieved, developers can move back to the Identify step to determine what
their next focus point will be to improve the reusability of their firmware.
Before we conclude this chapter and the book, I would like to point out several
additional best practices that developers should keep their eyes out for in order to ensure
that they can develop reusable firmware.
288
Chapter 12 A Practical Approach to Code Reuse
There are several factors that developers should watch for in order to identify a
design pattern. They can all be summed up in just a single point:
• If I’ve seen this before or think I may need it again, then a design
pattern either already exists or I should create a new one for future use.
It’s that simple. It’s not rocket science, and believe me, I know!
There are several common design patterns that the reader can find in almost every
embedded system. These patterns include:
• Memory-mapping drivers
• Calculating checksums
• Error handling
• Calibration
• Circular buffers
• and more
The list could go on and on. As you develop your software, ask yourself if this is a
problem that someone else may have encountered in the past, or that you have, and, if
so, do a quick search or browse your own code for the solution. It can save considerable
time and effort. Once a developer starts to recognize these design patterns, they can start
creating their own design-pattern templates and checklists.
289
Chapter 12 A Practical Approach to Code Reuse
for interacting with external memory devices. We discussed a few chapters back that
every memory device follows JEDEC standards, which makes writing, reading, and
interacting with those devices the same no matter who the manufacturer is. Interacting
with those devices becomes a design pattern, and once a developer creates that pattern
once, they can use it every time that problem presents itself.
Another template example is the Doxygen templates that are used to document
code. Having a consistent method for documenting code is crucial. It needs to be done
over and over again for every project. Rather than creating a new way to document
software in every project, I created a template that I could easily use on each and every
project. Over time, I do update and adjust those templates, but the base pattern is there,
and it decreases the effort tremendously.
Templates are a great way to speed up software development and prevent developers
from repeating work that has already done.
Another tool that I have found to be indispensable is using checklists. Checklists
can be used to manage everything from creating a new project and checking in a project
to revision control and final reviews for releasing software. A checklist is a great way to
take a complex procedure, or even one that is not done very often, and ensure that it is
repeatable.
For example, I have a project-setup checklist that I use at the start of every project.
The checklist doesn’t go into low-level details but has the high-level points to remind me
what I should set up and configure in order to get the project up and running the fastest.
For example, my project-setup checklist is set up to follow several different phases, as
shown in Figure 12-3.
290
Chapter 12 A Practical Approach to Code Reuse
1
https://www.beningo.com/tools-embedded-software-start-up-checklist/
291
Chapter 12 A Practical Approach to Code Reuse
As the reader can tell from the checklist, there is a lot that is done before a single line
of code is ever written for the project. Many of these items would be easily overlooked
if there were pressure on to start banging out code as quickly as possible. The checklist
ensures that proper procedures are followed that will maximize the project’s chances for
success.
Every firmware project that I work on starts with this checklist. If you examine the
checklist carefully, you’ll also notice that there are entries that remind me to bring
templates into the project. For example, there is mention of the Doxygen templates,
along with HALs and APIs. At that bullet point, if the project that is being developed
requires a communication protocol, circular buffer, command parser, and so forth,
those template components would be added to the code base. By the time the checklist
is completed, there is a nearly completed skeleton for the software along with the
implementation for any common design patterns.
In many instances, in just a day or so a base system can be brought online that
if developed from scratch would easily take a month or more. This is the power that
reusability and portability bring to the table.
292
Chapter 12 A Practical Approach to Code Reuse
293
Chapter 12 A Practical Approach to Code Reuse
5) Paste the change log into the commit comments and add any
additional relevant comments.
294
Chapter 12 A Practical Approach to Code Reuse
295
Chapter 12 A Practical Approach to Code Reuse
When I first started my business, I had worked at several large and small companies
and was absolutely set on making sure that:
1) I would use the right tools for the job no matter the cost.
In several instances, I purchased tools that cost more than $10,000 to the mutual
benefit of both myself and my clients. Each client that I served saved the $10,000 on the
tools, which they were then able to put back into their own development cycles. In the
grand scheme of things, the $10,000 was nothing to those companies, but to those clients
it was a huge gesture.
When developers are evaluating whether to start using reusable firmware in their
own development cycles, they need to ask themselves what the short-term and long-
term costs will be if they do nothing. It may cost the company $10,000, $20,000, or
maybe even $50,000 up front to create firmware that is reusable, or those amounts over
several years as reuse is increased in iterations. But what is the return on investment
over one, two, five, and ten years? It might be that with an upfront investment of $10,000
a company can save $100,000 in the next two years. Perhaps future products can beat
the competition to market or improve quality to a point that customers prefer their
product.
I see so many teams that make short-term decisions without considering the long-
term perspective. Unfortunately, I see many of these teams choke, stumble, and, in some
cases, even go out of business. Others are able to just barely survive and end up in a mad
dash to implement reusability and best practices that they should have been using all
along.
Don’t get caught up in short-term thinking. Keep this question on your mind and
ask it at every crossroads. The costliest mistakes that I’ve seen in the industry and in life
happen not when people jump into a situation, but rather when they do nothing and
hope for the best.
296
Chapter 12 A Practical Approach to Code Reuse
Final Thoughts
When looking back over my short career so far and examining what has made the
greatest impact on my clients’ products and software, I can sum it up in one word: reuse.
It’s a simple idea to reuse embedded software. Reuse has been going on for decades in
the PC world. Yet, firmware developers have always opted for writing software in a one-
off fashion, ignoring reuse and opting to just get it done and deal with the fires that are
burning today.
As we progress through the coming decades, it is absolutely clear in my mind
that the teams that will be the most successful are the teams that utilize reuse to the
furthest extent. Teams that leverage HALs, APIs, microcontroller platforms, and even
automatically generated code will develop software far faster than today’s standards.
Teams that reuse code can focus on their product’s key features, the differentiators that
set it apart from the competition.
Embedded-software developers have always been experts in the microcontroller,
the low-level bits and bytes. That is going to change over the coming decades. More
and more developers are going to be experts in HALs and APIs and have little to no
knowledge about the hardware. As we move to 32-bit microcontrollers, the complexity
will become so high that the only way we can possibly expect to get a product to market
in a year or less will be to reuse what we have already created and leverage existing code.
Microcontroller manufacturers, as experts in their own hardware, are starting
to provide frameworks and HALs for developers to use. We will see the hardware
abstracted, but even when that does happen, teams that utilize reusable concepts will
still have an edge over teams that are just getting things done for today with no thought
about tomorrow.
I’ve had the pleasure of working with teams in more than a dozen different countries
to improve software-development processes and help teams get their products to
market. As you contemplate the material and concepts in this book, I encourage you to
start with the low-hanging fruit that will have the most dramatic impact on your software
and business in the shortest amount of time. Reinvesting the time saved to further
implement and improve your software will have a powerful effect on your products and
end users.
297
Chapter 12 A Practical Approach to Code Reuse
G
oing Further
We have covered many topics in this book, and we are only at the beginning. Don’t
forget that this will be an iterative process that very well might take you years. These
are exciting times, and the following are a few more thoughts on where you can go
from here:
• Once you have your top three priorities, rank each priority and review
how well you are currently doing in this area.
2
https://www.beningo.com/store/an-api-standard-for-mcus/
298
Chapter 12 A Practical Approach to Code Reuse
299
Index
A ThreadX tx_thread_create, 55
wrappers, 55
Abstract Data Types (ADTs)
Assertion fundamentals
abstractions, 80
assert.h header file, 68
definition, 81
definition, 68
implementation data structure, 82
input and pre-condition, 69
initialization function, 83
macro implementation, 69
interface specification, 81
Automating tests, 269
operations, 81
pop method, 84
stack method initialization, 83 B
Stack_Push, 85
Boogeyman
Abstractions, see Abstract Data
integration issues, 35
Types (ADTs)
issues, 33
Application Programming Interfaces
microcontroller vendors, 34
(APIs), 23
peripheral technique, 35
architecture, 24
ramifications, 34
characteristics, 49
readability issues, 35
consistent look and feel, 53
Bootloaders framework, 252
const keyword, 49
documentation, 53
flexible and configuration, 53 C
Micrium uc/OS-III, 54 Callback functions
naming conventions, 50 ArrayInit function, 88
uOS III, 52 definition, 86
comparison (API and HAL), 58 elements to random numbers, 89
designing process, 53 implementation, 87
embedded-software developers, 49 initialization code, 87
FreeRTOS TaskCreate, 54 instances, 86
HAL design, 57 lower-level code, 87
scope, 48 signal handler, 87
301
© Jacob Beningo 2017
J. Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2
Index
302
Index
303
Index
304
Index
305
Index
306
Index
308