Reusable Firmware Dev Master
Reusable Firmware Dev Master
Reusable Firmware Dev Master
This work is subject to copyright. All rights are reserved by the Publisher,
whether the whole or part of the material is concerned, specifically the
rights of translation, reprinting, reuse of illustrations, recitation,
broadcasting, reproduction on microfilms or in any other physical way, and
transmission or information storage and retrieval, electronic adaptation,
computer software, or by similar or dissimilar methodology now known or
hereafter developed.
Trademarked names, logos, and images may appear in this book. Rather
than use a trademark symbol with every occurrence of a trademarked name,
logo, or image, we use the names, logos, and images only in an editorial
fashion and to the benefit of the trademark owner, with no intention of
infringement of the trademark.The use in this publication of trade names,
trademarks, service marks, and similar terms, even if they are not identified
as such, is not to be taken as an expression of opinion as to whether or not
they are subject to proprietary rights.
While the advice and information in this book are believed to be true and
accurate at the date of publication, neither the authors nor the editors nor
the publisher can accept any legal responsibility for any errors or omissions
that may be made. The publisher makes no warranty, express or implied,
with respect to the material contained herein.
Portable Firmware
Modularity
Following a Standard
Embedded-Software Architecture
Project Organization
Going Further
Wrapping APIs
Going Further
Design by Contract
Assertion Fundamentals
Callback Functions
Error Handling
Going Further
Reusable Drivers
Memory-Mapping Methodologies
Going Further
An Introduction to Doxygen
Installing Doxygen
Documenting Functions
Documenting Modules
Going Further
Going Further
Going Further
Going Further
Going Further
Designing APIs
Application Frameworks
Going Further
Unit Testing
Taking Advantage of Cyclomatic Complexity for Unit Testing
Functional Testing
Test-Driven Development
Regression Testing
Automating Tests
Going Further
Final Thoughts
Going Further
Index
About the Author and About the Technical
Reviewers
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.
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).
Rami Zewail
received Bachelor of Science and Master of
Science degrees in electronics and
communications engineering from the Arab
Academy for Science and Technology, Egypt,
earned in 2002 and 2004 respectively. He earned
his PhD in electrical and computer engineering
from the University of Alberta, Canada, in 2010.
He has over 15 years of academic and
industrial R&D experience in the areas of
embedded systems and machine learning. He has contributed to the
scientific community with a patent and over 19 publications in the areas of
embedded computing, machine learning, and statistical modeling.
Currently, he is co-founder and staff researcher at Smart Empower
Innovations Labs Inc., a Canada-based R&D and consulting corporation
specialized in the fields of embedded systems and machine learning.
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.
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_1
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.
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:
Decreasing time to market by not having to reinvent the wheel (which
can be time consuming)
Decreasing project costs by leveraging existing components and
libraries
Improving product quality through use of proven and continuously
tested software
Portable firmware also has several indirect advantages that many teams
overlook but that can far outweigh the direct benefits , such as:
More time in the development cycle to focus on product innovation
and differentiation
Decreased team stress levels due to limiting how much total code
needs to be developed (happy, relaxed engineers are more innovative
and efficient)
Organized and well-documented code that can make porting and
maintenance easier and more cost effective
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:
The software architecture’s needing to be well thought through
Understanding potential architectural differences between
microcontrollers
Developing regression tests to ensure porting is successful
Selecting real-time languages and understanding their interoperability
or lack thereof
Having experienced and high-skilled engineers available to develop a
portable and scalable architecture
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 .
Figure 1-2 Embedded-software programming language use2
1. is modular
2. is loosely coupled
4. is ANSI-C compliant
8. is simple
9. uses encapsulation and abstract data types
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
Improved software understanding through the modules’ organization
The ability to copy modules and use them in new applications
The ability to remove modules from a program and replace them with
new functionality
Easing requirements’ traceability
Developing automated regression testing for individual modules and
features
Overall decreased time to market and development costs
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.
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.
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 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 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.
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.
Figure 1-8 Using conditional compilation for non-portable constructs
Embedded-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.
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 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.
Figure 1-10 Three-layer embedded-software architecture
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.
Board-Support Package refers to driver code that is dependent
upon lower-level microcontroller driver code. These drivers usually
support external integrated circuits such as EEPROM or flash chips.
Middleware refers to the software layer that contains software
dependent upon the lower-lying hardware drivers but does not directly
contain application code. Application code is usually dependent upon the
software contained within this middle layer of software.
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.
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.
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.
The following is an example organization that a developer may decide
to implement to organize their project:
Drivers
Application
Task Schedulers
Protocol Stacks
Configuration
Supporting Files and Docs
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.
Select two or three compilers, such as GCC, Keil, and IAR. Download
their user manuals and review the documentation on how they
implemented the ambiguous areas in the selected standard.
Purchase a copy of MISRA C/C++ and become familiar with the
recommended best practices.
Develop your own coding standard on the constructs that are allowed
within an application and how compiler intrinsics and extensions
should be handled.
Review your typical software architecture. Does it have well-defined
layers? Does each layer have a well-defined interface? If not, now is
the perfect time to spend a few minutes architecting your firmware
stack-up. (Don’t be concerned with defining the interface just yet.
We’ll be covering how to do this in the coming chapters.)
Review the last section on “Getting Started Writing Portable
Firmware.” On a sheet of paper, draw your own spider diagram and
rank how well your code exhibits the portable-firmware
characteristics. Select one or two characteristics that you feel will have
the biggest impact on your code and focus on improving those.
Periodically review and reevaluate.
Footnotes
1 Embedded Marketing Study, 2009 – 2015, UBM
5 http://whatis.techtarget.com/definition/layering
6 http://whatis.techtarget.com/definition/interface
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_2
Software Terminology
Platform is a collection of APIs, HALs, modules, components, libraries,
and frameworks designed to work together to speed up embedded-
software development and decrease project costs.
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. For this reason, in
many cases these “industry standards” fail, and each vendor is now
producing their own unique and custom standard.
Software Terminology
Coding standards contain a set of programming rules, naming
conventions, and layout specifications that provide a consistent
software.4
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.
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.
Wrapping 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.
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
High-level components that require third-party software
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.
Going 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 the reader can do to strengthen their
understanding and start applying the concepts we’ve just discussed
immediately in their own development efforts:
Identify at least three different HALs that exist currently in the
embedded-software industry. Schedule time to review these standards.
While reviewing them, develop a simple chart that answers the
following questions:
What strengths does this HAL exhibit?
What weaknesses does this HAL exhibit?
How well does it meet the characteristics every good HAL should
have?
Identify at least three different APIs that exist currently in the
embedded-software industry. Schedule time to review these standards.
While reviewing them, develop a simple chart that answers the
following questions:
What strengths does this API exhibit?
What weaknesses does this API exhibit?
How well does it meet the characteristics every good API should
have?
Earlier in the chapter, three different microcontroller platforms were
mentioned that utilize a HAL and API framework. Investigate each
framework listed here and examine the similarities and differences:
Renesas Synergy Platform
Microchip Harmony
ST Microelectronics STM32CubeMx
From the preceding platforms, how easy would it be to switch from
one silicon vendor to the next? Are their APIs similar or completely
different? List several advantages and disadvantages to using these
platforms.
Review the characteristics of HALs and APIs. Make a simple
spreadsheet with each characteristic listed. Now, go online to a few
RTOS vendors. Review their API interfaces for task management,
semaphores, mutexes, and message queues. How well do these APIs
meet the characteristics of good APIs?
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.
Review Figures 2-4 to Figure 2-8. Do real-time operating systems have
a standard API interface that developers can follow? What does this
mean for developers when it comes to porting to a new OS,
development time, learning curve, and costs?
Design a wrapper that could be used to interact with any RTOS
function calls.
Footnotes
1 http://www.webopedia.com/TERM/A/API.html
2 https://www.arduino.cc/
3 https://www.mbed.com/en/
4 http://www.decision-making-confidence.com/kepner-tregoe-decision-
making.html
5 http://www.freertos.org/a00106.html
6 https://doc.micrium.com/pages/viewpage.action?pageId=10753180
7 http://www.freertos.org/a00125.html
8 https://doc.micrium.com/display/osiiidoc/OSTaskCreate
9 http://rtos.com/images/uploads/programmersguide_threadx.pdf
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_3
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.
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.
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.
Definitions
Pre-conditions a re 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.
Assertion 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.
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:
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.
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
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.
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 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.
Figure 3-12 DMA-controlled data transfer
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
Object-Oriented Programming in C
Developers should consider developing their drivers and their application
code in an object-oriented manner. The C programming language is not an
object-oriented programming language. C is a procedural programming
language where the primary focus is to specify a series of well-structured
steps and procedures within its programming context to produce a
program.7 An object-oriented programming language, on the other hand, is
a programming language that focuses on the definition of and operations
that are performed on data.
There are several characteristics that set an object-oriented
programming language apart from a procedural language . These include:
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.
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.
Objects are any entity that has a state or known behavior.
Classes are a logical software entity that is a collection of objects.
Inheritance is when a class inherits the characteristics of another
class.
Abstractions and Abstract Data Types (ADTs)
An abstraction hides the underlying implementation details while making
the functionality available to developers. For example, a well-implemented
GPIO driver will provide an interface that tells a developer what can be
done with the driver, but the developer doesn’t need to know any details
about how the driver is implemented or even on what hardware it runs.
Abstractions hide the details from developers, creating a black box that
simplifies what they need to know to use the software.
Abstractions don’t only apply to component interfaces. Abstractions can
just as easily be applied to data types. Abstract data types (often written as
ADT for short) are data types whose implementation details are hidden
from the view of the user for a data structure. There are several different
methods that can be used to create an ADT in C. One method that is
straightforward can be done in five easy steps. Let’s look at how we can
create an ADT for managing a memory stack.
First, a developer defines the abstract data type. The ADT in C is
usually defined as a pointer to a structure. The ADT is declared within a
header file without any underlying details, leaving it up to the implementer
to fully declare the ADT in the source module. An example of an ADT
would be a StackPtr_t, NodePtr_t, or QueuePtr_t, to name a few.
If a developer were to define an ADT for a stack, they would start by
defining the code shown in Figure 3-14 in the stack.h file. The details
for the members in StackStruct_t are completely hidden from the
users’ perspective. Any interaction with StackPtr_t must be done using
predefined operations.
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
destroying the stack
checking to see if the stack is full
checking to see if the stack is empty
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.
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.
Figure 3-18 Stack ADT push method
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.
Figure 3-19 Stack ADT pop method
Callback 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.
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,
transmission is interrupted, and so forth. Do everything necessary to try to
recover from an error state, and if the driver is unable to do so, don’t hang
there forever, but rather return an error code that can help developers debug
the problem.
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.
Footnotes
1 https://en.wikipedia.org/wiki/Design_by_contract
2 http://wiki.c2.com/?WhatAreAssertions
3 https://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
7 https://www.techopedia.com/definition/8982/procedural-language
8 http://www.javatpoint.com/java-oops-concepts
9 https://en.wikipedia.org/wiki/Callback_(computer_programming)
10 https://en.wikipedia.org/wiki/Software_design_pattern
11 http://www.beningo.com/wp-content/uploads/Downloads/ATP.zip
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_4
Reusable 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.
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.
Memory-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:
Direct memory mapping
Using pointers
Using structures
Using pointer arrays
Let’s examine the different methods that can be used to map a driver to
memory.
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.
Figure 4-12 Incorrectly using the volatile keyword for pointer data
Figure 4-13 Correctly using the volatile keyword for pointer data
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.
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.
Figure 4-18 Defining and using the memory-mapped structure
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.
Select a code module in one of your applications. Identify all the areas
where variables and functions are implicitly declared extern. Which
ones can be changed to static?
Examine the hardware register mapping file for your microcontroller.
What keywords are present? const? volatile?
Examine the hardware register mapping file for your microcontroller.
What memory mapping method is it using?
Examine the datasheet and hardware register files for your
microcontroller. Write three different timer drivers using each of the
following methods:
Directly accessing registers
Using structures
Using pointer arrays
Answer the following questions about the drivers:
Which driver was the fastest to implement?
Which has the smallest code size? The largest?
Which is more human readable?
Port each driver to a different microcontroller using the drivers just
written as the starting point. Answer the following questions about the
drivers:
Which driver was the fastest to implement?
Which has the smallest code size? The largest?
Which is more human readable?
Which driver was the easiest and quickest to port?
Footnotes
1 C in a Nutshell, pages 156, 165
3 C in a Nutshell, page 57
“Just because you don’t like something doesn’t mean that it isn’t
helping you.”
—Tim Harford
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.
An Introduction to Doxygen
Discipline cannot be taught from the pages of a book, but how to set up and
leverage automated documentation tools can be. Software tools such as
JavaDocs, NaturalDocs, and Doxygen are example tools that generate
documentation from the code and comments. In this book, we will focus on
Doxygen, a tool that is open source and widely adopted within the software
industry.
“Doxygen is a documentation system for C++, C, Java, Objective-C,
Python, IDL (Corba and Microsoft flavors), Fortran, VHDL, PHP, C#, and
to some extent D.”1 Doxygen offers several advantages to the software
developer who is looking to keep their documentation consistent and up to
date with what the source code is actually doing. Besides its free price,
which is hard to beat, Doxygen allows developers to use the comments
within the header, source, and other text files to generate documentation in
common formats, such as HTML, RTF, or PDF. Doxygen allows the
developer to show how a project was implemented by browsing files,
classes, modules, variables, and other types that are used in the program in
addition to generating graphs to show how they interact with each other.
Doxygen can be considered a way to automatically generate a software
manual for the project. Developers can even go so far as to document their
tools, standards, and nearly any other piece of project documentation that
might need to be generated.
Installing 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 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.
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.
Figure 5-4 DoxyWizard Output setup
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;
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
Function description; that is, what it does
A list of pre-conditions that should be completed before calling the
function
A list of post-conditions that a developer can expect to occur if the pre-
conditions have been met before calling the function
Descriptions of the function’s parameter list and whether the parameter
is used to input and/or output data
A description of the function return data, if there is any
An example code snippet on how to properly use the function
A list of related functions that would be relevant for a developer to be
aware of
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 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()
*//**
Listing 5-1 Function Start Block
* \b Description:
*
* This function is used to initialize the Dio
based on the
* configuration table defined in dio_cfg module.
Listing 5-2 Function Description Block
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.
* \b Example:
* @code
* const DioConfig_t *DioConfig =
Dio_ConfigGet();
*
* Dio_Init(DioConfig);
* @endcode
Listing 5-5 Function Example Code Block
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.
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
Listing 5-6 Functions Related Block
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.
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.
Figure 5-7 Function Revision Log
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
Original file date
Module version
Compiler used
Target
Any specialized notes
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
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
************************************************
******************/
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.
/** @}*/
Listing 5-8 @Addtogroup Comment Block
Creating a Reusable Template
For the most part, no developer is going to be able to remember from
memory all the details thast are required to fully document a module and its
contents. Remember, consistency and readability are important
characteristics for software that will be 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.
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.
Ten Tips for Commenting C Code3
During the hustle and bustle of the development cycle, it isn’t uncommon
for commenting the code to fall to the bottom of the priority list. With the
pressure to get the product out the door, discipline usually fails, and short
cuts result in a poorly 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:
Going 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:
Review the software documentation spectrum located in Figure 5-1.
Where do you/your team currently lie within the figure?
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.
Add a calendar reminder to review the progress being made in
improving the documentation process monthly.
Read “10 Tricks for Documenting Embedded Software” on Jacob’s
blog at EDN.com.
Download and install Doxygen.
Download Jacob’s Doxygen templates from www.beningo.com .
Review each template and become familiar with the different tags
used.
Select a module from an existing source project and convert it to use
the Doxygen template. Generate the documentation and examine the
resulting output.
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.
Generate output documentation for HTML, PDF, RTF, and LaTeX. Get
familiar with potential issues and workarounds that may be required to
get the look and feel needed for each documentation set.
Experiment with the advanced tabs within the DoxyWizard and learn
what each feature does and how it affects the generated output.
Footnotes
1 Doxygen, August 2015, www.doxygen.org .
2 Legal wording is modified from Freescale source example code and provided as an example.
6) Test.
7) Repeat for each peripheral.
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.
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 .
In my experience, many developers overlook the need to have callbacks
as part of their interface. Instead, every application has a slightly different
version of the driver that is dependent upon the application. The ability to
port this code drastically decreases and often causes confusion and issues
when trying to update the drivers. The interface example is fairly simple
and can be seen in Figure 6-3.
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.
*
* POST-CONDITION: The Pwm peripheral is set up
with the configuration settings.
*
* @param[in] Config is a pointer to the
configuration table that contains the
initialization for the peripheral.
*
* @return void
************************************************
**********************/
Listing 6-1 Documentation for pwm Initialization Interface
1) Read the feature name; create a function with the same name.
2) Populate the parameter list based on the @param tags in the
documentation.
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.
7) Review the documentation and populate examples and the @see tags.
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.
Going 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:
Download the Doxygen header and source modules from
https://www.beningo.com/162-code-templates/ .
Select three microcontroller development kits to test a HAL on.
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.
Set up a revision-control repository in which to store your
microcontroller HALs.
Identify two team members to participate in HAL design and schedule
regular weekly meetings for HAL reviews and development.
Footnotes
1 Groundhog Day, the 1993 comedy starring Bill Murray. If you don’t understand this reference then
stop now, go on Netflix, Hulu, etc., and watch the movie. An all-time classic.
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.
/***********************************************
***********************
* Includes
************************************************
**********************/
/***********************************************
***********************
* Preprocessor Constants
************************************************
**********************/
/**
* Defines the number of pins on each processor
port.
*/
#define NUMBER_OF_CHANNELS_PER_PORT 8U
/**
* 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 */
PORT1_2, /**< PORT1_2 */
PORT1_3, /**< PORT1_3 */
UHF_SEL, /**< PORT1_4 */
PORT1_5, /**< PORT1_5 */
PORT1_6, /**< PORT1_6 */
PORT1_7, /**< PORT1_7 */
DIO_MAX_PIN_NUMBER /**< MAX CHANNELS
*/
}DioChannel_t;
/**
* 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.
*/
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**********************************************
******/
Listing 7-1 Code Listing for Dio_Config.h
/***********************************************
***********************
* 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.
*/
const DioConfig_t DioConfig[] =
{
/* Resistor
Initial */
/*
Channel Enabled Direction Pin
Function */
/*
*/
{
PORT1_0, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_1, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_2, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_3, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_4, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_5, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_6, DISABLED, OUTPUT, HIGH,
FCN_GPIO },
{
PORT1_7, DISABLED, OUTPUT, HIGH,
FCN_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.
*
* @return A pointer to the
configuration table.
*
* \b Example Example:
* @code
* const Dio_ConfigType *DioConfig =
Dio_GetConfig();
*
* Dio_Init(DioConfig);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
*
************************************************
**********************/
const DioConfig_t * const Dio_ConfigGet(void)
{
/*
* The cast is performed to ensure that the
address of the first element
* of configuration table is returned as a
constant pointer and NOT a
* pointer that can be modified.
*/
return (const *)DioConfig[0];
}
/***********************************************
***********************
* 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
#ifdef __cplusplus
} // extern "C"
#endif
#endif /*DIO_H_*/
/***********************************************
**********************
* Module Preprocessor Macros
************************************************
**********************/
/***********************************************
***********************
* 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,
};
/**
* Defines a table of pointers to the
peripheral resistor enable register
* on the microcontroller
*/
static TYPE volatile * const Resistor[NUM_PORTS]
=
{
(TYPE*)®ISTER1, (TYPE*)®ISTER2,
};
/**
* 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.
*
* POST-CONDITION: The DIO peripheral is set up
with the configuration
* settings.
*
* @param Config is a pointer to
the configuration table that
* contains
the initialization for the peripheral.
*
* @return void
*
* \b Example:
* @code
* const DioConfig_t *DioConfig =
Dio_ConfigGet();
*
* Dio_Init(DioConfig);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
************************************************
**********************/
void Dio_Init(const DioConfig_t * Config)
{
/* TODO: Define implementation */
}
/***********************************************
***********************
* Function : Dio_ChannelRead()
*//**
* \b Description:
*
* This function is used to read the state of a
dio channel (pin)
*
* PRE-CONDITION: The channel is configured as
INPUT <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 is returned.
*
* @param Channel is the
DioChannel_t that represents a pin
*
* @return The state of the channel
as HIGH or LOW
*
* \b Example:
* @code
* uint8_t pin = Dio_ReadChannel(PORT1_0);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
************************************************
**********************/
DioPinState_t Dio_ChannelRead(DioChannel_t
Channel)
{
/***********************************************
***********************
* Function : Dio_ChannelWrite()
*//**
* \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
* DioPinS
tate_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
*
************************************************
**********************/
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
*
* <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
* peripher
al map
* @param Value is the value
to set the Dio register to
*
* @return void
*
* \b Example:
* @code
* Dio_RegisterWrite(0x1000, 0x15);
* @endcode
*
* @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
* 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.
*
* @param Function is the callback function
that will be registered
* @param CallbackFunction is a function
pointer to the desired
* function
*
* @return None.
*
* \b Example:
* @code
* DioCallback_t Dio_Function =
DIO_SAMPLE_COMPLETE;
*
* Dio_CallbackRegister(Dio_Function,
Dio_SampleAverage);
* @endcode
*
* @see Dio_Init
* @see Dio_ChannelRead
* @see Dio_ChannelWrite
* @see Dio_ChannelToggle
* @see Dio_RegisterWrite
* @see Dio_RegisterRead
* @see Dio_CallbackRegister
*
************************************************
**********************/
void Dio_CallbackRegister(DioCallback_t
Function,
TYPE (*CallbackFunction)(type))
{
/**
* 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,
};
/**
* 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
};
Listing 7-5 Pointer Array Memory Map Example for Kinetis-L KL25Z
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.
DioPinState_t Dio_ChannelRead(DioChannel_t
Channel)
{
/* Read the port associated with the desired
pin */
DioPinState_t PortState =
(DioPinState_t)*portsin[Channel/NUM_PINS_P
ER_PORT];
*RegisterPointer = Value;
}
Listing 7-10 GPIO RegisterWrite Example for Kinetis-L KL25Z
return *RegisterPointer;
}
Listing 7-11 GPIO RegisterRead Example for Kinetis-L KL25Z
Going 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.
Identify at least three different microcontrollers that you are currently
working with or interested in working with. Collect the GPIO
peripheral’s datasheets for each microcontroller.
Review the datasheets in detail and generate a peripheral feature list
like the one shown in Table 7-1. How do the results compare? Are they
the same or have new peripheral features such as input validation been
discovered?
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.
Create a documented template using the skills learned in Chapter 5 on
Doxygen and create the GPIO stubs. An alternative to creating the
template yourself is to visit www.beningo.com and purchase the
templates developed by Jacob Beningo.
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.
Develop basic test cases based on the configuration table and HAL
input and output features. Verify that the ported code behaves as
expected.
Consider developing test-case document templates that will be used to
test ported GPIO code.
Investigate how regression testing could be used to automatically
verify that the HAL is working as expected. Inject an error into the
code and verify that the regression testing is able to catch the issue.
Footnotes
1 NXP KL25Z Sub-Family Reference Manual
“No sensible decision can be made any longer without taking into
account not only the world as it is, but the world as it will be.”
—Isaac Asimov
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
*
* @param[in] Config is a pointer to the
configuration table that contains
* the initialization
for the peripheral.
*
* @return void
*
* \b Example:
* @code
* const SpiConfig_t *SpiConfig =
Spi_ConfigGet();
*
* Spi_Init(SpiConfig);
* @endcode
*
* @see Spi_ConfigGet
* @see Spi_Init
* @see Spi_Transfer
* @see Spi_RegisterWrite
* @see Spi_RegisterRead
* @see Spi_CallbackRegister
*
************************************************
************/
void Spi_Init(SpiConfig_t const * const Config)
{
}
Listing 8-1 SPI Init Function Template
/***********************************************
***********************
* Function : Spi_Transfer()
*//**
* \b Description:
*
* This function is used to initialize a data
transfer on the SPI bus.
*
* PRE-CONDITION: Spi_Init must be called with
valid configuration data
* PRE-CONDITION: SpiTransfer_t must be
configured for the device
* PRE-CONDITION: The MCU clocks must be
configured and enabled.
*
* POST-CONDITION: Data transferred based on
configuration
*
* @param[in] Config is a configured
structure describing the data
* transfer that occurs.
*
* @return void
*
* \b Example:
* @code
* const SpiConfig_t *SpiConfig =
Spi_ConfigGet();
*
* Spi_Init(SpiConfig);
* Spi_Transfer(AccelerometerConfig);
*
* @endcode
*
* @see Spi_ConfigGet
* @see Spi_Init
* @see Spi_Transfer
* @see Spi_RegisterWrite
* @see Spi_RegisterRead
* @see Spi_CallbackRegister
*
************************************************
**********************/
void Spi_Transfer(SpiTransfer_t const *
const Config)
{
}
Listing 8-2 SPI Transfer Function Template
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 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
};
/**
* 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
};
Listing 8-3 Example SPI Pointer-Array Mapping
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.
/****************************************
***********************
* 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;
}
}
*spibuf[Config->SpiChannel] = (*(Config-
>TxRxData + j));
Mcu_TimeoutStart(INTERVAL_10MS);
while(*spistat[Config->SpiChannel] &
REGBIT7 == 0)
{
if(Mcu_TimeoutCheck() == 1)
{
Fault_StateSet(FAULT_SPI_RECEIVE);
break;
}
*(Config->TxRxData + j) =
*spibuf[Config->SpiChannel];
} // End for
/***************************************
************************
* 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);
}
Listing 8-5 Example Spi_Transfer Function
Going 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.
Identify at least three different microcontrollers that you are currently
working with or are interested in working with. Collect the SPI
peripheral datasheets for each microcontroller.
Review the datasheets in detail and generate a peripheral feature list
like the one shown in Table 8-1. How do the results compare? Are they
the same or do they have new peripheral features beyond what we
discussed in this chapter?
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
Create a document template using the skills learned in Chapter 5 on
Doxygen and create the SPI stubs. An alternative to creating the
template yourself is to visit www.beningo.com and purchase the
templates developed by Jacob Beningo.
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.
Consider developing test-case document templates that will be used to
test ported SPI code.
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.
Footnotes
1 NXP KL25Z Sub-Family Reference Manual
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.
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.
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.
At this point, the base HAL is in place and we are ready to start building
the documentation and software stubs.
/***********************************************
***********************
* 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
*
* @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!
}
Listing 9-1 Example EEPROM Init HAL Documentation
/***********************************************
***********************
* 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
* 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_Read(Buffer, 0x0, 8);
* @endcode
*
* @see Eeprom_ConfigGet
* @see Eeprom_Init
* @see Eeprom_Read
* @see Eeprom_Write
* @see Eeprom_RegisterWrite
* @see Eeprom_RegisterRead
************************************************
**********************/
void Eeprom_Read(uint8_t *Dest, uint32_t Src,
uint32_t Size)
{
// Enter Read code here!
}
Listing 9-2 Example EEPROM Read HAL Documentation
/***********************************************
***********************
* 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
*
* 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
************************************************
**********************/
Listing 9-3 Example EEPROM Write HAL Documentation
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.
// Setup Command
EepromConfig.TxRxData[0] = EEPROM_WRITE;
EepromConfig.TxRxData[1] = ((Dest & 0xFFFFFF)
>> 16);
EepromConfig.TxRxData[2] = ((Dest & 0xFFFF) >>
8);
EepromConfig.TxRxData[3] = (Dest & 0xFF);
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:
Source address is valid.
Destination address is valid.
Data size is valid.
Check for write errors.
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.
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.
Going 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.
Identify at least three EEPROM devices that you are interested in
working with. Collect the datasheets and begin following the seven
HAL design steps that we have been discussing. If you want to make
things interesting, select devices in the following categories:
three external EEPROM and at least one microcontroller with
internal EEPROM
three external Flash devices and at least three microcontrollers
that have internal flash controllers
Review the datasheets in detail and generate a peripheral feature list
like the one shown in Table 9-1. How do the results compare? Are they
the same or do they have new peripheral features beyond what we
discussed in this chapter?
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.
Create a documented template using the skills learned in Chapter 5 on
Doxygen and create the EEPROM and flash stubs. An alternative to
creating the template yourself is to visit www.beningo.com and
purchase the templates developed by Jacob Beningo.
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.
Develop basic test cases based on the configuration table and HAL
input and output features. Verify that the ported code behaves as
expected.
Consider developing test-case document templates that will be used to
test ported EEPROM and flash code.
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.
Footnotes
1 Microchip 25AA160D, 16 kb EEPROM, https://www.digikey.com/product-
detail/en/microchip-technology/25AA160D-I-ST/25AA160D-I-ST-
ND/2125495
SOFTWARE TERMINOLOGY
An application framework is a collection of different components, a set
of APIs, that are interrelated and assist a developer in rapidly developing
an application.
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.
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.
Common Software Frameworks—FAT File System
Another component that a developer can leverage and that they probably
wouldn’t want to create themselves is a FAT file-system component. FAT
file systems are often used on embedded systems to store log data or files
on either an SD card, an external memory device, or sometimes even on
internal flash memory. There are many different FAT file-system
components available if one does a quick internet search. One particular
component that has gained traction and a big following in the embedded
space is FatFS.4
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 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.
Go online and review some common open source software. Evaluate
how well that software provides the following:
Appropriate APIs
Software architecture
Speed that support is provided for
Software-development process
Testing procedures
Review the APIs from different RTOS suppliers. Which APIs seem to
be the easiest to use and remember?
Review your own software and identify common software features that
could easily be converted into their own separate reusable software
governed by a simple set of APIs.
Implement those features as a reusable component and start building
your own libraries and frameworks.
Examine the software components that are open source and
microcontroller-vendor-specific that we discussed in this chapter. Then
do the following:
Identify the best practices used in each.
Determine what could be done better.
Footnotes
1 https://www.renesas.com/en-us/products/synergy/features.html
2 http://www.webopedia.com/TERM/L/library.html
3 http://www.microchip.com/mplab/mplab-harmony
4 http://elm-chan.org/fsw/ff/00index_e.html
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_11
“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.
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 for proper operation.1 For
firmware engineers, a unit is an individual function. As engineers develop
their functions, they should also be developing test cases that will validate
the functions work as expected.
A unit test should test the function by validating that the range of
possible inputs to the function produce known and expected outputs. Unit
tests should also include inputs that are known to be invalid to ensure that
the function can handle errors appropriately. Figure 11-1 shows at a high
level how a function would be tested.
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
Unit testing is a software-development process in which the smallest
testable parts of an application are individually and independently
scrutinized for proper operation.1
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.
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.
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();
}
else
{
Code();
}
}
Listing 11-2 Cyclomatic Complexity, Three Functions with Four Paths
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
Visual Studio IDE (built-in)
Understand IDE (built-in)
Functional 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.
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.
Test-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:
It is verified that every test case can detect a failed state.
Test cases are created incrementally for every piece of code that is
written.
Adding new code that breaks previously written code is immediately
detected.
A test harness is used that allows for easy regression testing.
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.
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:
Needing to create mocks to simulate hardware accesses
Setting up the development environment is time-consuming and tricky
Adopting the mindset and truly following TDD is difficult
The process can feel very time-consuming
Despite these disadvantages, developers may still want to investigate
TDD and determine which pieces could work best for their reusable
firmware.
Regression 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.
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
using a C/C++ test harness; and
creating a Python-based test harness.
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
they are open source;
developers already know C/C++; and
they can be used to create automated tests.
There are several disadvantages as well, including the following:
Being open source, there is limited support to get them up and running.
I have found that they are difficult to set up initially.
Python test harnesses can be very interesting to developers as an
alternative to a C/C++ harness. I have found them to be more flexible for
system-level testing, similar to what we discussed in the section on in-line
hardware testing. Python is an easy-to-learn scripting language that is very
powerful. It also includes libraries specifically designed to perform testing.
The direction that any team chooses to go will be highly dependent on
their skillsets and their end requirements. It may also depend on when their
products are due and how much time and budget they have allocated for
testing. One thing is certain though; if you are planning to create reusable
firmware, you need to have automated tests to ensure the software
continues to behave as you expect it to.
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
Giving and taking objects such as semaphores
Current status of all tasks
This information can be used to dramatically improve the verification
process involved with reusable firmware .
Figure 11-5 Manually inspecting event data that reveals a deadlock
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
Verified against best practices and coding standards
Compiled under multiple toolchains
Tested on target hardware through the following:
Unit tests
Functional tests
Regression tests
Tested in a software harness that performs the following:
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.
Going 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:
Review McCabe’s12 white paper on using cyclomatic complexity for
testing, located at http://www.mccabe.com/pdf/mccabe-
nist235r.pdf .
Identify a cyclomatic complexity calculator and run it on your own
code base.
Reduce the complexity of functions with a value greater than 10 as it
makes sense.
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.
Invest in a copy of James Grenning’s book Test-Driven Development
for Embedded C. The book has great content, but be warned the
examples are a bit strenuous to set up and complete.
Review your development kit or product under development and list
out what would be necessary to perform hardware in-loop testing.
Download and set up Segger’s SystemView trace tool along with
Percepio’s Tracealyzer. Become familiar with how to set up, automate,
and use these trace tools.
Review the Renesas Synergy™ Platform along with the Renesas
Synergy™ Quality Handbook.
Footnotes
1 http://searchsoftwarequality.techtarget.com/definition/unit-
testing
2 McCabe, Thomas Jr. “Software Quality Metrics to Identify Risk.” Presentation to the Department
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.
3 http://www.ironiacorp.com/
4 https://stackoverflow.com/questions/24191174/cyclomatic-
complexity-1-if-statements
5 http://gmetrics.sourceforge.net/gmetrics-
CyclomaticComplexityMetric.html
6 https://msquaredtechnologies.com/
7 http://www.ldra.com/en/
8 Grenning, James (2011). Test-Driven Development for Embedded C, The Pragmatic Programmers.
9 https://www.techopedia.com/definition/19509/functional-testing
10 https://en.wikipedia.org/wiki/Regression_testing
12 http://www.mccabe.com/pdf/mccabe-nist235r.pdf
© Jacob Beningo 2017
Jacob Beningo, Reusable Firmware Development, https://doi.org/10.1007/978-1-4842-3297-2_12
3) Define a roadmap to get from where you currently are to the desired
result.
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.
5) Paste the change log into the commit comments and add any additional
relevant comments.
1) I would use the right tools for the job no matter the cost.
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.
Going 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:
Consider purchasing my API Standard2 book, which provides a
Doxygen-documented starting point for many microcontroller
peripheral features and provides the Doxygen template source code
with it.
Determine whether this will be a personal development effort to start
developing more reusable firmware or whether this is a team effort that
will have management support. Get the key players and decision
makers on board.
Identify three potential areas to immediately improve in. What is your
company’s low-hanging fruit? Could it be:
Implementing Doxygen templates for readability?
Leveraging the APIs and HALs in this book?
Identifying design patterns used in your products?
Once you have your top three priorities, rank each priority and review
how well you are currently doing in this area.
Create a roadmap of how these three priorities will be implemented in
the next several months and what needs to happen in order to be
successful. Don’t forget that this doesn’t need to be a detailed, formal
plan.
Identify metrics that need to be tracked in order to monitor the
improvements and also the results that they are getting for the
company.
Schedule reviews to monitor progress; adjust the roadmap and plan if
necessary.
Review your products and identify common design elements and
procedures that could be turned into templates and checklists.
Schedule time to convert these design patterns and procedures.
Calculate the cost in opportunity, project delays, troubleshooting, and
development costs that doing nothing could incur.
Enjoy developing reusable firmware and improving the products that
you work on.
Footnotes
1 https://www.beningo.com/tools-embedded-software-start-up-
checklist/
2 https://www.beningo.com/store/an-api-standard-for-mcus/
Index
A
Abstract Data Types (ADTs)
abstractions
definition
implementation data structure
initialization function
interface specification
operations
pop method
stack method initialization
Stack_Push
Abstractions
See Abstract Data Types (ADTs)
Application Programming Interfaces (APIs)
architecture
characteristics
consistent look and feel
const keyword
documentation
flexible and configuration
Micrium uc/OS-III
naming conventions
uOS III
comparison (API and HAL)
designing process
embedded-software developers
FreeRTOS TaskCreate
HAL design
scope
ThreadX tx_thread_create
wrappers
Assertion fundamentals
assert.h header file
definition
input and pre-condition
macro implementation
Automating tests
B
Boogeyman
integration issues
issues
microcontroller vendors
peripheral technique
ramifications
readability issues
Bootloaders framework
C
Callback functions
ArrayInit function
definition
elements to random numbers
implementation
initialization code
instances
lower-level code
signal handler
Classes definition
Cohesion
Commercial off-the-shelf (COTS)
Coupling method
C programming language
bit fields
conditional compilation
data type
demonstration code
preprocessor directives
structures and unions
D
Data hiding
Designing API
application framework
creation
embedded applications
advantages
application framework
disadvantages
hardware abstraction layer
implementation
modifications
modules
software frameworks
bootloaders
console applications
FAT file system
parsers
RTOS and schedulers
Design patterns
Device driver models
blocking driver
non-blocking driver
polling
Documentation
C code
coding style guide
commenting code
consistent comment location
Doxygen tags
explanation
file header
line command
mathematical type identification
template creation
update comments
Doxygen
See Doxygen
DoxyWizard
diagrams setup
folder structure
mode setup
output setup
project setup
run tab
wizard tab
embedded software
enum and struct
functions
code block
description block
factors
parameter and return block
pre-condition/post-condition block
related block
revision log
start block
load operation
approaches
single source
software spectrum
main.c file
main page
modules
@Addtogroup comment block
header file
source files
reusable template
Doxygen
comment fundamentals
control and develop documentation
installation
Drivers
abstraction and ADT
component definition
component organization
components
expected results and recommendations
files
fundamental unit
interface
component identification
design contract
hardware abstraction layer
lasagna software architecture
outputs
pre-conditions
modules
naming convention
object-oriented programming
procedural language
E
EEPROM devices,
See also Memory devices
datasheet
EepromErase_t
EepromRegister_t definition
extending HAL
_ext file
feature comparison
files
interface
memory devices
repeat
stubs and documentation templates
functions
Init()
Read()
Write()
target processor
functions
initialization function
read function
write function
testing
write state enumeration
Embedded-software processes/code base
Encapsulation
Error handling
F
FAT file systems
Firmware project
advantages
benefits
code reuse
development team
disadvantages
embedded-software
architecture
dependencies and interactions
design/reuse
formal models
functional boundary
interfaces
low-level driver
portable firmware creation
three-layer model
features
HAL
See Hardware Abstraction Layers (HAL)
microcontrollers
modularity
module coupling and cohesion
project development time
portability issues
See C programming language
qualities of
software
smart solar panel
standard revisions
Functional testing
black-box/white-box testing methods
test-driven development
testing process
G
General-purpose input/output (GPIO)
datasheet
HAL interface
microcontrollers
overview
peripheral features
stubs and documentation templates
Dio.c
Dio_Config.c
Dio_Config.h
Dio.h
HAL organization
target processor
ChannelRead
ChannelWrite
Dio_ChannelToggle function
Dio_ChannelWrite function
GPIO initialization
pointer array memory map
RegisterRead
RegisterWrite
repeat option
test harnesses
H
Hardware abstraction layer (HAL)
application layer
APIs
See also Application Programming Interfaces (APIs)
architecture
board-support package
benefits
characteristics
C99
coding standards
debugging software
deterministic and well-understood behavior
error-handling and diagnostic capabilities
evaluation
extensible
hardware features
integrated regression testing
integration server
modern compiler
modular and adaptable
reasonable documentation and comments
well-defined coding standard
configuration layer
comparison list
design process
all-encompassing HAL
core features identification
Doxygen
initialization
iterate
multiple development kits
naming conventions
register-access hooks
second set of eyes
view
driver layer
factors
Good, Bad, and Ugly
GPIO peripheral
interface
callback interface
creation
developers
generic definition
peripheral features
landscape
microcontroller peripheral datasheet
middleware
peripheral identification
platform
potential issues
See Boogeyman
software terminology
stubs and documentation templates
target processor(s)
testing
Hardware in-loop (HIL) testing
automating tests
COMM port
components
debugger
factors
Python scripts
regression
I, J, K, L
Inheritance
Internet of Things (IoT)
Invariants
M, N
Memory devices
flash and EEPROM devices
internal and external devices
issues
overview
Memory map
CPU
EEPROM
flash memory regions
generic microcontroller memory
memory
microcontroller
peripheral memory
RAM
ROM
Memory-mapping methodologies
arrays
controls
declaration
direct register access
methods
non-constant pointer
pointers
register bit
structures
volatile keyword
Module coupling
O
Object-Oriented Programming (OOP)
Objects definition
P, Q
Polling vs . Interrupt-driven drivers
attitude determination and control
DMA-controlled data transfer
Hello World
interrupts
printf statement
transmit interrupt frequency
UART transmit interrupt duration
Portable firmware
See Firmware project
characteristics
code evaluation
portability
reuse software
Post-conditions
Practical approach
definition
desired results and outcomes
business perspective, management and shareholders
development costs
identification
quality increases
time to market
evaluation
metrics
phases
recognizing design patterns
results
software practice improvement
templates and checklists creation
tracking metrics
unpractical environment
VCS
See Version-control systems (VCS)
Pre-conditions
Project organization
R
Real-Time Operating System (RTOS)
advantages
compiler optimizations
microchip
scheduler
scheduling algorithm
wrapper layer
Regression testing
Renesas Synergy™ platform
Reusable drivers
const keyword
extern and static keywords
explicit
function and variable scope
global variables
implicit
programming language
implementation
memory-mapping
See Memory-mapping methodologies
timer
See Timer driver
volatile keyword
location
optimization
prevent code optimization
UART Tx
S
Scheduler
See Real-Time Operating System (RTOS)
Serial Peripheral Interface bus (SPI)
advantages
architecture
datasheet
features
hardware level
interface
repeat
stubs and documentation templates
design patterns
init function
module files
transfer function
target processor
array mapping
flow chart
initialization function
Spi_Transfer function
testing
Side effects
Standard tests
T
Test-Driven Development (TDD)
Testing
application software
block diagram
deadlock
events
reusable firmware
task statistics
automation and regression
development teams
embedded system
functional testing
See Functional testing
HIL testing
regression testing
renesas Synergy™ platform
standard tests
unit test
Timer driver
channel definition
configuration structure
configuration table
design pattern
driver interface
initialization function
init loop code
overview
peripheral channels
pointer arrays
steps
U
Unit testing
cyclomatic complexity
function
if/else statements
linearly independent paths
measurements
nodes (program statements)
parameters
tools
function
harness test
V, W, X, Y, Z
Version-control systems (VCS)
add files
code-comparison tools
commit frequently
log information
process definition
lock modules
merging code branches