Topic Notes
Topic Notes
Embedded
Software
Topic Notes
2020
RDRF set InChar
Read data
from input
FIFO_Put FIFO_Get
yes yes
FIFO FIFO
full? empty?
no no
Put FIFO Get
buffer
Return data
Error to caller Error
rti return
producer
consumer
RxFIFO_Get RxFIFO_Put RDRF
SCI_InChar RxFIFO SCI
Set input
packet
consumer
producer
TxFIFO_Put TxFIFO_Get TDRE SCI
SCI_OutChar TxFIFO output
Set
SCI
interrupt
or polling
PMcL
Preface
These notes comprise part of the learning material for 48434 Embedded
Software. They are not a complete set of notes. Extra material and examples
may also be presented in the face-to-face activities.
These notes are hyperlinked. All green text is a link to somewhere else within
this document. For example, the contents page links to the appropriate page in
the text, and the page numbers in the header on each page link back to the
contents page. There are also some internal linked words that take you to the
relevant text.
Links to external web pages are red in colour. Provided your PDF reader (e.g.
Adobe Acrobat Reader) is set up correctly these links should open the
appropriate page in your web browser.
Contact
If you discover any errors or feel that some sections need clarifying, please do
not hesitate in contacting me:
Peter McLean
School of Electrical, Mechanical and Mechatronic Systems
Faculty of Engineering and Information Technology
University of Technology Sydney
Contents
1 Embedded Systems
Introduction ..................................................................................................... 1.2
1.1 Embedded Systems Characteristics ........................................................... 1.5
1.2 Microcontrollers ........................................................................................ 1.5
1.3 Development Boards ................................................................................. 1.6
1.3.1 The NXP FRDM-K64F ................................................................ 1.7
1.3.2 The Kinetis MK64FN1M0VLL12 Microcontroller Unit (MCU) 1.8
1.4 ARM Microcontrollers ............................................................................ 1.13
1.4.1 The ARM® Cortex®-M Series of Processors .............................. 1.14
1.5 Embedded Systems Programming Languages ........................................ 1.16
1.6 The Apollo Guidance Computer (AGC) ................................................. 1.18
Hardware ................................................................................................... 1.20
Software .................................................................................................... 1.23
Margaret Hamilton .................................................................................... 1.25
Legacy .................................................................................................... 1.26
References ................................................................................................. 1.27
2 Embedded C
Introduction ..................................................................................................... 2.4
2.1 Program Structure ...................................................................................... 2.4
2.1.1 Case Study 1: Microcomputer-Based Lock .................................. 2.4
2.1.2 Case Study 2: A Serial Port K64 Program ................................... 2.7
2.1.3 Free field language ....................................................................... 2.9
2.1.4 Precedence .................................................................................. 2.16
2.1.5 Associativity ............................................................................... 2.17
2.1.6 Comments ................................................................................... 2.18
2.1.7 Preprocessor Directives .............................................................. 2.19
2.1.8 Global Declarations .................................................................... 2.20
2.1.9 Declarations and Definitions ...................................................... 2.20
2.1.10 Functions .................................................................................... 2.23
2.1.11 Compound Statements ................................................................ 2.26
2.1.12 Global Variables ......................................................................... 2.27
2.1.13 Local Variables ........................................................................... 2.29
2.1.14 Source Files ................................................................................ 2.30
PMcL Index
2020 Contents
ii
2.2 Tokens ...................................................................................................... 2.33
2.2.1 ASCII Character Set.................................................................... 2.34
2.2.2 Literals ........................................................................................ 2.35
2.2.3 Keywords .................................................................................... 2.36
2.2.4 Names.......................................................................................... 2.37
2.2.5 Punctuation.................................................................................. 2.40
2.2.6 Operators ..................................................................................... 2.45
2.3 Numbers, Characters and Strings ............................................................. 2.46
2.3.1 Binary representation .................................................................. 2.46
2.3.2 8-bit unsigned numbers ............................................................... 2.48
2.3.3 8-bit signed numbers ................................................................... 2.51
2.3.4 16 bit unsigned numbers ............................................................. 2.54
2.3.5 16-bit signed numbers ................................................................. 2.55
2.3.6 Typedefs for Signed and Unsigned Data Types.......................... 2.56
2.3.7 Big- and Little-Endian ................................................................ 2.57
2.3.8 Boolean information ................................................................... 2.59
2.3.9 Decimal Numbers ....................................................................... 2.59
2.3.10 Octal Numbers ............................................................................ 2.62
2.3.11 Hexadecimal Numbers ................................................................ 2.63
2.3.12 Character Literals ........................................................................ 2.65
2.3.13 String Literals .............................................................................. 2.66
2.3.14 Escape Sequences ....................................................................... 2.67
2.4 Variables and Constants ........................................................................... 2.69
2.4.1 Statics .......................................................................................... 2.70
2.4.2 Volatile ........................................................................................ 2.74
2.4.3 Automatics .................................................................................. 2.77
2.4.4 Implementation of Automatic Variables ..................................... 2.78
2.4.5 Implementation of Constant Locals ............................................ 2.84
2.4.6 Externals...................................................................................... 2.85
2.4.7 Scope ........................................................................................... 2.86
2.4.8 Declarations ................................................................................ 2.88
2.4.9 Character Variables ..................................................................... 2.91
2.4.10 Mixing Signed and Unsigned Variables ..................................... 2.91
2.4.11 When Do We Use Automatics Versus Statics? .......................... 2.92
2.4.12 Initialization of variables and constants ...................................... 2.93
2.4.13 Implementation of the initialization ............................................ 2.95
2.4.14 Summary of Variable Attributes ................................................. 2.97
2.4.15 Summary of Variable Lifetimes .................................................. 2.97
2.5 Expressions .............................................................................................. 2.98
2.5.1 Precedence and Associativity...................................................... 2.98
2.5.2 Unary operators ......................................................................... 2.100
2.5.3 Binary operators ........................................................................ 2.101
2.5.4 Assignment Operators ............................................................... 2.104
2.5.5 Expression Types and Explicit Casting .................................... 2.105
2.5.6 Selection operator ..................................................................... 2.108
2.5.7 Arithmetic Overflow and Underflow ........................................ 2.109
Index PMcL
Contents 2020
iii
2.6 Procedural Statements ........................................................................... 2.119
2.6.1 Simple Statements .................................................................... 2.120
2.6.2 Compound Statements .............................................................. 2.121
2.6.3 The if Statement ...................................................................... 2.122
2.6.4 The switch Statement ............................................................. 2.124
2.6.5 The while Statement ............................................................... 2.127
2.6.6 The for Statement ................................................................... 2.129
2.6.7 The do Statement ...................................................................... 2.131
2.6.8 The return Statement ............................................................. 2.132
2.6.9 Null Statements......................................................................... 2.133
2.6.10 The goto Statement ................................................................. 2.134
2.6.11 Missing Statements ................................................................... 2.135
2.7 Pointers .................................................................................................. 2.136
2.7.1 Addresses and Pointers ............................................................. 2.136
2.7.2 Pointer Declarations.................................................................. 2.137
2.7.3 Pointer Referencing .................................................................. 2.138
2.7.4 Memory Addressing ................................................................. 2.146
2.7.5 Pointer Arithmetic .................................................................... 2.149
2.7.6 Pointer Comparisons................................................................. 2.150
2.7.7 A FIFO Queue Example ........................................................... 2.151
2.7.8 I/O Port Access ......................................................................... 2.158
2.8 Arrays and Strings ................................................................................. 2.160
2.8.1 Array Subscripts ....................................................................... 2.160
2.8.2 Array Declarations .................................................................... 2.162
2.8.3 Array References ...................................................................... 2.163
2.8.4 Pointers and Array Names ........................................................ 2.163
2.8.5 Negative Subscripts .................................................................. 2.164
2.8.6 Address Arithmetic ................................................................... 2.165
2.8.7 String functions in string.h ................................................ 2.166
2.8.8 A FIFO Queue Example using Indices ..................................... 2.173
2.9 Structures ............................................................................................... 2.175
2.9.1 Structure Declarations .............................................................. 2.175
2.9.2 Accessing Members of a Structure ........................................... 2.177
2.9.3 Initialization of a Structure ....................................................... 2.178
2.9.4 Using pointers to access structures ........................................... 2.180
2.9.5 Passing Structures to Functions ................................................ 2.182
2.9.6 Linear Linked Lists ................................................................... 2.183
2.9.7 Example of a Huffman Code .................................................... 2.188
PMcL Index
2020 Contents
iv
2.10 Functions .............................................................................................. 2.193
2.10.1 Function Declarations ............................................................... 2.195
2.10.2 Function Definitions ................................................................. 2.198
2.10.3 Function Calls ........................................................................... 2.201
2.10.4 Argument Passing ..................................................................... 2.203
2.10.5 Private versus Public Functions ................................................ 2.206
2.10.6 Finite State Machine using Function Pointers .......................... 2.207
2.10.7 Linked List Interpreter using Function Pointers ....................... 2.210
2.11 Preprocessor Directives ....................................................................... 2.212
2.11.1 Macro Processing ...................................................................... 2.212
2.11.2 Conditional Compiling.............................................................. 2.215
2.11.3 Including Other Source Files .................................................... 2.217
2.11.4 Implementation-Dependent Features ........................................ 2.218
2.12 Assembly Language Programming ...................................................... 2.219
2.12.1 How to Insert Single Assembly Instructions............................. 2.219
2.13 Hardware Abstraction Layers .............................................................. 2.221
Index PMcL
Contents 2020
v
5 Interrupts
Introduction ..................................................................................................... 5.2
5.1 Exceptions ................................................................................................. 5.2
5.2 Interrupts .................................................................................................... 5.3
5.2.1 Using Interrupts ............................................................................ 5.4
5.2.2 Interrupt Processing ...................................................................... 5.5
5.2.3 Interrupt Polling ............................................................................ 5.6
5.3 The Vector Table ....................................................................................... 5.7
5.4 Interrupt Service Routines (ISRs).............................................................. 5.8
5.4.1 Declaring Interrupt Service Routines in C for Generic Processors
...................................................................................................... 5.9
5.4.2 Declaring Interrupt Service Routines in C for ARM® Cortex®-M
Processors ................................................................................... 5.10
5.4.1 Specifying an ISR Address in the Vector Table ......................... 5.11
5.5 Enabling and Disabling Interrupts ........................................................... 5.12
5.5.1 Interrupt Latency ........................................................................ 5.13
5.6 Interrupt Priority ...................................................................................... 5.13
5.7 The Nested Vectored Interrupt Controller (NVIC) ................................. 5.14
5.7.1 Pending Status ............................................................................ 5.15
5.7.2 NVIC Registers for Interrupt Control ......................................... 5.16
5.8 Foreground / Background Threads .......................................................... 5.22
5.9 Serial Communication Interface using Interrupts .................................... 5.23
5.9.1 Output Device Interrupt Request on Transition to Ready .......... 5.24
5.9.2 Output Device Interrupt Request on Ready ................................ 5.26
5.10 Communicating Between Threads......................................................... 5.27
5.10.1 Critical Sections in C for the ARMv7-M ................................... 5.29
5.11 References ............................................................................................. 5.31
PMcL Index
2020 Contents
vi
7 Concurrent Software
Introduction ...................................................................................................... 7.2
7.1 Threads ....................................................................................................... 7.4
7.1.1 Thread Control Blocks (TCBs) ..................................................... 7.7
7.2 Schedulers .................................................................................................. 7.8
7.2.1 Other Scheduling Algorithms ..................................................... 7.10
7.3 The SysTick Timer .................................................................................. 7.11
7.4 A Simple Operating System ..................................................................... 7.12
7.5 The Semaphore ........................................................................................ 7.17
7.5.1 Mutual Exclusion with Semaphores ........................................... 7.18
7.5.2 Simple Mutual Exclusion ............................................................ 7.19
7.5.3 Priority Inversion in Real-Time Systems .................................... 7.19
7.5.4 Synchronisation using Semaphores ............................................ 7.20
7.5.5 The Producer / Consumer Problem using Semaphores ............... 7.21
8 Interfacing
Introduction ...................................................................................................... 8.2
8.1 Input Switches ............................................................................................ 8.2
8.1.1 Interfacing a Switch to the Microcontroller .................................. 8.2
8.1.2 Hardware Debouncing Using a Capacitor .................................... 8.4
8.1.3 Software Debouncing .................................................................. 8.10
8.2 Analog to Digital Conversion .................................................................. 8.21
8.2.1 ADC Module ............................................................................... 8.21
8.3 Digital to Analog Conversion .................................................................. 8.23
8.3.1 Pulse Width Modulator ............................................................... 8.24
9 Fixed-Point Processing
Introduction ...................................................................................................... 9.2
9.1 Q Notation .................................................................................................. 9.3
9.2 Other Notations .......................................................................................... 9.6
9.3 Fixed-Point Calculations ............................................................................ 9.7
9.3.1 Multiplication ................................................................................ 9.7
9.3.2 Division ......................................................................................... 9.8
9.3.3 Addition ........................................................................................ 9.8
9.3.4 Subtraction .................................................................................... 9.8
9.3.5 Fixed-Point Operations Using a Universal 32Q16 Notation ...... 9.12
9.4 Square Root Algorithm for a Fixed-Point Processor ............................... 9.15
9.4.1 Number of Iterations ................................................................... 9.18
Index PMcL
Contents 2020
vii
PMcL Index
2020 Contents
1.1
1 Embedded Systems
Contents
Introduction
Computing systems are everywhere. Billions of computing systems are built
every year that are embedded within larger electronic devices, repeatedly
carrying out a particular function, often going completely unrecognized by the
device’s user.
Examples of
embedded systems
Outdoors Indoors
1. Helicopter: control, navigation, 34. Cordless phone
communication , etc.
35. Coffee maker
2. Medicine administering systems
36. Rice cooker
3. Smart hospital bed with sensors and
37. Portable radio
communication
38. Programmable oven
4. Patient monitoring system
39. Microwave oven
5. Surgical displays
40. Smart refrigerator
6. Ventilator
41. In-home computer network router
7. Digital thermometer
42. Clothes dryer
8. Portable data entry systems
43. Clothes washing machine
9. Pacemaker
44. Portable MP3 player
10. Automatic door
45. Digital camera
11. Electric wheelchair
46. Electronic book
12. Smart briefcase with fingerprint
enabled lock 47. Garbage compactor
13. Ambulance: medical and 48. Hearing aid
communication equipment
49. Dishwasher
14. Automatic irrigation systems
50. Electronic clock
15. Jet aircraft: control, navigation,
51. Video camera
communication, autopilot, collision-
avoidance, in-flight entertainment, 52. Electronic wristwatch
passenger telephones, etc.
53. Pager
16. Laptop computer (contains embedded
systems) 54. Mobile phone
24. Car (engine control, cruise control, 62. TV-based Web access box
temperature control, music system, 63. House temperature control
anti-lock brakes, active suspension,
navigation, toll transponder, etc.) 64. Home alarm system
Nearly any device that runs on electricity either already has or soon will have a
computing system embedded within it. In 2015, 1.43 billion smart phones, 212
million tablet PCs and 20 million eReaders were shipped.1
1
http://www.idc.com/getdoc.jsp?containerId=prUS40664915
http://www.idc.com/getdoc.jsp?containerId=prUS25867215
(Accessed 2016-02-19)
2. Tightly constrained: Embedded systems often must cost just a few dollars,
must be sized to fit into compact spaces, must perform fast enough to process
data in real time, must consume minimum power, and must be designed
rapidly to capture market windows.
1.2 Microcontrollers
A microcontroller is an integrated circuit that has a microprocessor connected
up to various peripherals such as timers, serial ports, analog-to-digital
converters, etc. You can think of a microcontroller as a “system-on-a-chip”. An
embedded system is usually made from a microcontroller and associated
electronic circuitry that deals with interfacing the microcontroller to the “real
world”. The “art” of embedded systems programming is to write an application
that utilises the hardware peripherals and interacts with the outside world in a
manner which meets constraints 2 and 3 listed above. This requires a
rudimentary understanding of the microcontroller architecture, the nature of the
on-board peripherals, as well as understanding how the microcontroller
interfaces with the real world. Invariably this means that an embedded software
engineer must have a basic ability to interpret an electrical schematic diagram.
Various
microcontroller
development boards
2
The mebibit, abbreviated Mib, is a multiple of the unit “bit” used to quantify digital information.
It is a member of the set of units with binary prefixes defined by the International
Electrotechnical Commission (IEC). The prefix mebi (symbol Mi) represents 10242,
or 1 048 576.
http://www.arm.com/products/processors/cortex-m/cortex-m4-processor.php
The following figure shows the block diagram for the K64 family:
The Kinetis K64 MCU family offers low power and mixed-signal analog
integration for applications such as industrial control panels, navigational
displays, point-of-sale terminals, and medical monitoring equipment.
Communication Interfaces
Ethernet MAC with IEEE 1588 10/100 Mbps Ethernet MAC with hardware
capability (ENET) support for IEEE 1588.
USB On-The-Go (OTG) (low- USB 2.0 compliant module with support for
/full-speed) host, device, and On-The-Go modes.
USB Device Charger Detect Detects a smart charger meeting the USB
(USBDCD) Battery Charging Specification Rev 1.2.
USB voltage regulator Powers on-chip USB subsystem.
Controller Area Network (CAN) Supports the full implementation of the CAN
Specification Version 2.0, Part B.
Serial peripheral interface (SPI) Synchronous serial bus for communication to
an external device.
Inter-integrated circuit (I2C) Allows communication between a number of
devices.
Universal asynchronous receiver Asynchronous serial bus communication
/ transmitter (UART) interface with support for CEA709.1-B (Lon
works) and the ISO 7816 smart card interface.
Secure Digital host controller Interface between the host system and the SD,
(SDHC) SDIO, MMC, or CE-ATA cards.
Inter-IC Sound (I2S) Provides a synchronous audio interface (SAI)
that supports full duplex serial interfaces such
as AC97 and codec / DSP interfaces.
Human-Machine Interface
General purpose input/output General purpose pins.
(GPIO)
Table 1.1 – K64 Modules Grouped by Functional Categories
RISC is, in its broadest form, a design philosophy for processors. It stems from
a belief that a processor with a relatively simple instruction set will be more
efficient than one which is more complex. The term originally came into use
back in the 1980s with a research project called Berkeley RISC that investigated
the possibilities of this approach to design and then created processors based on
it.
All ARM processors are considered RISC designs. Processors that have a RISC
architecture typically require fewer transistors than those with a complex
instruction set computing (CISC) architecture (such as the x86 processors found
in most personal computers), which improves cost, power consumption, and heat
dissipation. These characteristics are desirable for light, portable, battery-
powered devices – including smartphones, laptops and tablet computers, and
other embedded systems.
A company called ARM Holdings is responsible for ARM, and it is only a design
company. They manage the instruction set and design new versions of the core
architecture and then license it to other companies. Those companies can then
improve it and pair it with whatever hardware seems appropriate.
http://www.arm.com/products/processors/cortex-m/index.php
The Cortex-M family is optimized for cost and power sensitive MCU and
mixed-signal devices for applications such as Internet of Things,
connectivity, motor control, smart metering, human interface devices,
automotive and industrial control systems, domestic household
appliances, consumer products and medical instrumentation.3
3
http://www.arm.com/products/processors/cortex-m/index.php (Accessed 2015-07-24)
In summary, there are many variants of ARM on the market and they all perform
differently. However, if you are familiar with the ARM core, you should be able
to move easily from chip to chip and vendor to vendor – this is the advantage of
learning about and using a chip from the ARM ecosystem.
Between 1991 and 2013, ARM shipped 50 billion units. By 2005, ARM was
producing a billion units per year, and by 2009 the company was shipping a
billion units every quarter. To date, ARM has 100 billion units in the market; by
2021, the company expects to have shipped 100 billion more.4
4
https://www.tomshardware.com/news/arm-dynamiq-multicore-microachitecture,33947.html
(Accessed 2018-04-26)
In all these respects, and for historical reasons, the C language is dominant in
embedded systems programming. An IEEE survey from 2019 shows the top 10
programming languages used in industry:
In this subject, we will be using just plain old C, and specifically C11 which is a
superseded international standard (ISO/IEC 9899:2011). C18 is now the current
standard (ISO/IEC 9899:2018), but compiler support in the “embedded world”
for this standard is minimal at this time.
If you are comfortable and proficient in the C language (i.e. you consider
yourself an advanced C programmer), you are welcome to program in C++.
Wikimedia Commons
You have to remember that when the Apollo program began, computers were
still gigantic machines that took up whole rooms. The task of the MIT team
(which ultimately expanded to over 400 engineers and programmers) was to
handle the complexity of space navigation with a digital computer that weighed
only 32 kg, consumed only 55 W of power, had the computing power of a
modern hand-held calculator and the dimensions of 61 x 32 x 17 cm.
For a taste of what the computers were asked to accomplish, consider the
workload of the lunar module’s AGC during a critical phase of the flight – the
powered descent to the Moon’s surface. The first task was navigation: measuring
the craft’s position, velocity, and orientation, then plotting a trajectory to the
target landing site. Data came from the gyroscopes and accelerometers of an
inertial guidance system, supplemented in the later stages of the descent by
readings from a radar altimeter that bounced signals off the Moon’s surface.
The lunar module
returning from the
moon to rendezvous
with the Apollo 11
command module
Wikimedia Commons
After calculating the desired trajectory, the AGC had to swivel the nozzle of the
rocket engine to keep the capsule on course. At the same time it had to adjust the
magnitude of the thrust to maintain the proper descent velocity. These guidance
and control tasks were particularly challenging because the module’s mass and
centre of gravity changed as fuel was consumed and because a spacecraft sitting
atop a plume of rocket exhaust is fundamentally unstable – like a broomstick
balanced upright on the palm of your hand.
Along with the primary tasks of navigation, guidance, and control, the AGC also
had to update instrument displays in the cockpit, respond to commands from the
astronauts, and manage data communications with ground stations. Such
multitasking is routine in computer systems today. In the early 1960s, however,
the tools and techniques for creating an interactive, “real-time” computing
environment were in a primitive state.
PMcL The Apollo Guidance Computer (AGC) Index
34 instructions
2048 words of erasable memory
36864 words of read-only memory
16-bit word format (15 data + 1 parity)
1.024 MHz clock
The hardware was built from just one type of logic gate – the 3-input NOR gate.
There were only 2800 integrated circuits (ICs) used in the design – each IC was
a dual 3-input NOR gate.
They were connected on the back of a flat panel via welds and wire wrap, and
then cast in epoxy resin.
Wikimedia Commons
The peripherals were fed directly into memory-mapped I/O channels that were
accessible only by special I/O instructions. There were 7 input channels and 14
output channels, all 16-bits wide.
The AGC also had a power-saving mode for use in midcourse flight, but this was
never used (it was left on full power for all phases of the mission).
The AGC system had an interface for the astronaut to communicate directly with
it. Engineer Ramon Alonso came up with a simple display and keyboard, named
the “DSKY” (pronounced DIS-kee), where two-digit numbers represent
programs, verbs and nouns. The astronaut punched data and commands into the
system. When the computer requested the astronaut to take some action, the
numbers would flash to attract attention.
NASA
The fact that the AGC had such limited computing power meant the software
had to be very special. When the design requirements for the AGC were defined,
software programming techniques did not exist so it had to be designed from
scratch. A real-time operating system (RTOS) was designed by Hal Laning, with
no prior examples to guide him. The AGC effectively prioritised certain “tasks”,
and let the “unimportant” ones languish whilst carrying out the vital control and
navigation tasks as the lunar module landed.
The bulk of the software was stored in read-only rope memory and thus couldn't
be changed in operation, but some key parts of the software were stored in
standard read-write magnetic-core memory and could be overwritten by the
astronauts using the DSKY interface.
Raytheon
During the Apollo 14 mission, a faulty abort switch would have caused an
aborted landing attempt when the lunar descent was begun. The hardware error
was detected shortly before descent. In less than 2 hours, the problem was
diagnosed and a software patch was developed, tested, and relayed to the
astronauts to key in by hand using the DSKY – resulting in a successful lunar
landing!
An interrupt system was implemented in the AGC via five “vectored interrupts”.
The AGC responded to each interrupt by temporarily suspending the current
program, executing a short interrupt service routine, and then resuming the
interrupted program.
The AGC also had a sophisticated software interpreter that implemented a virtual
machine with more complex and capable pseudo-instructions than the native
AGC. These instructions simplified the navigational programs. Interpreted code,
which featured double precision trigonometric, scalar and vector arithmetic (16
and 24-bit), and even a (matrix × vector) instruction, could be mixed with native
AGC code. While the execution time of the pseudo-instructions was increased
(due to the need to interpret these instructions at runtime) the interpreter
provided many more instructions (more than 100) than the AGC natively
supported (34). The use of pseudo-instructions lowered the memory (at that time
memory was very expensive) and eased the burden of programming complex
mathematical and logical operations.
Being new and untested technology, NASA was reluctant to let the AGC be the
primary means of control and navigation of the Command and Service Module
(CSM) spacecraft. Ultimately, primary navigation of the CSM was performed
using Earth-based radar systems during the majority of the moon mission (using
Doppler shift radar for position and velocity). However, data generated by the
AGC would provide critical navigational data to the crew about spacecraft
position, direction, velocity and acceleration when they were completely cut off
from ground radar and communications – while orbiting the far side of the moon.
The AGC was, however, absolutely essential for the Lunar Module (LM). It
carried programs for the three phases of landing – braking, approach, and final
descent. In the final descent (started between 300 to 150 m altitude), the LM
would be manually flown by the astronauts for about a minute. There was only
one attempt at landing – if anything went wrong, astronauts would have to hit an
abort button which would fire the ascent engines and return the LM to the
orbiting CSM.
At the time, the AGC was the latest and most advanced fly-by-wire and inertial
guidance system, the first digital flight computer, the most advanced miniature
computer, the first computer to use silicon chips, and the first on-board computer
where the lives of crew depended on it functioning as advertised.
By the end of the last Apollo mission in 1972, the AGC was hopelessly outdated.
But it had flown on 15 manned missions, including nine moon flights, six lunar
landings, and three Skylab missions. It was also used in experimental fly-by-
wire aircraft. It never failed.
The design of the AGC has a powerful human resonance, and the history of its
development offers a glimpse of the cultural milieu of a high-profile, high-risk,
high-stress engineering project. I encourage you to delve deeper into its history.
Hall, Eldon C: Journey to the Moon: The History of the Apollo Guidance
Computer, American Institute of Aeronautics and Astronautics, Inc., 1996.
https://en.wikipedia.org/wiki/Apollo_Guidance_Computer
(Accessed 2020-02-12)
https://www.americanscientist.org/article/moonshot-computing
(Accessed 2020-02-12)
https://tcf.pages.tcnj.edu/files/2013/12/Apollo-Guidance-Computer-2009.pdf
(Accessed 2020-02-12)
http://www.righto.com/2019/07/software-woven-into-wire-core-rope-and.html
(Accessed 2020-02-12)
2 Embedded C
Contents
2020 2 - Embedded C
2.2
2.4 Variables and Constants ........................................................................... 2.69
2.4.1 Statics .......................................................................................... 2.70
2.4.2 Volatile ........................................................................................ 2.74
2.4.3 Automatics .................................................................................. 2.77
2.4.4 Implementation of Automatic Variables ..................................... 2.78
2.4.5 Implementation of Constant Locals ............................................ 2.84
2.4.6 Externals...................................................................................... 2.85
2.4.7 Scope ........................................................................................... 2.86
2.4.8 Declarations ................................................................................ 2.88
2.4.9 Character Variables ..................................................................... 2.91
2.4.10 Mixing Signed and Unsigned Variables ..................................... 2.91
2.4.11 When Do We Use Automatics Versus Statics? .......................... 2.92
2.4.12 Initialization of variables and constants ...................................... 2.93
2.4.13 Implementation of the initialization ............................................ 2.95
2.4.14 Summary of Variable Attributes ................................................. 2.97
2.4.15 Summary of Variable Lifetimes .................................................. 2.97
2.5 Expressions .............................................................................................. 2.98
2.5.1 Precedence and Associativity...................................................... 2.98
2.5.2 Unary operators ......................................................................... 2.100
2.5.3 Binary operators ........................................................................ 2.101
2.5.4 Assignment Operators ............................................................... 2.104
2.5.5 Expression Types and Explicit Casting .................................... 2.105
2.5.6 Selection operator ..................................................................... 2.108
2.5.7 Arithmetic Overflow and Underflow ........................................ 2.109
2.6 Procedural Statements ............................................................................ 2.119
2.6.1 Simple Statements ..................................................................... 2.120
2.6.2 Compound Statements .............................................................. 2.121
2.6.3 The if Statement ...................................................................... 2.122
2.6.4 The switch Statement ............................................................. 2.124
2.6.5 The while Statement ............................................................... 2.127
2.6.6 The for Statement .................................................................... 2.129
2.6.7 The do Statement ...................................................................... 2.131
2.6.8 The return Statement ............................................................. 2.132
2.6.9 Null Statements ......................................................................... 2.133
2.6.10 The goto Statement .................................................................. 2.134
2.6.11 Missing Statements ................................................................... 2.135
2.7 Pointers .................................................................................................. 2.136
2.7.1 Addresses and Pointers ............................................................. 2.136
2.7.2 Pointer Declarations .................................................................. 2.137
2.7.3 Pointer Referencing................................................................... 2.138
2.7.4 Memory Addressing .................................................................. 2.146
2.7.5 Pointer Arithmetic ..................................................................... 2.149
2.7.6 Pointer Comparisons ................................................................. 2.150
2.7.7 A FIFO Queue Example ........................................................... 2.151
2.7.8 I/O Port Access ......................................................................... 2.158
2 - Embedded C 2020
2.3
2.8 Arrays and Strings ................................................................................. 2.160
2.8.1 Array Subscripts ....................................................................... 2.160
2.8.2 Array Declarations .................................................................... 2.162
2.8.3 Array References ...................................................................... 2.163
2.8.4 Pointers and Array Names ........................................................ 2.163
2.8.5 Negative Subscripts .................................................................. 2.164
2.8.6 Address Arithmetic ................................................................... 2.165
2.8.7 String functions in string.h ................................................ 2.166
2.8.8 A FIFO Queue Example using Indices ..................................... 2.173
2.9 Structures ............................................................................................... 2.175
2.9.1 Structure Declarations .............................................................. 2.175
2.9.2 Accessing Members of a Structure ........................................... 2.177
2.9.3 Initialization of a Structure ....................................................... 2.178
2.9.4 Using pointers to access structures ........................................... 2.180
2.9.5 Passing Structures to Functions ................................................ 2.182
2.9.6 Linear Linked Lists ................................................................... 2.183
2.9.7 Example of a Huffman Code .................................................... 2.188
2.10 Functions ............................................................................................. 2.193
2.10.1 Function Declarations ............................................................... 2.195
2.10.2 Function Definitions ................................................................. 2.198
2.10.3 Function Calls ........................................................................... 2.201
2.10.4 Argument Passing ..................................................................... 2.203
2.10.5 Private versus Public Functions ................................................ 2.206
2.10.6 Finite State Machine using Function Pointers .......................... 2.207
2.10.7 Linked List Interpreter using Function Pointers ....................... 2.210
2.11 Preprocessor Directives ....................................................................... 2.212
2.11.1 Macro Processing...................................................................... 2.212
2.11.2 Conditional Compiling ............................................................. 2.215
2.11.3 Including Other Source Files .................................................... 2.217
2.11.4 Implementation-Dependent Features ........................................ 2.218
2.12 Assembly Language Programming ..................................................... 2.219
2.12.1 How to Insert Single Assembly Instructions ............................ 2.219
2.13 Hardware Abstraction Layers .............................................................. 2.221
2020 2 - Embedded C
2.4
Introduction
This document gives a basic overview of programming in C for an embedded
system – based on a specific 32-bit ARM® processor, NXP’s “K64”.
K64
solenoid
PA7 +5V
PA6
PA5
PA4
PA3
PA2
PA1
PA0
If the 7-bit binary pattern on Port A bits 6-0 becomes 0100011 for at least
10 ms, then the solenoid will activate. The 10 ms delay will compensate for the
switch bounce. We see that Port A bits 6-0 are input signals to the computer and
Port A bit 7 is an output signal.
2 - Embedded C 2020
2.5
Before we write C code, we need to develop a software plan. Software
development is an iterative process. The steps below are listed in a 1, 2, 3, …
order, whereas in reality we iterate these steps over and over.
Iterative steps used
1. We begin with a list of the inputs and outputs. We specify the range of to write software
values and their significance. In this example we will use GPIOA. Bits 6-
0 will be inputs. The 7 input signals represent an unsigned integer from
0 to 127. Port A bit 7 will be an output. If GPIOA bit 7 is 1 then the
solenoid will activate and the door will be unlocked. In C we use #define
macros to assign symbolic names, GPIOA_PSOR, GPIOA_PCOR and
GPIOA_PDDR, to the corresponding addresses of these registers,
0x400FF004, 0x400FF008 and 0x400FF014. Accessing
microcontroller ports
#define GPIOA_PSOR *(uint32_t volatile *)(0x400FF004) in C
#define GPIOA_PCOR *(uint32_t volatile *)(0x400FF008)
#define GPIOA_PDDR *(uint32_t volatile *)(0x400FF014)
2. Next, we make a list of the required data structures. Data structures are
used to save information. If the data needs to be permanent, then it is
allocated in global space. If the software will change its value then it will
be allocated in RAM. In this example we need a 16-bit unsigned counter.
uint16_t Count;
If a data structure can be defined at compile time and will remain fixed,
then it can be allocated in Flash memory. In this example we will define
an 8-bit fixed constant to hold the key code, which the operator needs to
set to unlock the door. The compiler will place these lines with the
program so that they will be defined in Flash memory.
It is not clear at this point exactly where in Flash this constant will be,
but luckily for us, the compiler will calculate the exact address
automatically. After the program is compiled, we can look in the listing
file or in the map file to see where in memory each structure is allocated.
2020 2 - Embedded C
2.6
3. Next we develop the software algorithm, which is a sequence of
operations we wish to execute. There are many approaches to describing
the algorithm. Experienced programmers can develop the algorithm
directly in the C language. On the other hand, most of us need an abstract
method to document the desired sequence of actions. Flowcharts and
pseudo-code are two common descriptive formats. There are no formal
rules regarding pseudo-code, rather it is a shorthand for describing what
to do and when to do it. We can place our pseudo-code as documentation
into the comment fields of our program. The following figure shows a
flowchart on the left and pseudo-code and C code on the right for our
digital lock example.
Psuedo Code
A flowchart and
1) initialize port A
corresponding PA6-PA0 inputs
pseudo-code and C PA7 output
2) turn off solenoid
code main
3) set counter to 4000
4) repeat indefinitely
Initialize ports if switch matches key then
a) if counter > 0 then
decrement counter
Solenoid = off else
turn on solenoid
else
Count = 4000 a) turn off solenoid
b) set counter to 4000
different C Code
switches
InitPortA();
GPIOA_PCOR = 0x80;
Solenoid = off match key Count = 4000;
while (1)
>0 {
Count Count = Count - 1 if ((GPIOA_PDIR & 0x7f) == KEY)
Count = 4000 {
if (Count)
0 Count--;
else
Solenoid = on GPIOA_PSOR = 0x80;
}
else
{
GPIOA_PCOR = 0x80;
Count = 4000;
}
}
2 - Embedded C 2020
2.7
2.1.2 Case Study 2: A Serial Port K64 Program
Let's begin with a small program. This simple program is typical of the
operations we perform in an embedded system. This program will read 8-bit data
from parallel port C and transmit the information in serial fashion using the
Universal Asynchronous Receiver/Transmitter number 2 (UART2). The
numbers in the first column are not part of the software, but have been added to
simplify our discussion.
Note: This program is vastly simplified and will not run on a K64.
PMcL Program Structure Index
2020 2 - Embedded C
2.8
The first line of the program is a comment giving a brief description of its
function. Lines 3 through 8 define macros that provide programming access to
ports and registers of the K64. These macros specify the format (unsigned 8, 16
or 32 bit) and address (the K64 employs memory mapped I/O). For example, in
line 3 the #define invokes the preprocessor to replace each instance of
GPIOC_PDIR with *(uint32_t volatile *)(0x400FF090).
Lines 11-17 define a function or procedure that when executed will initialize the
UART port. The assignment statement is of the form value at address = data.
In particular line 14 (UART2_BD = 0x34;) will output a hexadecimal 0x34 to I/O
configuration register at location 0x4006C000. Similarly line 16 will output a
hexadecimal 0x0C to I/O configuration register at location 0x4006C003. Notice
that comments can be added virtually anywhere in order to clarify the software
function. UART_Init is an example of a function that is executed only once at the
beginning of the program.
Line 9 is another #define that specifies the transmit data ready empty (TDRE) bit
as bit 7. This #define illustrates the usage of macros that make the software more
readable. Line 19 is a comment. Lines 20-26 define another function, UART_Out,
having an 8-bit input parameter that when executed will output the data to the
UART2 port. In particular line 23 will read the UART2 status register at
0x4006C004 over and over again until bit 7 (TDRE) is set. Once TDRE is set, it is
safe to start another serial output transmission. This is an example of I/O polling.
Line 25 copies the input parameter, data, to the serial port, starting a serial
transmission. Line 25 is an example of an I/O output operation.
Lines 28 through 42 define the main program. After some brief initialization this
is where the software will start after a reset or after being powered up. The
sequence unsigned char info in line 30 will define a local variable. Notice that
the size (char means 8-bit), type (unsigned) and name (info) are specified. Line
32 calls the initialization function UART_Init. Line 34 writes a 0 to the I/O
configuration register at 0x400ff094, specifying all 32 bits of PORTC will be inputs
(writing ones to a direction register specifies the bits as outputs). The sequence
while (1) {...} defines a control structure that executes forever and never
2 - Embedded C 2020
2.9
finishes. In particular lines 37 to 40 are repeated over and over without end. Most
software on embedded systems will run forever (or until the power is removed).
Line 38 will read Port C and copy the voltage levels into the variable info. This
is an example of an I/O input operation. Each of the lower 8 lines of the 32-bit
PORTC corresponds to one of the 8 bits of the variable info. A digital logic high
(a voltage above 2 V), is translated into a 1. A digital logic low (a voltage less
than 0.7 V) is translated into a 0. Line 40 will execute the function UART_Out that
will transmit the 8-bit data via the UART2 serial port.
With the MCUXpresso (MX) IDE, the system installs a reset vector address and
will create code to initialize then jump to the main program automatically.
In some programming languages the column position and line number affect the
meaning. On the contrary, C is a free field language. Except for preprocessor
lines (that begin with #), spaces, tabs and line breaks have the same meaning.
The other situation where spaces, tabs and line breaks matter is string constants.
We cannot type tabs or line breaks within a string constant. This means we can
place more than one statement on a single line, or place a single statement across
multiple lines. For example the function UART_Init could have been written
without any line breaks
2020 2 - Embedded C
2.10
Similarly we could have added extra line breaks:
void UART_Init(void)
{
UART2_BD=
0x34;
UART2_C2=
0x0C;
}
Just because C allows such syntax, it does not mean it is desirable. After much
experience you will develop a programming style that is easy to understand.
Although spaces, tabs, and line breaks are syntactically equivalent, their proper
usage will have a profound impact on the readability of your software.
A token in C can be a user defined name (e.g., the variable info and function
UART_Init) or a predefined operation (e.g., *, unsigned, while). Each token must
be contained on a single line. We see in the above example that tokens can be
separated by white spaces (space, tab, line break) or by the special characters,
which we can subdivide into punctuation marks and operations. Punctuation
marks (semicolons, colons, commas, apostrophes, quotation marks, braces,
brackets, and parentheses) are very important in C. It is one of the most frequent
sources of errors for both beginning and experienced programmers.
Punctuation marks
separate tokens Punctuation Name Meaning
2 - Embedded C 2020
2.11
The next table shows the single character operators.
Special characters
Operation Name Meaning can be operators
@ at Address of
+ plus Addition
- minus Subtraction
| pipe Bitwise OR
2020 2 - Embedded C
2.12
The next table shows the operators formed with multiple characters.
Multiple special
characters can also Operation Name Meaning
be operators
== is equal to Equal to comparison
|| logical or Boolean OR
2 - Embedded C 2020
2.13
The following section illustrates some of the common operators. We begin with
the assignment operator.
/* Three variables */
short x, y, z;
void Example(void)
{
/* set the value of x to 1 */
x = 1;
/* set the value of y to 2 */
y = 2;
/* set the value of z to the value of x (both are 1) */
z = x;
/* all three to zero */
x = y = z = 0;
}
Notice that in the line x = 1;, x is on the left hand side of the =. This specifies
the address of x is the destination of assignment. On the other hand, in the line z
= x;, x is on the right hand side of the =. This specifies the value of x will be
assigned into the variable z. Also remember that the line z = x; creates two
copies of the data. The original value remains in x, while z also contains this
value.
/* Three variables */
Arithmetic operators
short x, y, z;
void Example(void)
{
/* set the values of x and y */
x = 1; y = 2;
/* arithmetic operation */
z = x + 4 * y;
/* same as x = x + 1; */
x++;
/* same as y = y - 1; */
y--;
/* left shift same as x = 4 * y; */
x = y << 2;
/* right shift same as x = y / 4; */
z = y >> 2;
/* same as y = y + 2; */
y += 2;
}
2020 2 - Embedded C
2.14
Next we will introduce a simple conditional control structure.
void Example(void)
{
/* test bit 2 of PORTE */
The C if-else if ((GPIOE_PDIR & 0x00000004) == 0)
control structure {
/* if PORTE bit 2 is 0, then make PORTB = 0 */
GPIOB_PSOR = 0;
}
else
{
/* if PORTE bit 2 is not 0, then make PORTB = 100 */
GPIOB_PSOR = 100;
}
}
GPIOB_PSOR is an output port, and GPIOE_PDIR is an input port on the K64. The
expression (GPIOE_PDIR & 0x00000004) will return 0 if PORTE bit 2 is 0 and will
return a 4 if PORTE bit 2 is 1. The expression (GPIOE_PDIR & 0x00000004) == 0
will return TRUE if PORTE bit 2 is 0 and will return a FALSE if PORTE bit 2 is 1. The
statement immediately following the if will be executed if the condition is TRUE.
The else statement is optional.
2 - Embedded C 2020
2.15
Like the if statement, the while statement has a conditional test (i.e., returns a
TRUE/FALSE).
void Example(void)
{
unsigned char counter;
/* loop until counter equals 200 */
counter = 0;
while (counter != 200) The C while control
{ structure
/* toggle PORTA bit 3 output */
GPIOA_PTOR = 0x00000008;
/* increment counter */
counter++;
}
}
GPIOA_PTOR is a register used to toggle the PORTA pins on the K64. The statement
immediately following the while will be executed over and over until the
conditional test becomes FALSE.
The for control structure has three control expressions and a body.
void Example(void)
{
unsigned char counter;
/* loop until counter equals 200 */
for (counter = 0; counter < 200; counter++) The C for loop
{ control structure
/* toggle PORTA bit 3 output */
GPIOA_PTOR = 0x00000008;
}
}
The loop test expression, counter < 200, is evaluated at the beginning of each
iteration through the loop, and if it is FALSE then the loop terminates.
Finally, the counting expression, counter++, is evaluated at the end of each loop
iteration and is usually responsible for altering the loop variable.
2020 2 - Embedded C
2.16
2.1.4 Precedence
As with all programming languages the order of the tokens is important. There
are two issues to consider when evaluating complex statements. The precedence
of the operator determines which operations are performed first.
2 - Embedded C 2020
2.17
2.1.5 Associativity
Associativity determines the left to right or right to left order of evaluation when
multiple operations of the precedence are combined. For example + and - have
the same precedence, so how do we evaluate the following?
z = y – 2 + x;
We know that + and - associate from left to right. This function is the same as
z = (y – 2) + x;, meaning the subtraction is performed first because it is more
to the left than the addition. Most operations associate left to right, but the
following table illustrates that some operators associate right to left.
Precedence and
Precedence Operators Associativity associativity
determine the
highest () [] . -> left to right order of operation
++(postfix) -–(postfix)
++(prefix) –-(prefix) ! ~ sizeof(type) right to left
+(unary) –(unary) &(address)
*(dereference)
* / % left to right
+ - left to right
<< >> left to right
< <= > >= left to right
== != left to right
& left to right
^ left to right
| left to right
&& left to right
|| left to right
?: right to left
= += -= *= /= %= <<= >>= right to left
|= &= ^=
lowest , left to right
2020 2 - Embedded C
2.18
2.1.6 Comments
There are two types of comments. The first type explains how to use the
software. These comments are usually placed at the top of the file, within the
header file, or at the start of a function. The reader of these comments will be
writing software that uses or calls these routines. Lines 1 and 19 in Listing 2.1
are examples of this type of comment. The second type of comments assist a
future programmer (ourselves included) in changing, debugging or extending
these routines. We usually place these comments within the body of the
functions. The comments above each line in Listing 2.1 are examples of the
second type. We place comments on separate lines so that the implementation is
separate from the explanation.
Comments in the C Comments begin with the /* sequence and end with the */ sequence. They may
language
extend over multiple lines as well as exist in the middle of statements. The
following is the same as UART2_BD = 0x34;
Some compilers do allow for the use of C++ style comments. The start comment
sequence is // and the comment ends at the next line break or end of file. Thus,
the following two lines are equivalent:
We will assume (for the sake of clarity) that C++ comments are allowed in this
document from now on!
C does allow the comment start and stop sequences within character constants
and string constants. For example the following string contains all seven
characters, not just the ac:
2 - Embedded C 2020
2.19
For example, the following attempt to comment-out the call to UART_Init will
result in a compiler error.
void main(void)
{
unsigned char info;
/*
/* turn on UART serial port */
UART_Init();
*/
/* specify Port C as input */
GPIOC_PDDR = 0;
while (1)
{
// input 8 bits from parallel port C
info = (unsigned char)GPIOC_PDIR;
// output 8 bits to serial port
UART_Out(info);
}
}
Preprocessor directives begin with # in the first column. As the name implies
preprocessor commands are processed first, i.e., the compiler passes through the
program handling the preprocessor directives. We have already seen the macro
Preprocessor
definition (#define) used to define I/O ports and bit fields. A second important directives are
processed first by
directive is the #include, which allows you to include another entire file at that the compiler
position within the program. The following directive will define all the K64 I/O
port names.
#include <MK64F12.h>
2020 2 - Embedded C
2.20
2.1.8 Global Declarations
An object may be a data structure or a function. Objects that are not defined
within functions are global. Objects that may be declared in MX include:
MX supports 32-bit long integers, 64-bit long long integers, and single- and
double-precision floating point types. We will focus on 8-, 16- and 32-bit
objects. The object code generated with the compiler is often more efficient
using 32-bit parameters rather than 8- or 16-bit ones.
2 - Embedded C 2020
2.21
We can see that the declaration shows us how to use the function, not how the
function works. Because the C compilation is a one-pass process, an object must
be declared or defined before it can be used in a statement. (Actually the
preprocess performs a pass through the program that handles the preprocessor
directives.) Notice that the function UART_Out was defined before it was used in
Listing 2.1. The following alternative approach first declares the functions, uses
them, and lastly defines the functions:
void main(void)
{
unsigned char info;
// turn on UART serial port
UART_Init();
// specify Port C as input
GPIOC_PDDR = 0;
while (1)
{
// input 8 bits from parallel port C
info = (unsigned char)GPIOC_PDIR;
// output 8 bits to serial port
UART_Out(info);
}
}
2020 2 - Embedded C
2.22
An object may be said to exist in the file in which it is defined, since compiling
the file yields a module containing the object. On the other hand, an object may
be declared within a file in which it does not exist. Declarations of data structures
that are defined elsewhere are preceded by the keyword extern. Thus
short RunFlag;
declares the function name and type just like a regular function declaration. The
Use extern to
specify that an extern tells the compiler that the actual function exists in another module and
object is defined
elsewhere the linker will combine the modules so that the proper action occurs at run time.
The compiler knows everything about extern objects except where they are. The
linker is responsible for resolving that discrepancy. The compiler simply tells
the assembler that the objects are in fact external. And the assembler, in turn,
makes this known to the linker.
2 - Embedded C 2020
2.23
2.1.10 Functions
2020 2 - Embedded C
2.24
The interesting part is that after the operations within the function are performed
control returns to the place right after where the function was called. In C,
execution begins with the main program. The execution sequence is shown
below:
12 void main(void)
13 {
14 short a, b;
15 a = add(2000, 2000); // call to add
1 short add(short x, short y)
2 {
3 short z;
4 z = x + y; // z = 4000
5 if ((x > 0) && (y > 0) && (z < 0))
6 z=32767;
7 if ((x < 0) && (y < 0) && (z > 0))
8 z=-32768;
9 return z;
10 } // return 4000 from call
16 b = 0;
17 while (1)
18 {
19 b = add(b, 1); // call to add
1 short add(short x, short y)
2 {
3 short z;
4 z = x + y; // z = 1
5 if ((x > 0) && (y > 0) && (z < 0))
6 z=32767;
7 if ((x < 0) && (y < 0) && (z > 0))
8 z=-32768;
9 return z;
10 } // return 1 from call
20 }
17 while (1)
18 {
19 b = add(b, 1); // call to add
1 short add(short x, short y)
2 {
3 short z;
4 z = x + y; // z = 2
5 if ((x > 0) && (y > 0) && (z < 0))
6 z=32767;
7 if ((x < 0) && (y < 0) && (z > 0))
8 z=-32768;
9 return z;
10 } // return 2 from call
20 }
Notice that the return from the first call goes to line 16, while all the other returns
go to line 20. The execution sequence repeats lines 17, 18, 19, 1-10, 20
indefinitely.
2 - Embedded C 2020
2.25
C does not allow for the nesting of procedural declarations. In other words you
cannot define a function within another function. In particular all function
declarations must occur at the global level.
A function definition consists of two parts: a declaration specifier and a body. A function definition
has two parts – a
The declaration specifier states the return type, the name of the function and the declarator and a
names of arguments passed to it. The names of the argument are only used inside body
the function. In the add function above, the declaration specifier is
short add(short x, short y) meaning it has one 16-bit output parameter, and
two 16-bit input parameters.
The parentheses are required even when there are no arguments. The following
four statements are equivalent:
The void should be included as the return parameter if there is none, because it
is a positive statement that the function does not return a parameter. When there
are no arguments, a void should be specified to make a positive statement that
the function does not require parameters.
The body of a function consists of a statement that performs the work. Normally
the body is a compound statement between a {} pair. If the function has a return
parameter, then all exit points must specify what to return.
2020 2 - Embedded C
2.26
2.1.11 Compound Statements
Although C is a free-field language, notice how the indenting has been added to
the above example. The purpose of this indenting is to make the program easier
to read. On the other hand since C is a free-field language, the following two
statements are quite different
2 - Embedded C 2020
2.27
In both cases n2 = 100; is executed if n1 > 100. In the first case the statement
n3 = 0; is always executed, while in the second case n3 = 0; is executed only
if n1 > 100.
There are two reasons to employ global variables. The first reason is data
Global variables are
permanence. The other reason is information sharing. Normally we pass used for data
sharing between
information from one module to another explicitly using input and output modules
parameters, but there are applications like interrupt programming where this
method is unavailable. For these situations, one module can store data into a
global while another module can view it.
void UART_Init(void)
{
// initialize global counter
Count = 0;
// 9600 baud
UART2_BD=0x34;
// enable UART, no interrupts
UART2_C2=0x0C;
}
2020 2 - Embedded C
2.28
void UART_Out(const uint8_t data)
{
// Incremented each time
Count = Count + 1;
// Wait for TDRE to be set
while ((UART2_S1 & TDRE) == 0);
// then output
UART2_D = data;
}
Although the following two examples are equivalent, the second case is
preferable because its operation is more self-evident. In both cases the global is
allocated in RAM, and initialized at the start of the program to 1.
short Flag = 1;
void main(void)
{
// main body goes here
}
short Flag;
void main(void)
{
Flag = 1;
// main body goes here
}
From a programmer's point of view, we usually treat the I/O ports in the same
category as global variables because they exist permanently and support shared
access.
2 - Embedded C 2020
2.29
2.1.13 Local Variables
if (x > y)
{
// create a temporary variable
short z;
// swap x and y
z = x; x = y; y = z;
// then destroy z
}
Notice that the local variable z is declared within the compound statement.
Unlike globals, which are said to be static, locals are created dynamically when
their block is entered, and they cease to exist when control leaves the block.
Furthermore, local names supersede the names of globals and other locals Local variables are
declared at higher levels of nesting. Therefore, locals may be used freely without local to their block
and supersede the
regard to the names of other variables. Although two global variables cannot use names of variables
at higher levels
the same name, a local variable of one block can use the same name as a local
variable in another block. Programming errors and confusion can be avoided by
understanding these conventions.
2020 2 - Embedded C
2.30
2.1.14 Source Files
Our programs may consist of source code located in more than one file. The
simplest method of combining the parts together is to use the #include
preprocessor directive. Another method is to compile the source files separately,
then combine the separate object files as the program is being linked with library
Software usually
consists of many modules. The linker/library method should normally be used, as only small
source files
pieces of software are changed at a time. The MX supports the automatic linking
of multiple source files once they are added to a project. Remember that a
function or variable must be defined or declared before it can be used. The
following example is one method of dividing our simple example into multiple
files.
2 - Embedded C 2020
2.31
// **** file my.c ************
// Translates parallel input data to serial outputs
#include "MK64F12.h"
#include "UART.h"
void main(void)
{
unsigned char info;
This division of functions across multiple source files is clearly a matter of style.
Breaking a software system into files has a lot of advantages. The first reason is
code reuse. Consider the code in this example. If a UART output function is
needed in another application, then it would be a simple matter to reuse the
UART.h and UART.c files. The next advantage is clarity. Compare the main
program in Listing 2.16 with the entire software system in Listing 2.1. Since the
details have been removed, the overall approach is easier to understand.
2020 2 - Embedded C
2.32
The next reason to break software into files is parallel development. As the
software system grows it will be easier to divide up a software project into
subtasks, and to recombine the modules into a complete system if the subtasks
have separate files. The last reason is upgrades. Consider an upgrade in our
simple example where the 9600 bits/sec serial port is replaced with a high-speed
Universal Serial Bus (USB). For this kind of upgrade we implement the USB
functions then replace the UART.c file with the new version. If we plan
appropriately, we should be able to make this upgrade without changes to the
files UART.h and my.c.
2 - Embedded C 2020
2.33
2.2 Tokens
This section defines the basic building blocks of a C program. Understanding the
concepts in this section will help eliminate the syntax bugs that confuse even the
veteran C programmer. A simple syntax error can generate 100's of obscure
compiler errors.
void main(void)
{
short z;
z = 0;
while (1)
{
z = z + 1;
}
}
The following sequence shows the tokens and punctuation marks from the above
listing:
Since tokens are the building blocks of programs, we begin our revision of the
C language by defining its tokens.
2020 2 - Embedded C
2.34
2.2.1 ASCII Character Set
Like most programming languages C uses the standard ASCII character set. The
following table shows the 128 standard ASCII codes. One or more white space
can be used to separate tokens and or punctuation marks. The white space
characters in C include horizontal tab (9=0x09), the carriage return (13=0x0D),
the line feed (10=0x0A), and space (32=0x20).
ASCII character
codes Bits 4 to 6
0 1 2 3 4 5 6 7
0 NUL DLE SP 0 @ P ` p
1 SOH DC1 ! 1 A Q a q
2 STX DC2 “ 2 B R b r
3 ETX DC3 # 3 C S c s
4 EOT DC4 $ 4 D T d t
5 ENQ NAK % 5 E U e u
7 BEL ETB ’ 7 G W g w
8 BS CAN ( 8 H X h x
9 HT EM ) 9 I Y i y
A LF SUB * : J Z j z
B VT ESC + ; K [ k {
C FF FS , < L \ l |
D CR GS - = M ] m }
E SO RS . > N ^ n ~
F SI US / ? O _ o DEL
Table 2.5 – ASCII Character Codes
2 - Embedded C 2020
2.35
The first 32 (values 0 to 31 or 0x00 to 0x1F) and the last one (127=0x7F) are
classified as control characters. Codes 32 to 126 (or 0x20 to 0x7E) include the
"normal" characters. Normal characters are divided into
The ASCII codes
the space character (32=0x20), are divided into
control characters
the numeric digits 0 to 9 (48 to 57 or 0x30 to 0x39), and normal
characters
the uppercase alphabet A to Z (65 to 90 or 0x41 to 0x5A),
the lowercase alphabet a to z (97 to122 or 0x61 to 0x7A), and
the special characters (all the rest).
2.2.2 Literals
2020 2 - Embedded C
2.36
2.2.3 Keywords
There are some predefined tokens, called keywords, that have specific meaning
in C programs. The reserved words we will cover in this document are:
Keywords are Keyword Meaning
predefined tokens __asm Insert assembly code.
auto Specifies a variable as automatic (created on the stack).
break Causes the program control structure to finish.
case One possibility within a switch statement.
char 8-bit integer.
const Defines a global parameter as a constant in Flash, and
defines a local parameter as a fixed value.
continue Causes the program to go to beginning of loop.
default Used in switch statement for all other cases.
do Used for creating program loops.
double Specifies a variable as double precision floating point.
else Alternative part of a conditional.
extern Defined in another module.
float Specifies a variable as single precision floating point.
for Used for creating program loops.
goto Causes program to jump to specified location.
if Conditional control structure.
int 32-bit integer (same as long on the K64). It should be
avoided in most cases because the implementation will
vary from compiler to compiler.
long 32-bit integer.
register Specifies how to implement a local.
return Leave function.
short 16-bit integer.
signed Specifies variable as signed (default).
sizeof Built-in function returns the size of an object.
static Stored permanently in memory, accessed locally.
struct Used for creating data structures.
switch Complex conditional control structure.
typedef Used to create new data types.
unsigned Always greater than or equal to zero.
void Used in parameter list to mean no parameter.
volatile Can change implicitly outside the direct action of the
software. It disables compiler optimization, forcing the
compiler to fetch a new value each time.
while Used for creating program loops.
Table 2.6 – Keywords have predefined meanings
2 - Embedded C 2020
2.37
Notice that all of the keywords in C are lowercase. Notice also that as a matter
of style, a mixture of upper and lowercase are used for variable names, and all C is case sensitive
and all keywords are
uppercase for the I/O ports. It is a good programming practice not to use these lowercase
keywords for your variable or function names.
2.2.4 Names
We use names to identify our variables, functions, and macros. MX names may
be up to 63 characters long. Names must begin with a letter or underscore and Names define
variables, functions
the remaining characters must be either letters or digits. We can use a mixture of and macros
upper and lowercase or the underscore character to create self-explaining
symbols, e.g.,
time_of_day
go_left_then_stop
TimeOfDay
GoLeftThenStop;
The careful selection of names goes a long way to making our programs more Names are case
sensitive
readable. Names may be written with both upper and lowercase letters. The
names are case sensitive. Therefore the following names are different:
thetemperature
THETEMPERATURE
TheTemperature
The practice of naming macros in uppercase calls attention to the fact that they
are not variable names but defined symbols. The I/O port names are implemented
as macros in the header file MK64F12.h.
Every global name defined with the MX is left as-is by the compiler. However,
it defines certain names for its own use, such as startup code and library files,
and precedes them with an underscore. The purpose of the underscore is to avoid
clashes with the user's own global names. So, as a matter of practice, we should
not ordinarily use names with leading underscores. For examples of this naming
convention, observe the linker map file generated by the compiler (in the *.map
file in the Debug folder in the project window).
2020 2 - Embedded C
2.38
Developing a naming convention will avoid confusion. Possible ideas to
consider include:
1. Start every variable name with its type, like Systems Hungarian notation used
by the Microsoft Windows API (abandoned with .NET). For example,
b means Boolean true/false
n means 8-bit signed integer
u means 8-bit unsigned integer
The Systems
Hungarian variable m means 16-bit signed integer
naming convention
v means 16-bit unsigned integer
l means 32-bit integer
p means 32-bit pointer (address)
c means 8-bit ASCII character
sz means null terminated ASCII string
3. Start every global variable and function with the associated file or module
A naming
convention similar to name. In the following example the names all begin with Bit_. Notice how
C++ objects
similar this naming convention recreates the look and feel of the modularity
achieved by classes in C++.
#define Bit_FIFOSize 4
// 16 * 4 = 64 bits of storage
// storage for Bit Stream
unsigned short Bit_FIFO[Bit_FIFOSize];
2 - Embedded C 2020
2.39
struct Bit_Pointer
{
// 0x8000, 0x4000,...,2,1
unsigned short mask;
// Pointer to word containing bit
unsigned short *pWord;
};
void Bit_Init(void)
{
Bit_PutPt.mask = Bit_GetPt.mask = 0x8000;
Bit_PutPt.pWord = Bit_GetPt.pWord = &Bit_FIFO[0]; // Empty
}
2020 2 - Embedded C
2.40
2.2.5 Punctuation
Semicolons
The semicolon is Semicolons are used as statement terminators. Strange and confusing syntax
used as a statement errors may be generated when you forget a semicolon, so this is one of the first
terminator
things to check when trying to remove syntax errors. Notice that one semicolon
is placed at the end of every simple statement in the following example,
void Step(void)
{
GPIOB_PSOR = 10;
GPIOB_PSOR = 9;
GPIOB_PSOR = 5;
GPIOB_PSOR = 6;
}
2 - Embedded C 2020
2.41
Preprocessor directives do not end with a semicolon since they are not actually
part of the C language proper. Preprocessor directives begin in the first column
with the # and conclude at the end of the line. The following example will fill
the array DataBuffer with data read from the input port (GPIOC_PDIR). We
assume in this example that Port C has been initialized as an input. Semicolons
are also used in the for loop statement, as illustrated by:
void Fill(void)
{
short j; The semicolon is
for (j = 0; j < 100; j++) also used in the for
{ loop
DataBuffer[j] = GPIOC_PDIR;
}
}
Listing 2.20 – Semicolons are used to separate fields of the for statement
Colons
We can define a label using the colon. Although C has a goto statement, its use
is strongly discouraged. Software is easier to understand using the block-
structured control statements (if, if else, for, while, do while, and switch
case). The following example will return after the Port C input reads the same
value 100 times in a row. Again we assume Port C has been initialized as an
input. Notice that every time the current value on Port C is different from the
previous value the counter is reinitialized.
char Debounce(void)
{
short count;
unsigned char lastData;
Start:
count = 0; // number of times Port C is the same
lastData = GPIOC_PDIR;
Loop:
if (++count == 100) goto Done; // same thing 100 times
if (lastData != GPIOC_PDIR) goto Start; // changed
goto Loop;
Done:
return lastData;
}
Listing 2.21 – Colons are used to define labels (places we can jump to)
2020 2 - Embedded C
2.42
Colons also terminate case and default prefixes that appear in switch
statements. In the following example, the next output is found (the proper
sequence is 10, 9, 5, 6). The default case is used to restart the pattern.
Listing 2.22 – Colons are also used with the switch statement
For both applications of the colon (goto and switch), we see that a label is
created that is a potential target for a transfer of control.
Commas
Commas separate items that appear in lists. We can create multiple variables of
the same type. For example,
Commas are used
to separate items in unsigned short beginTime, endTime, elapsedTime;
lists
Lists are also used with functions having multiple parameters (both when the
function is defined and called):
2 - Embedded C 2020
2.43
void main(void)
{
short a, b;
a = add(2000, 2000);
b = 0;
while (1)
{
b = add(b, 1);
}
}
Apostrophes
Apostrophes are used to specify character literals. Assuming the function
OutChar will print a single ASCII character, the following example will print the
void Alphabet(void)
{
unsigned char mych;
Apostrophes are
for (mych = 'a'; mych <= 'z'; mych++)
used to specify
{ character literals
OutChar(mych); // Print next letter
}
}
Quotation marks
Quotation marks are used to specify string literals. For example
2020 2 - Embedded C
2.44
The command Letter = 'A'; places the ASCII code (65) into the variable
Letter. The command pt = "A"; creates an ASCII string and places a pointer to
Braces
Braces {} are used throughout C programs. The most common application is for
creating a compound statement. Each open brace { must be matched with a
Braces are used to
create compound closing brace }. One approach that helps to match up braces is to use indenting.
statements
Each time an open brace is used, the source code is spaced to the right by two
spaces. In this way, it is easy to see at a glance the brace pairs. Examples of this
approach to tabbing are the Bit_Put function within Listing 2.18 and the median
function in Listing 2.9.
Brackets
PutPt = FIFO;
assigns the variable PutPt to the address of the first entry of the array.
Parentheses
Parentheses enclose argument lists that are associated with function declarations
Parentheses
enclose argument and calls. They are required even if there are no arguments.
lists and control the
order of expression
evaluation As with all programming languages, C uses parentheses to control the order in
which expressions are evaluated. Thus, (11+3)/2 yields 7, whereas 11+3/2 yields
12. Parentheses are very important when writing expressions.
2 - Embedded C 2020
2.45
2.2.6 Operators
The special characters used as expression operators are covered in the operator
Special characters
section further on in this document. There are many operators, some of which are used for
expression
are single characters, operators
<<= >>=
2020 2 - Embedded C
2.46
Numbers are stored in the computer in binary form. In other words, information
Binary information is
represented by 1’s is encoded as a sequence of 1’s and 0’s. On most computers, the memory is
and 0’s
organized into 8-bit bytes. This means each 8-bit byte stored in memory will
have a separate address. Precision is the number of distinct or different values.
We express precision in “alternatives”, “decimal digits”, “bytes”, or “binary
bits”. Alternatives are defined as the total number of possibilities. For example,
an 8-bit number scheme can represent 256 different numbers. An 8-bit digital to
analog converter (DAC) can generate 256 different analog outputs. An 8-bit
analog to digital converter (ADC) can measure 256 different analog inputs. We
use the expression 4½ decimal digits to mean about 20,000 alternatives and the
expression 4¾ decimal digits to mean more than 20,000 alternatives but less than
100,000 alternatives. The ½ decimal digit means twice the number of
alternatives or one additional binary bit. For example, a voltmeter with a range
2 - Embedded C 2020
2.47
of 0.00 to 9.99V has a three decimal digit precision. Let the operation x be the
greatest integer of x . E.g., 2.1 is rounded up to 3. Table 2.7 and Table 2.8
illustrate various representations of precision.
Various
Binary Bits Bytes Alternatives
representations of
8 1 256 precision
10 1,024
12 4,096
16 2 65,536
20 1,048,576
24 3 16,777,216
30 1,073,741,824
32 4 4,294,967,296
n n 8 2n
Table 2.7 – Relationships between various representations of precision
3 1,000
3½ 2,000
3¾ 4,000
4 10,000
4½ 20,000
4¾ 40,000
5 100,000
n 10 n
Table 2.8 – Relationships between various representations of precision
2020 2 - Embedded C
2.48
For large numbers we use abbreviations. A binary prefix is a prefix attached
before a unit symbol to multiply it by a power of 2. In computing, such a prefix
is seen in combination with a unit of information (bit, byte, etc.), to indicate a
power of 1024. IEC 80000-13:2008 is an international standard that defines
quantities and units used in information science, and specifies names and
symbols for these quantities and units, as shown in the following table.
A byte is 8 bits
b7 b6 b5 b4 b3 b2 b1 b0
where each bit b7, ..., b0 is binary and has the value 1 or 0. We specify b7 as the
most significant bit or MSB, and b0 as the least significant bit or LSB. If a byte
is used to represent an unsigned number, then the value of the number is
The value of an
unsigned byte N 128 b7 64 b6 32 b5 16 b4 8 b3 4 b2 2 b1 b0
There are 256 different unsigned 8-bit numbers. The smallest unsigned 8-bit
number is 0 and the largest is 255. For example, 000010102 is 8 + 2 or 10.
2 - Embedded C 2020
2.49
Other examples are shown in the following table.
The basis of a number system is a subset from which linear combinations of the
basis elements can be used to construct the entire set. For the unsigned
8-bit number system, the basis is
The basis of an
{ 128, 64, 32, 16, 8, 4, 2, 1 } unsigned byte
One way for us to convert a decimal number into binary is to use the basis
elements. The overall approach is to start with the largest basis element and work
towards the smallest. One by one we see whether or not we need that basis
element to create our number. If we do, then we set the corresponding bit in our
binary result and subtract the basis element from our number. If we do not need
Converting a
it, then we clear the corresponding bit in our binary result. We will work through decimal number to
binary using the
the algorithm with the example of converting 100 to 8-bit binary. We begin with unsigned basis
the largest basis element (in this case 128) and see whether or not we need to
include it to make 100. Since our number is less than 128, we do not need it so
bit 7 is zero. We go to the next largest basis element, 64 and see if we need it.
We do need 64 to generate 100, so bit 6 is one and we subtract 64 from 100 to
get 36. We go to the next basis element, 32 and see if we need it. Again, we do
need 32 to generate 36, so bit 5 is one and we perform 36 minus 32 to get 4.
Continuing along, we need basis element 4 but not 16, 8, 2 or 1, so bits 43210
are 00100 respectively. Putting it together we get 011001002 (which means 64 +
32 + 4).
2020 2 - Embedded C
2.50
This operation can be visualized using the table below.
If the least significant bit is zero, then the number is even. (2.4)
We define an unsigned 8-bit number using the unsigned char format. When a
number is stored into an unsigned char it is converted to an 8-bit unsigned value.
For example
2 - Embedded C 2020
2.51
2.3.3 8-bit signed numbers
If a byte is used to represent a signed 2’s complement number, then the value of
the number is
The value of a
N 128 b7 64 b6 32 b5 16 b4 8 b3 4 b2 2 b1 b0 signed byte
There are also 256 different signed 8 bit numbers. The smallest signed 8-bit
number is -128 and the largest is 127. For example, 100000102 is 128 2 or
126 . Other examples are shown in the following table.
Notice that the same binary pattern of 111111112 could represent either 255 or -
1. It is very important for the software developer to keep track of the number
format. The computer cannot determine whether the 8-bit number is signed or
unsigned. You, as the programmer, will determine whether the number is signed
or unsigned by the specific assembly or C instructions you select to operate on
the number. Some operations like addition, subtraction, and shift left (multiply
by 2) use the same hardware (instructions) for both unsigned and signed
operations. On the other hand, multiply, divide, and shift right (divide by 2)
require separate hardware (instructions) for unsigned and signed operations. For
example, the K64 has both unsigned umull, and signed smull, multiply
2020 2 - Embedded C
2.52
instructions. So if you use the smull instruction, you are implementing signed
arithmetic. The compiler will automatically choose the proper implementation.
Care must be taken when dealing with a mixture of numbers of different sizes
and types.
Similar to the unsigned algorithm, we can use the basis to convert a decimal
number into signed binary. We will work through the algorithm with the example
of converting -100 to 8-bit binary. We start with the largest basis element (in this
case -128) and decide if we need to include it to make -100. Without -128, we
would be unable to add the other basis elements together to get any negative
Converting a
result, so we set bit 7 and subtract the basis element from our value. Our new
decimal number to
binary using the value is -100 minus -128, which is 28. We go to the next largest basis element,
signed basis
64 and see if we need it. We do not need 64 to generate 28, so bit6 is zero. We
go to the next basis element, 32 and see if we need it. We do not need 32 to
generate 28, so bit5 is zero. Now we need the basis element 16, so we set bit4,
and subtract 16 from 28 (28 – 16 = 12). Continuing along, we need basis
elements 8 and 4 but not 2 and 1, so bits 3210 are 1100. Putting it together we
get 100111002 (which means -128+16+8+4).
2 - Embedded C 2020
2.53
This operation can be visualized using the table below.
A second way to convert negative numbers into binary is to first convert them
into unsigned binary, then do a 2’s complement negate. For example, we earlier Converting a
decimal number to
found that +100 is 011001002. The 2’s complement negate is a two step process. binary using 2’s
complement
First, we do a logic complement (toggle all bits) to get 100110112. Then, add
one to the result to get 100111002.
A third way to convert negative numbers into binary is to first add the number
to 256, then convert the unsigned result to binary using the unsigned method. Converting a
For example, to find -100, we add -100 to 256 to get 156. Then we convert 156 decimal number to
binary using modulo
to binary resulting in 100111002. This method works because in 8-bit binary arithmetic
maths adding 256 to a number does not change the value.
2020 2 - Embedded C
2.54
We define a signed 8-bit number using the char format. When a number is
stored into a char it is converted to an 8-bit signed value. For example
where each bit b15, ..., b0 is binary and has the value 1 or 0. If a half-word is
used to represent an unsigned number, then the value of the number is
There are 65,536 different unsigned 16-bit numbers. The smallest unsigned
16-bit number is 0 and the largest is 65535. For example, 0010 0001 1000 01002
or 0x2184 is 8192 + 256 + 128 + 4 or 8580.
2 - Embedded C 2020
2.55
Other examples are shown in the following table.
{ 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 } The basis of an
unsigned half-word
8 b3 4 b2 2 b1 b0
There are also 65,536 different signed 16-bit numbers. The smallest signed
16-bit number is -32768 and the largest is 32767.
2020 2 - Embedded C
2.56
For example, 1101 0000 0000 01002 or 0xD004 is -32768 + 16384 + 4096 + 4
or -12284. Other examples are shown in the following table.
The basis of a
signed half-word
{ -32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }
We define a signed 16-bit number using the short format. When a number is
stored into a short it is converted to a 16-bit signed value. For example
To avoid confusion and make the signed and unsigned data types easy to
recognise, stdint.h makes the following type definitions:
// Signed types
typedef char int8_t;
typedef int int16_t;
typedefs for signed typedef long int32_t;
and unsigned data
// Unsigned types
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
typedef unsigned long uint32_t;
2 - Embedded C 2020
2.57
2.3.7 Big- and Little-Endian
When we store 16-bit data into memory it requires two bytes. Since the memory
systems on most computers are byte addressable (a unique address for each Big- and little-endian
defined
byte), there are two possible ways to store in memory the two bytes that
constitute the 16-bit data. Some NXP microprocessors (those that came from
Freescale, formerly Motorola) implement the big-endian approach that stores the
most significant part first. Intel microprocessors implement the little-endian
approach that stores the least significant part first. The PowerPC is bi-endian,
because it can be configured to efficiently handle both big- and little-endian.
For example, assume we wish to store the 16-bit number 1000 (0x03E8) at
locations 0x50, 0x51, then
Big-endian Little-endian
We also can use either the big- or little-endian approach when storing 32-bit
numbers into memory that is byte (8-bit) addressable. If we wish to store the 32-
bit number 0x12345678 at locations 0x50-0x53 then
In the above two examples we normally would not pick out individual bytes
(e.g., the 0x12), but rather capture the entire multiple byte data as one non-
divisible piece of information. On the other hand, if each byte in a multiple byte
data structure is individually addressable, then both the big- and little-endian
schemes store the data in first to last sequence.
2020 2 - Embedded C
2.58
For example, if we wish to store the 4 ASCII characters ‘6812’ which is
0x36383132 at locations 0x50-0x53, then the ASCII ‘6’ = 0x36 comes first in
both big- and little-endian schemes.
address contents
Example of big- and
little-endian storage 0x0050 0x36
of a multiple byte
structure 0x0051 0x38
0x0052 0x31
0x0053 0x32
Big- and Little-endian
The term "Big-Endian" comes from Jonathan Swift’s satiric novel Gulliver’s
Travels. In Swift’s book, a Big-Endian refers to a person who cracks their egg
on the big end. The Lilliputians considered the big-endians as inferiors. The big-
endians fought a long and senseless war with the Lilliputians who insisted it was
only proper to break an egg on the little end.
2 - Embedded C 2020
2.59
2.3.8 Boolean information
A Boolean number has two states. The two values could represent the logical
values of true or false. The positive logic representation defines true as a 1 or
high, and false as a 0 or low. If you were controlling a motor, light, heater or air
conditioner then Boolean could mean on or off. In communication systems, we A Boolean type has
logical values of true
represent the information as a sequence of Booleans, mark or space. For black or false
or white graphic displays we use Booleans to specify the state of each pixel. The
most efficient storage of Booleans on a computer is to map each Boolean into
one memory bit. In this way, we could pack 8 Booleans into each byte. If we
have just one Boolean to store in memory, out of convenience we allocate an
entire byte or word for it. Most C compilers including GCC define:
#define FALSE 0
#define TRUE 1
Decimal numbers are written as a sequence of decimal digits (0 through 9). The
number may be preceded by a plus or minus sign or followed by an L or U. Lower
case l or u could also be used. The minus sign gives the number a negative value, Decimal numbers
have notation to
otherwise it is positive. The plus sign is optional for positive values. Unsigned specify the type
integer literals should be followed by U. You can place an L at the end of the
number to signify it to be a 32-bit signed number.
2020 2 - Embedded C
2.60
The range of a decimal number depends on the data type as shown in the
following table.
Minimize the use of Since the MC9S12 microcomputers do not have direct support of 32-bit
32-bit numbers on a
16-bit computer numbers, the use of long data types on these devices should be minimized. On
the other hand, a careful observation of the code generated yields the fact that
the compilers are more efficient with 16-bit numbers than with 8-bit numbers.
2 - Embedded C 2020
2.61
Decimal numbers are reduced to their two's complement or unsigned binary
equivalent and stored as 8/16/32-bit binary values.
The manner in which decimal literals are treated depends on the context. For
example
short I;
unsigned short J;
char K;
unsigned char L;
long M;
void main(void)
{
I = 97; // 16 bits 0x0061
J = 97; // 16 bits 0x0061
K = 97; // 8 bits 0x61
L = 97; // 8 bits 0x61
M = 97; // 32 bits 0x00000061
}
2020 2 - Embedded C
2.62
2.3.10 Octal Numbers
Notice that the octal values 0 through 07 are equivalent to the decimal values 0
Each octal digit through 7. One of the advantages of this format is that it is very easy to convert
maps to 3 bits back and forth between octal and binary. Each octal digit maps directly to/from
3 binary digits.
2 - Embedded C 2020
2.63
2.3.11 Hexadecimal Numbers
The hexadecimal number system uses base 16 as opposed to our regular decimal
number system that uses base 10. Like the octal format, the hexadecimal format
Hexadecimal
is also a convenient mechanism for humans to represent binary information, numbers are base
16, and code into a
because it is extremely simple to convert back and forth between binary and 4-bit nibble
hexadecimal. A nibble is defined as 4 bits. Each value of the 4-bit nibble is
mapped into a unique hex digit.
2020 2 - Embedded C
2.64
Computer programming environments use a wide variety of symbolic notations
to specify the numbers in various bases. The following table illustrates various
formats for numbers.
binary %11011001111101
Converting from
binary to
hexadecimal nibbles 0011 0110 0111 1101
hexadecimal 0x367D
1) convert each hexadecimal digit into its corresponding 4-bit binary nibble;
2) combine the nibbles into a single binary number.
hexadecimal 0x1B3F
Converting from
hexadecimal to
binary nibbles 0001 1011 0011 1111
binary
%0001101100111111
2 - Embedded C 2020
2.65
The range of a hexadecimal number depends on the data type as shown in the
following table.
short I;
unsigned short J;
char K;
unsigned char L;
long M;
void main(void)
{
I = 'a'; // 16 bits 0x0061
J = 'a'; // 16 bits 0x0061 Character literals
K = 'a'; // 8 bits 0x61 are surrounded by
L = 'a'; // 8 bits 0x61 apostrophes
M = 'a'; // 32 bits 0x00000061
}
All standard ASCII characters are positive because the high-order bit is zero. In
most cases it doesn't matter if we declare character variables as signed or
unsigned. On the other hand, we have seen earlier that the compiler treats signed The C char type is
signed and is
and unsigned numbers differently. Unless a character variable is specifically therefore different to
unsigned ASCII
declared to be unsigned, its high-order bit will be taken as a sign bit. Therefore, literals if the sign bit
we should not expect a character variable, which is not declared unsigned, to is set
compare equal to the same character literal if the high-order bit is set.
2020 2 - Embedded C
2.66
2.3.13 String Literals
Strictly speaking, C does not recognize character strings, but it does recognize
arrays of characters and provides a way to write character arrays, which we call
char *pt;
void main(void)
Strings are {
surrounded by pt = "John"; // pointer to the string
quotes printf(pt); // passes the pointer, not the data itself
}
The compiler places the string in memory and uses a pointer to it when calling
printf. MX pushes the parameter on the stack.
Notice that the pointer, pt, is allocated in RAM (.bss) and the string is stored
in Flash memory (.text). The assignment statement pt = "John"; copies the
address, not the data. Similarly, the function printf() must receive the address
Strings in Flash
memory are of a string as its first (in this case, only) argument. First, the address of the string
referenced by a
pointer in RAM is assigned to the character pointer pt. Unlike other languages, the string itself is
not assigned to pt, only its address is. After all, pt is a 32-bit object and,
therefore, cannot hold the string itself. The same program could be written better
as
void main(void)
{
printf("John"); // passes the pointer, not the data itself
}
Notice again that the program passes a pointer to the string into printf(), and
not the string itself.
Index Numbers, Characters and Strings PMcL
2 - Embedded C 2020
2.67
In this case, it is tempting to think that the string itself is being passed to
printf(); but, as before, only its address is.
Since strings may contain as few as one or two characters, they provide an
alternative way of writing character literals in situations where the address,
rather than the character itself, is needed.
Remember that 'J' is different from "A". Consider the following example:
2020 2 - Embedded C
2.68
The following escape sequences are recognized by the GCC compiler:
Sequence Name Value
\n newline, linefeed 0x0A = 10
\t tab 0x09 = 9
\b backspace 0x08 = 8
\f form feed 0x0C = 12
\a bell 0x07 = 7
\r return 0x0D = 13
\v vertical tab 0x0B = 11
\0 null 0x00 = 0
\" ASCII double quote 0x22 = 34
\\ ASCII back slash 0x5C = 92
\' ASCII single quote 0x27 = 39
Table 2.23 – The escape sequences supported by MX
Other nonprinting characters can also be defined using the \ooo octal format.
The digits ooo can define any 8-bit octal number. The following three lines are
equivalent:
printf("\tJohn\n");
printf("\11John\12");
printf("\011John\012");
The term newline refers to a single character which, when written to an output
A newline character
device, starts a new line. Some hardware devices use the ASCII carriage return
is represented by \n (13) as the newline character while others use the ASCII line feed (10). It really
doesn't matter which is the case as long as we write \n in our programs. Avoid
using the ASCII value directly since that could produce compatibility problems
between different compilers.
There is one other type of escape sequence: anything undefined. If the backslash
Backslash is also is followed by any character other than the ones described above, then the
used to specify
literal characters
backslash is ignored and the following character is taken literally. So the way to
code the backslash is by writing a pair of backslashes and the way to code an
apostrophe or a quote is by writing \' or \" respectively.
2 - Embedded C 2020
2.69
For example:
The concepts of precision and type (unsigned vs. signed) developed for numbers
in the last section apply to variables and constants as well. In this section we will
begin the discussion of variables that contain integers and characters. Even
though pointers are similar in many ways to 32-bit unsigned integers, pointers
will be treated in a later section. Although arrays and structures also fit the
definition of a variable, they are regarded as collections of variables and will be
discussed in later sections.
2020 2 - Embedded C
2.70
The term storage class refers to the method by which an object is assigned space
The distinction in memory. The MX compiler recognizes three storage classes – static,
between global and
local variables automatic, and external. In this document we will use the term global variable
to mean a regular static variable that can be accessed by all other functions.
Similarly, we will use the term local variable to mean an automatic variable that
can be accessed only by the function that created it. As we will see in the
following sections there are other possibilities like a static global and static local.
2.4.1 Statics
Static variables are given space in memory at some fixed location within the
program. They exist when the program starts to execute and continue to exist
throughout the program's entire lifetime. The value of a static variable is
faithfully maintained until we change it deliberately (or remove power from the
memory). A constant, which we define by adding the modifier const, can be read
but not changed.
In an embedded system we normally wish to place all variables in RAM and all
constants in Flash memory.
In the MX IDE, we set the starting memory address for the static variables in the
linker parameter *.ld file by specifying the m_data user segment. The
m_data segment is just the entire RAM of the microcontroller and is used to
store data. The program instructions will be placed in the m_text user segment,
which is normally a page of Flash memory reserved for instructions. The
constants will also be placed in the m_text user segment in an area of Flash
“bss” stands for
“Block Started by memory reserved for constants and string literals.
Symbol”, and is a
leftover acronym
from an early The MX compiler places static variables in the .bss section, which we can view
assembler written
for an IBM in the linker output *.map file in the “SECTIONS” section. It also places the
mainframe computer
in the 1950’s. It is program in the .text section, and constants in the .rodata (read only data)
the name of the data
section containing
section. The MX linker automatically places sections into their correct segments.
uninitialized
variables
2 - Embedded C 2020
2.71
The MX compiler uses the name of each static variable to define an assembler
label. The following example sets a global, called TheGlobal, to the value 1000.
This global can be referenced by any function from any file in the software
system. It is truly global.
main:
ldr r3, TheGlobal
mov r2, #1000
str r2, [r3]
bx lr
The fact that these types of variables exist in permanently reserved memory
means that static variables exist for the entire life of the program. When the
power is first applied to an embedded computer, the values in its RAM are
usually undefined. Therefore, initializing global variables requires special run-
time software. The MX compiler will attach the C code in the startup.c file
to the beginning of every program. This software is executed first, before our
main() program is started. We can see by observing the startup.c file that
the MX compiler will clear all static variables to zero (ZeroOut) immediately
after a hardware reset, and then copy all the values of initialized static variables
from Flash to RAM (CopyDown).
A static global is very similar to a regular global. In both cases, the variable is A static global can
only be accessed
defined in RAM permanently. The assembly language access is identical. The within the file where
only difference is the scope. The static global can only be accessed within the it is defined
file where it is defined.
2020 2 - Embedded C
2.72
The following example also sets a global, called TheGlobal, to the value 1000.
This static global cannot be referenced outside the scope of this file.
The K64 code generated by the MX compiler is the same as a regular global.
MX limits access to the static global to functions defined in the same file.
main:
ldr r3, TheGlobal
mov r2, #1000
str r2, [r3]
bx lr
A static local is similar to a static global. Just as with the other statics, the
A static local retains
its value from one variable is defined in RAM permanently. The assembly language code generated
function call to
another, and can by the compiler that accesses the variable is identical. The only difference is the
only be accessed
within the function scope. The static local can only be accessed within the function where it is
where it is defined defined. The following example sets a static local, called TheLocal, to the value
1000.
void main(void)
{
static int TheLocal; // a static local variable
TheLocal = 1000;
}
Again the K64 code generated by the MX compiler is the same as a regular
global. MX limits access to the static local to the function in which it is defined.
main:
ldr r3, TheGlobal
mov r2, #1000
str r2, [r3]
bx lr
2 - Embedded C 2020
2.73
A static local can be used to save information from one instance of the function All static variables
are initialized to zero
call to the next. Assume each function wished to know how many times it has by code created by
been called. Remember upon reset, the startup code will initialize all statics to the compiler
zero (including static locals).
The following functions maintain such a count, and these counts cannot be
accessed by other functions. Even though the names are the same, the two static
locals are in fact distinct.
void function1(void)
{
static int TheCount;
TheCount++;
}
void function2(void)
{
static int TheCount;
TheCount++;
}
Listing 2.33 – Two static local variables with the same name
function1:
ldr r3, .L2
ldr r3, [r3]
adds r2, r3, #1
ldr r3, .L2
str r2, [r3]
bx lr
.L2:
.word TheCount.3933
function2:
ldr r3, .L5
ldr r3, [r3]
adds r2, r3, #1
ldr r3, .L5
str r2, [r3]
bx lr
.L5:
.word TheCount.3937
Listing 2.34 – Two static local variables with the same name in assembly
The MX compiler limits the scope of the local variables to within their functions
only.
2020 2 - Embedded C
2.74
2.4.2 Volatile
We add the volatile modifier to a variable that can change value outside the scope
of the function. Usually the value of a global variable changes only as a result of
A volatile explicit statements in the C function that is currently executing. This paradigm
modifier is used to
indicate that a results when a single program executes from start to finish, and everything that
variable can change
due to external happens is an explicit result of actions taken by the program. There are two
influences
situations that break this simple paradigm in which the value of a memory
location might change outside the scope of a particular function currently
executing:
1) interrupts and
2) input/output ports.
2 - Embedded C 2020
2.75
volatile char Time;
void __attribute__ ((interrupt)) TickHandler(void)
{
// every 10 ms
Time++;
}
void main(void)
{
// Disable SysTick
SYST_CSR = 0;
// Enable SysTick
SYST_CSR = 0x00000007;
// Initialise time
Time = 0;
Without the volatile modifier the compiler might look at the two statements:
Time = 0;
while (Time < 100);
and conclude that since the while loop does not modify Time, it could never reach
100. Some compilers might attempt to move the read Time operation, performing
it once before the while loop is executed. The volatile modifier disables the
optimization, forcing the program to fetch a new value from the variable each
time the variable is accessed.
2020 2 - Embedded C
2.76
In the next K64 example, assume GPIOA_PDIR is an input port containing the
current status of some important external signals. The program wishes to collect
status versus time data of these external signals.
void main(void)
{
int i;
Without the volatile modifier in the GPIOA_PDIR definition, the compiler might
optimize the for loop, reading GPIOA_PDIR once, then storing 100 identical
copies into the data array.
2 - Embedded C 2020
2.77
2.4.3 Automatics
Automatic variables do not have fixed memory locations. They are dynamically
allocated when the block in which they are defined is entered, and they are
discarded upon leaving that block. Specifically, they are allocated on the K64
stack by subtracting a value (one for characters, two for short integers and four Automatic variables
are created on the
for long integers) from the stack pointer register (SP). Since automatic objects stack, and only exist
locally
exist only within blocks, they can only be declared locally. An automatic
variable can only be referenced (read or written to) by the function that created
it. In this way, the information is protected or local to the function.
When a local variable is created it has no dependable initial value. It must be set
to an initial value by means of an assignment operation. C provides for automatic
variables to be initialized in their declarations, like globals. It does this by
generating "hidden" code that assigns values automatically after variables are
allocated space.
It is tempting to forget that automatic variables go away when the block in which
they are defined exits. This sometimes leads new C programmers to fall into the
"dangling reference" trap in which a function returns a pointer to a local variable,
as illustrated by
int* BadFunction(void)
{
int z;
z = 1000;
return(&z);
}
When callers use the returned address of z they will find themselves messing
around with the stack space that z used to occupy. This type of error is NOT
flagged as a syntax error, but rather will cause unexpected behaviour during
execution.
2020 2 - Embedded C
2.78
2.4.4 Implementation of Automatic Variables
In order to understand both the machine architecture and the C compiler, we can
look at the assembly code generated. For the MX compiler, the linker/loader
allocates 3 segmented memory sections: code pointed to by the PC (.text
section); globals accessed with absolute addressing (.data section); and locals
pointed to by the stack pointer SP. This example shows a simple C program with
three local variables. Although the function doesn't do much (and will be in
general be optimised out of any object code) it will serve to illustrate how local
variables are created (allocation), accessed (read and write) and destroyed
(deallocated).
void sub(void)
{
short y1, y2, y3; // 3 local variables
y1 = 1000;
y2 = 2000;
y3 = y1 + y2;
}
The disassembled output of the MX compiler shown below has been highlighted
to clarify its operation. In the K64 the program counter (PC) always points to the
next instruction to be executed.
2 - Embedded C 2020
2.79
Code and K64
registers and stack
address data showing the creation
0x00000BE0 R2 and use of
automatic variables
0x00000BE4 R3
0x00000BE8 R4
0x00000BEC SP 0x00000BF4
0x00000BF0
0x00000BF4 return address SP
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
PC sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
address data
0x00000BE0 R2
0x00000BE4 R3
0x00000BE8 y3 SP R4
0x00000BEC y2 SP 0x00000BE8
0x00000BF0 y1
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
PC mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
2020 2 - Embedded C
2.80
address data
0x00000BE0 R2 1000
0x00000BE4 R3
0x00000BE8 y3 SP R4
0x00000BEC y2 SP 0x00000BE8
0x00000BF0 y1
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
PC str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
address data
0x00000BE0 R2 1000
0x00000BE4 R3
0x00000BE8 y3 SP R4
0x00000BEC y2 SP 0x00000BE8
0x00000BF0 y1 = 1000
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
PC mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
2 - Embedded C 2020
2.81
address data
0x00000BE0 R2 1000
0x00000BE4 R3 2000
0x00000BE8 y3 SP R4
0x00000BEC y2 SP 0x00000BE8
0x00000BF0 y1 = 1000
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
PC str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
address data
0x00000BE0 R2 1000
0x00000BE4 R3 2000
0x00000BE8 y3 SP R4
0x00000BEC y2 = 2000 SP 0x00000BE8
0x00000BF0 y1 = 1000
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
PC add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
2020 2 - Embedded C
2.82
address data
0x00000BE0 R2 3000
0x00000BE4 R3 2000
0x00000BE8 y3 SP R4
0x00000BEC y2 = 2000 SP 0x00000BE8
0x00000BF0 y1 = 1000
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
PC str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
address data
0x00000BE0 R2 3000
0x00000BE4 R3 2000
0x00000BE8 y3 = 3000 SP R4
0x00000BEC y2 = 2000 SP 0x00000BE8
0x00000BF0 y1 = 1000
0x00000BF4 return address
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
PC adds sp,sp,#12 ; deallocate y1, y2, y3
bx lr
2 - Embedded C 2020
2.83
address data
0x00000BE0 R2 3000
0x00000BE4 R3 2000
0x00000BE8 R4
0x00000BEC SP 0x00000BF4
0x00000BF0
0x00000BF4 return address SP
0x00000BF8
.text ;sub in ROM
; y3 -> sp,#0
; y2 -> sp,#4
; y1 -> sp,#8
void sub()
sub: {
sub sp,sp,#12 ; allocate y1, y2, y3 int y1, y2, y3;
mov r2,#1000 ; y1 = 1000 y1 = 1000;
str r2,[sp,#8]
mov r3,#2000 ; y2 = 2000 y2 = 2000;
str r3,[sp,#4]
add r3,r3,r2 ; y3 = y1 + y2 y3 = y1 + y2;
str r3,[sp,#0] }
adds sp,sp,#12 ; deallocate y1, y2, y3
PC bx lr
2020 2 - Embedded C
2.84
The sub sp,sp,#12 instruction allocates the local variables, and thereafter
they are accessed by indexing the stack pointer. Within the subroutine the local
variables of other functions are not accessible. If a function is called from within
another function, the new function will allocate its own local variable space on
the stack, without disturbing the existing data.
A constant local is different to a regular local. Unlike the other locals, the
constant is not defined temporarily on the stack. Since it cannot be changed, the
assembly language code generated by the MX compiler that references the
constant local replaces the reference with the actual value.
The K64 code generated by the MX compiler is as follows (notice the reservation
of space in the .bss section for the global variable)
.text
sub:
mov r3,#1000
ldr r2,TheGlobal
str r3,[r2]
bx lr
.bss
TheGlobal
2 - Embedded C 2020
2.85
2.4.6 Externals
Objects that are defined outside of the present source module have the external
storage class. This means that, although the compiler knows what they are
(signed / unsigned, 8-bit, 16-bit, 32-bit, etc.), it has no idea where they are. It
simply refers to them by name without reserving space for them. Then, when the Externals are
variables defined
linker brings together the object modules, it resolves these "pending" references elsewhere
by finding the external objects and inserting their addresses into the instructions
that refer to them. The compiler knows an external variable by the keyword
extern that must precede its declaration.
Only global declarations can be designated extern and only globals in other
modules can be referenced as external.
The following example sets an external global, called ExtGlobal, to the value
1000. This global can be referenced by any function from any file in the software
The assembly language the MX compiler generates does not include the
definition of ExtGlobal. The K64 code generated by the MX compiler is as
follows
.text
main:
mov r3,#1000
ldr r2,ExtGlobal
str r3,[r2]
bx lr
2020 2 - Embedded C
2.86
2.4.7 Scope
The scope of a variable is the portion of the program from which it can be
Scope refers to referenced. We might say that a variable's scope is the part of the program that
where a variable
can be “seen” "knows" or "sees" the variable. As we shall see, different rules determine the
scopes of global and local objects.
When a variable is declared globally (outside of a function) its scope is the part
Global variables
have a scope that of the source file that follows the declaration – any function following the
extends from the declaration can refer to it. Functions that precede the declaration cannot refer to
declaration to the
end of the file it. Most C compilers would issue an error message in that case.
The scope of local variables is the block in which they are declared. Local
declarations must be grouped together before the first executable statement in
Local variables have
a scope that is
the block – at the head of the block. This is different from C++ and C11 that
restricted to the allow local variables to be declared anywhere in the function. It follows that the
function where they
are defined scope of a local variable effectively includes all of the block in which it is
declared. Since blocks can be nested, it also follows that local variables are seen
in all blocks that are contained in the one that declares the variables.
If we declare a local variable with the same name as a global object or another
local in a superior block, the new variable temporarily supersedes the higher
level declarations. Consider the following program.
2 - Embedded C 2020
2.87
This program declares variables with the name x, assigns values to them, and
outputs them to GPIOA_PDOR in such a way that, when we consider its output, the Local variables can
have nested scope
scope of its declarations becomes clear. When this program runs, it outputs 321.
This only makes sense if the x declared in the inner most block masks the higher
level declarations so that it receives the value '3' without destroying the higher
level variables. Likewise the second x is assigned '2' which it retains throughout
the execution of the inner-most block. Finally, the global x, which is assigned '1',
is not affected by the execution of the two inner blocks. Notice, too, that the
placement of the last two GPIOA_PDOR = x; statements demonstrates that leaving
a block effectively unmasks objects that were hidden by declarations in the
block. The second GPIOA_PDOR = x; sees the middle x and the last GPIOA_PDOR =
x; sees the global x.
One of the mistakes a C++ programmer makes when writing C code is trying to
define local variables in the middle of a block. In C local variables must be
defined at the beginning of a block. The following example is proper C++ or
C11 code, but results in a syntax error in C.
void sub(void)
{
int x; // a valid local variable declaration
x = 1;
int y; // this declaration is improper in C
y = 2;
}
2020 2 - Embedded C
2.88
2.4.8 Declarations
Describing a variable involves two actions. The first action is declaring its type
and the second action is defining it in memory (reserving a place for it). Although
A declaration
defines the type of a
both of these may be involved, we refer to the C construct that accomplishes
variable and where them as a declaration. As we saw previously, if the declaration is preceded by
it is located in
memory extern it only declares the type of the variable, without reserving space for it. In
such cases, the definition must exist in another source file. Failure to do so will
result in an unresolved reference error at link time.
Table 2.24 contains examples of legitimate variable declarations. Notice that the
declarations are introduced by one or two type keywords that state the data types
of the variables listed. The keyword char declares 8-bit values, short declares
16-bit values, int declares 32-bit values, and long declares 32-bit values. Unless
Variables have a
signed modifier by the modifier unsigned is present, the variables declared by these statements are
default
assumed by the compiler to contain signed values. You could add the keyword
signed before the data type to clarify its type.
When more than one variable is being declared, they are written as a list with the
individual names separated by commas. Each declaration is terminated with a
semicolon, as are all simple C statements.
2 - Embedded C 2020
2.89
The following tables shows the available storage classes and modifiers for
variables.
The MX compiler allows the register modifier for automatic variables, but this
is usually unnecessary as the compiler will use registers in preference to locals
on the stack (for speed reasons).
2020 2 - Embedded C
2.90
In all cases const means the variable has a fixed value and cannot be changed.
When modifying a global on an embedded system like the K64, it also means
the parameter will be allocated in Flash memory. In the following example, CR
A const modifier
means a variable is allocated in Flash memory. When const is added to a parameter or a local
cannot be changed
variable, it means that parameter cannot be modified by the function. It does not
change where the parameter is allocated. For example, this example is legal:
On the other hand, the following example is not legal because the function
attempts to modify the input parameter. count in this example would have been
allocated on the stack or in a register.
2 - Embedded C 2020
2.91
Similarly, the following example is not legal because the function attempts to
modify the local variable. COUNT in this example would have been substituted by
the value 5.
void IllegalFuntion2(void)
{
const short COUNT = 5;
while (COUNT)
{
UART_OutChar(13);
COUNT--; // this operation is illegal
}
}
2.4.9 Character Variables
Character variables are stored as 8-bit quantities. When they are fetched from
Characters are 8-bit
memory, they are always promoted automatically to 32-bit integers. Unsigned quantities promoted
8-bit values are promoted by adding 24 zeros into the most significant bits. to 32-bits
Signed values are promoted by copying the sign bit (bit7) into the 24 most
significant bits.
2.4.10 Mixing Signed and Unsigned Variables
There is a confusion when signed and unsigned variables are mixed into the same
expression. It is good programming practice to avoid such confusions. As with Do not mix signed
and unsigned
integers, when a signed character enters into an operation with an unsigned variables in
expressions
quantity, the character is interpreted as though it was unsigned. The result of
such operations is also unsigned. When a signed character joins with another
signed quantity, the result is also signed.
char x; // signed 8-bit global
unsigned short y; // unsigned 16-bit global
void sub(void)
{
y = y + x;
// x treated as unsigned even though defined as signed
}
Listing 2.43 – Code showing the mixture of signed and unsigned variables
There is also a need to change the size of characters when they are stored, since
they are represented in the CPU as 32-bit values. In this case, however, it does
not matter whether they are signed or unsigned. Obviously there is only one
reasonable way to put a 32-bit quantity into an 8-bit location. When the high-
order byte is chopped off, an error might occur. It is the programmer's
responsibility to ensure that significant bits are not lost when characters are
stored.
PMcL Variables and Constants Index
2020 2 - Embedded C
2.92
2.4.11 When Do We Use Automatics Versus Statics?
Because their contents are allowed to change, all variables must be allocated in
RAM and not Flash memory. An automatic variable contains temporary
information used only by one software module. Automatic variables are
typically allocated, used, then deallocated from the stack. Since an interrupt will
save registers and create its own stack frame, the use of automatic variables is
Automatic variables important for creating re-entrant software. Automatic variables provide
contain temporary
information used protection, limiting the scope of access in such a way that only the program that
only by one software
module, and they created the local variable can access it. The information stored in an automatic
are interrupt proof
variable is not permanent. This means if we store a value into an automatic
variable during one execution of the module, the next time that module is
executed the previous value is not available. Typically we use automatics for
loop counters and temporary sums. We use an automatic variable to store data
that is temporary in nature. In summary, reasons why we place automatic
variables on the stack include:
A static variable is information shared by more than one program module. For
Static variables
contain information example, we use globals to pass data between the main (or background) process
that is shared
between software and an interrupt (or foreground) process. Static variables are not deallocated. The
modules
information they store is permanent. We can use static variables for the time of
day, date, user name, temperature, pointers to shared data, etc. The MX compiler
uses absolute addressing (direct or extended) to access the static variables.
2 - Embedded C 2020
2.93
2.4.12 Initialization of variables and constants
Most programming languages provide ways of specifying initial values; that is,
the values that variables have when program execution begins. The MX compiler Static variables are
initialized to zero
will initially set all static variables to zero. Constants must be initialized at the
time they are declared, and we have the option of initializing the variables.
Specifying initial values is simple. In its declaration, we follow a variable's name Constants must be
initialized when they
with an equals sign and a constant expression for the desired value. Thus are declared
declares Letter to be a character, and gives it the value of the tab character. If
array elements are being initialized, a list of constant expressions, separated by
commas and enclosed in braces, is written. For example,
declares Steps to be an unsigned 16-bit constant integer array, and gives its
elements the values 10, 9, 6, and 5 respectively. If the size of the array is not
specified, it is determined by the number of initializers. Thus
2020 2 - Embedded C
2.94
Character arrays and character pointers may be initialized with a character string.
Character arrays In these cases, a terminating zero is automatically generated. For example,
can be initialized
with a character char Name[5] = "John";
string
declares Name to be a character array of five elements with the first four initialized
to 'J', 'o', 'h' and 'n' respectively. The fifth element contains zero. If the size
of the array is not given, it will be set to the size of the string plus one. Thus
also contains the same five elements. If the size is given and the string is shorter,
trailing elements default to zero. For example, the array declared by
contains zeroes in its last three elements. If the string is longer than the specified
size of the array, the array size is increased to match.
If we write
the effect is quite different from initializing an array. First a word (32 bits) is set
Pointers can be aside for the pointer itself. This pointer is then given the address of the string.
initialized to point to
Then, beginning with that byte, the string and its zero terminator are assembled.
a constant character
string The result is that NamePtr contains the address of the string "John". The MX
compiler accepts initializers for character variables, pointers, and arrays; and for
integer variables and arrays. The initializers themselves may be either constant
expressions, lists of constant expressions, or strings.
2 - Embedded C 2020
2.95
2.4.13 Implementation of the initialization
The compiler initializes static constants simply by defining its value in Flash
memory (normally as part of the instruction for small values). In the following
example, J is a static constant, and K is a literal.
.text
main:
movs r2,#96 ;constant J=96
ldr r3,I
strh r2,[r3] ;I=J (store half-word, 16-bits)
ldr r3,I
movs r2,#97
strh r2,[r3] ;I=K (store half-word, 16-bits)
bx lr
.word I
Notice the use of the #define macro which is used to implement an operation
that is equivalent to I = 97;.
The compiler initializes a static variable by defining its initial value in Flash
memory. It creates another segment called .rodata (in addition to the .data and
.text sections). It places the initial values in the .rodata segment, then copies the
data dynamically from .rodata Flash memory into .data RAM variables at the
start of the program (before main is started). For example
For the MX compiler, code in the startup.c file will copy the 95 from
.rodata (Flash memory) into I in .bss (RAM) upon a hardware reset.
2020 2 - Embedded C
2.96
This copy is performed transparently before the main program is started.
.text
main:
...
bx lr
.global I
.section .data.I,"aw",%progbits
.align 1
I:
.short 95
Even though the following two initializations of a global variable are technically
proper, the explicit initialization of a global variable is a better style.
// good style // poor style
int I; int I = 95;
void main(void) void main(void)
{ {
I = 95;
} }
2 - Embedded C 2020
2.97
2.4.14 Summary of Variable Attributes
Every variable possesses a number of different attributes, as summarized in the
table below:
Attribute Description
2020 2 - Embedded C
2.98
2.5 Expressions
Most programming languages support the traditional concept of an expression
as a combination of constants, variables, array elements, and function calls
An expression is a joined by various operators (+, -, etc.) to produce a single numeric value. Each
combination of
constants, variables operator is applied to one or two operands (the values operated on) to produce a
array elements and
function calls joined single value which may itself be an operand for another operator. This idea is
by operators
generalized in C by including non-traditional data types and a rich set of
operators. Pointers, unsubscripted array names, and function names are allowed
as operands. As Table 2.29 through to Table 2.34 illustrate, many operators are
available. All of these operators can be combined in any useful manner in an
expression. As a result, C allows the writing of very compact and efficient
expressions which at first glance may seem a bit strange. Another unusual feature
of C is that anywhere the syntax calls for an expression, a list of expressions,
with comma separators, may appear.
Operand count
Operand count refers to the classification of operators as unary, binary, or ternary
refers to how many according to whether they operate on one, two, or three operands. The unary
variables the
operator is applied minus sign, for instance, reverses the sign of the following operand, whereas the
to
binary minus sign subtracts one operand from another.
y = 254 * x / 100;
If we divide first, then y can only take on values that are multiples of 254 (e.g.,
0, 254, 508 etc.), so the following statement is incorrect:
y = 254 * (x / 100);
2 - Embedded C 2020
2.99
The proper approach is to multiply first then divide. To multiply first we must
guarantee that the product 254 * x will not overflow the precision of the
computer. How do we know what precision the compiler used for the
Precedence and
intermediate result 254 * x? To answer this question, we must observe the associativity are
assembly code generated by the compiler. Since multiplication and division confusing – use
parentheses
associate left to right, the first statement without parentheses, although
ambiguous will actually calculate the correct answer. It is good programming
style to use parentheses to clarify the expression. The following statement has
both good style and proper calculation:
y = (254 * x) / 100;
2020 2 - Embedded C
2.100
2.5.2 Unary operators
Unary operators Unary operators take a single input and give a single output. In the following
have a single input,
and a single output examples, assume all numbers are 16-bit signed (short). The following variables
are listed:
2 - Embedded C 2020
2.101
2.5.3 Binary operators
/ division 123 / 10 12
% remainder 123 % 10 3
The binary bitwise logical operators take two inputs and give a single result.
2020 2 - Embedded C
2.102
The binary Boolean operators take two Boolean inputs and give a single Boolean
result.
|| OR 0 || 1 1 (true)
Many programmers confuse the logical operators with the Boolean operators.
Logical operators take two numbers and perform a bitwise logical operation.
Don’t confuse Boolean operators take two Boolean inputs (0 and not zero) and return a Boolean
bitwise logical
(0 or 1). In the program below, the operation c = a & b; will perform a bitwise
operators with
Boolean operators logical AND of 0x0F0F and 0xF0F0 resulting in 0x0000. In the d = a && b;
expression, the value a is considered as a TRUE (because it is not zero) and the
value b also is considered a TRUE (not zero). The Boolean operation of TRUE AND
TRUE gives a TRUE result (1).
short a, b, c, d;
void main(void)
{
a = 0x0F0F;
b = 0xF0F0;
c = a & b; // logical result c will be 0x0000
d = a && b; // Boolean result d will be 1 (true)
}
2 - Embedded C 2020
2.103
The binary relational operators take two number inputs and give a single Boolean
Binary relational
result. operators
short a, b;
void program(void)
{
if (a == 0)
subfunction(); // execute subfunction if a is zero
if (b = 0)
subfunction();//set b to zero, never execute subfunction
}
2020 2 - Embedded C
2.104
2.5.4 Assignment Operators
The assignment operator is used to store data into variables. The syntax is
The assignment
operator variable = expression; where variable has been previously defined. At run-
time, the result of the expression is saved into the variable. If the type of the
expression is different from the variable, then the result is automatically
converted. The assignment operation itself has a result, so the assignment
operation can be nested.
short a, b;
void initialize(void)
{
a = b = 0; // set both variables to zero
}
The read / modify / write assignment operators are convenient. Examples are
shown below.
short a, b;
void initialize(void)
{
a += b; // same as a = a + b
a -= b; // same as a = a - b
a *= b; // same as a = a * b
Read / modify / write a /= b; // same as a = a / b
assignment a %= b; // same as a = a % b
operators a <<= b; // same as a = a << b
a >>= b; // same as a = a >> b
a |= b; // same as a = a | b
a &= b; // same as a = a & b
a ^= b; // same as a = a ^ b
}
Most compilers will produce the same code for the short and long version of the
operation. Therefore you should use the read / modify / write operations only in
situations that make the software easier to understand.
void function(void)
{
GPIOA_PDOR |= 0x01; // set PA0 high
GPIOB_PDOR &= ~0x80; // clear PB7 low
GPIOC_PDOR ^= 0x40; // toggle PC6
}
2 - Embedded C 2020
2.105
2.5.5 Expression Types and Explicit Casting
We saw earlier that numbers are represented in the computer using a wide range
of formats. A list of these formats is given in Table 2.35. Notice that for the
MC9S12, the int and short types are the same. On the other hand, with the K64,
the int and long types are the same. This difference may cause confusion when Declare the size of
an integer type
porting code from one system to another. You should use the short type when explicitly
you are interested in efficiency and don't care about precision, and use the long
type when you want a variable with a 32-bit precision.
What happens when two numbers of different types are operated on? Before
operation, the C compiler will first convert one or both numbers so they have the
same type. The conversion of one type into another has many names:
automatic conversion;
Names for type
implicit conversion; conversion
coercion;
promotion; or
widening.
2020 2 - Embedded C
2.106
There are three ways to consider this issue. The first way to think about this is if
Promotion of type
the range of one type completely fits within the range of the other, then the
number with the smaller range is converted (promoted) to the type of the number
with the larger range. In the following examples, a number of type1 is added to
a number of type2. In each case, the number range of type1 fits into the range of
type2, so the parameter of type1 is first promoted to type2 before the addition.
The second way to consider mixed precision operations is that in most cases the
compiler will promote the number with the smaller precision into the other type
before operation. If the two numbers are of the same precision, then the signed
number is converted to unsigned. These automatic conversions may not yield
correct results. The third and best way to deal with mixed type operations is to
perform the conversions explicitly using the cast operation. We can force the
Use a typecast to
force a particular type of an expression by explicitly defining its type. This approach allows the
type conversion
programmer to explicitly choose the type of the operation.
Consider the following digital filter with mixed type operations. In this example,
we explicitly convert x and y to signed 16-bit numbers and perform 16-bit signed
arithmetic. Note that the assignment of the result into y, will require a demotion
of the 16-bit signed number into an 8-bit signed number. Unfortunately, C does
not provide any simple mechanisms for error detection / correction.
2 - Embedded C 2020
2.107
char y; // output of the filter
unsigned char x; // input of the filter
void filter(void)
{
y = (12 * (short)x + 56 * (short)y) / 100;
}
We saw previously that casting was used to assign a symbolic name to an I/O
port. In particular the following #define casts the number 0x400FF000 as a
pointer type, which points to volatile unsigned 32-bit data.
2020 2 - Embedded C
2.108
2.5.6 Selection operator
The selection operator takes three input parameters and yields one output result.
The format is
The selection
operator Expr1 ? Expr2 : Expr3
The first input parameter is an expression, Expr1, which yields a Boolean (0 for
false, not zero for true). Expr2 and Expr3 return values that are regular numbers.
The selection operator will return the result of Expr2 if the value of Expr1 is true,
and will return the result of Expr3 if the value of Expr1 is false. The type of the
expression is determined by the types of Expr2 and Expr3. If Expr2 and Expr3
have different types, then the usual promotion is applied. The resulting type is
determined at compile time, in a similar manner as the Expr2 + Expr3 operation,
and not at run-time depending on the value of Expr1. The following two
subroutines have identical functions.
short a, b;
void sub1(void)
{
a = (b==1) ? 10 : 1;
}
void sub2(void)
{
if (b == 1)
a = 10;
else
a = 1;
}
2 - Embedded C 2020
2.109
2.5.7 Arithmetic Overflow and Underflow
For example when two 8-bit numbers are added, the sum may not fit back into
the 8-bit result. We saw earlier that the same digital hardware (instructions)
could be used to add and subtract unsigned and signed numbers. Unfortunately,
we will have to design separate overflow detection for signed and unsigned
addition and subtraction.
All microcomputers have a condition code register which contain bits which
specify the status of the most recent operation. In this section, we will introduce Condition code bits
4 condition code bits common to most microcomputers. If the two inputs to an are used to detect
an overflow or
addition or subtraction operation are considered as unsigned, then the C bit underflow
(carry) will be set if the result does not fit. In other words, after an unsigned
addition, the C bit is set if the answer is wrong. If the two inputs to an addition
or subtraction operation are considered as signed, then the V bit (overflow) will
2020 2 - Embedded C
2.110
be set if the result does not fit. In other words, after a signed addition, the V bit
is set if the answer is wrong.
Table 2.37 – Condition code bits contain the status of the previous
arithmetic or logical operation
For an 8-bit unsigned number, there are only 256 possible values, 0 to 255. We
can think of the numbers as positions along a circle. There is a discontinuity at
the 0|255 interface, everywhere else adjacent numbers differ by 1 . If we add
For unsigned two unsigned numbers, we start at the position of the first number and move in
numbers, errors
occur when crossing
a clockwise direction the number of steps equal to the second number. For
the 0|255 boundary example, if 96 + 64 is performed in 8-bit unsigned precision, the correct result
of 160 is obtained. In this case, the carry bit will be 0 signifying the answer is
correct. On the other hand, if 224 + 64 is performed in 8-bit unsigned precision,
the incorrect result of 32 is obtained. In this case, the carry bit will be 1,
signifying the answer is wrong.
64 64
96 32
0 0
+64 128 128 +64
255 255
160 224
192 192
2 - Embedded C 2020
2.111
For subtraction, we start at the position of the first number and move in a counter
clockwise direction the number of steps equal to the second number. For
example, if 160 - 64 is performed in 8-bit unsigned precision, the correct result
of 96 is obtained (carry bit will be 0). On the other hand, if
32 - 64 is performed in 8-bit unsigned precision, the incorrect result of 224 is
obtained (carry bit will be 1) .
64 64
96 32
0 0
-64 128 128 -64
255 255
160 224
192 192
In general, we see that the carry bit is set when we cross over from 255 to 0 while
adding or cross over from 0 to 255 while subtracting.
For an 8-bit signed number, the possible values range from -128 to 127. Again
there is a discontinuity, but this time it exists at the -128|127 interface,
everywhere else adjacent numbers differ by 1 . The meanings of the numbers For signed numbers,
errors occur when
with bit 7 = 1 are different from unsigned, but we add and subtract signed crossing the
-128|127 boundary
numbers on the number wheel in a similar way (e.g., addition of a positive
number moves counterclockwise.) Adding a negative number is the same as
subtracting a positive number hence this operation would cause a clockwise
motion. For example, if -32 + 64 is performed, the correct result of 32 is
obtained. In this case, the overflow bit will be 0 signifying the answer is correct.
On the other hand, if 96 + 64 is performed, the incorrect result of
2020 2 - Embedded C
2.112
-96 is obtained. In this case, the overflow bit will be 1 signifying the answer is
wrong.
64 64
32 96
127 0 127 0
+64 +64
-128 -1 -128 -1
-32 -96
-64 -64
64 64
32 96
127 0 127 0
-64 -64
-128 -1 -128 -1
-32 -96
-64 -64
2 - Embedded C 2020
2.113
In general, we see that the overflow bit is set when we cross over from 127 to
-128 while adding or cross over from -128 to 127 while subtracting.
Another way to determine the overflow bit after an addition is to consider the
carry out of bit 6. The V bit will be set of there is a carry out of bit 6 (into bit 7) Overflow can be
detected by Boolean
but no carry out of bit 7 (into the C bit). It is also set if there is no carry out of operations on the
individual bits
bit 6 but there is a carry out of bit 7. Let X7-X0 and M7-M0 be the individual
binary bits of the two 8-bit numbers which are to be added, and let R7-R0 be
individual binary bits of the 8-bit sum. Then, the 4 condition code bits after an
addition are shown in Table 2.38.
M 7 R7 or add one number above 127 and get a number below 128;
or add one number above 127 and get a number below 128
X 7 R7
2020 2 - Embedded C
2.114
Let the result R be the result of the subtraction X - M. Then the 4 condition code
bits are shown in Table 2.39.
or started with a number below 127 and get one above 127
X 7 R7
2 - Embedded C 2020
2.115
There are some applications where arithmetic errors are not possible. For
example, if we had two 8-bit unsigned numbers that we knew were in the range
of 0 to 100, then no overflow is possible when they are added together.
Typically the numbers we are processing are either signed or unsigned (but not
both), so we need only consider the corresponding C or V bit (but not both the C
and V bits at the same time.) In other words, if the two numbers are unsigned,
then we look at the C bit and ignore the V bit. Conversely, if the two numbers
are signed, then we look at the V bit and ignore the C bit. There are two
appropriate mechanisms to deal with the potential for arithmetic errors when
Promotion is used
adding and subtracting. The first mechanism, used by most compilers, is called by compilers to
avoid overflow and
promotion. Promotion involves increasing the precision of the input numbers, underflow problems
and performing the operation at that higher precision. An error can still occur if
the result is stored back into the smaller precision. Fortunately, the program has
the ability to test the intermediate result to see if it will fit into the smaller
precision. To promote an unsigned number we add zero’s to the left side.
2020 2 - Embedded C
2.116
In a previous example, we added the unsigned 8-bit 224 to 64, and got the wrong
result of 32. With promotion we first convert the two 8-bit numbers to 16-bits,
then add. We can check the 16-bit intermediate result (e.g., 228) to see if the
answer will fit back into the 8-bit result. In the following flowchart, X and M are
8-bit unsigned inputs, X 16 , M 16 , and R16 are 16-bit intermediate values, and R
is an 8-bit unsigned output.
Promotion of
unsigned numbers
unsigned add unsigned sub
to avoid overflow
and underflow
Promote X to X 16 Promote X to X 16
Promote M to M16 Promote M to M16
ok overflow ok underflow
R 16 255 R 16 > 255 R 16 0 R 16 < 0
R 16 R 16
R = R 16 R = 255 R = R 16 R= 0
end end
2 - Embedded C 2020
2.117
We can check the 16-bit intermediate result (e.g., -160) to see if the answer will
fit back into the 8-bit result. In the following flowchart, X and M are 8-bit signed
inputs, X 16 , M 16 , and R16 are 16-bit signed intermediate values, and R is an 8-
bit signed output.
Promotion of signed
numbers to avoid
signed add signed sub
overflow and
underflow
Promote X to X 16 Promote X to X 16
Promote M to M16 Promote M to M16
R = R 16 R = R 16
end end
The other mechanism for handling addition and subtraction errors is called
ceiling and floor. It is analogous to movements inside a room. If we try to move Ceiling and floor can
be used to avoid
up (add a positive number or subtract a negative number) the ceiling will prevent overflow and
us from exceeding the bounds of the room. Similarly, if we try to move down underflow
(subtract a positive number or add a negative number) the floor will prevent us
from going too low. For our 8-bit addition and subtraction, we will prevent the
0 to 255 and 255 to 0 crossovers for unsigned operations and
-128 to +127 and +127 to -128 crossovers for signed operations. These operations
are described by the following flowcharts. If the carry bit is set after an unsigned
addition the result is adjusted to the largest possible unsigned number (ceiling).
If the carry bit is set after an unsigned subtraction, the result is adjusted to the
smallest possible unsigned number (floor.)
2020 2 - Embedded C
2.118
Using ceiling and
floor of unsigned
numbers to avoid unsigned add unsigned sub
overflow and
underflow
R= X+ M R= X M
C=1 C=1
C C
end end
If the overflow bit is set after a signed operation the result is adjusted to the
largest (ceiling) or smallest (floor) possible signed number depending on
whether it was a -128 to 127 cross over (N = 0) or 127 to -128 cross over (N = 1).
Notice that after a signed overflow, bit 7 of the result is always wrong because
Using ceiling and there was a cross over.
floor of signed
numbers to avoid
overflow and
signed add signed sub
underflow
R= X+ M R= X M
end end
2 - Embedded C 2020
2.119
In the C language, statements can be written only within the body of a function;
more specifically, only within compound statements. The normal flow of control
among statements is sequential, proceeding from one statement to the next. Compound
statements and
However, most of the statements in C are designed to alter this sequential flow decision statements
so that algorithms of arbitrary complexity can be implemented. This is done with are used to make
algorithms of
statements that control whether or not other statements execute and, if so, how arbitrary complexity
many times. Furthermore, the ability to write compound statements permits the
writing of a sequence of statements wherever a single, possibly controlled,
statement is allowed. These two features provide the necessary generality to
implement any algorithm, and to do it in a structured way.
2020 2 - Embedded C
2.120
2.6.1 Simple Statements
When one statement controls other statements, a terminator is applied only to the
controlled statements. Thus we would write
Simple statements
are terminated with if (x > 5)
a semicolon x = 0;
else
x++;
with two semicolons, not three. Perhaps one good way to remember this is to
think of statements that control other statements as "super" statements that
"contain" ordinary (simple and compound) statements. Then remember that only
simple statements are terminated. This implies, as stated above, that compound
statements are not terminated with semicolons.
Thus
while (x < 5)
{
func();
x++;
}
is perfectly correct. Notice that each of the simple statements within the
compound statement is terminated.
2 - Embedded C 2020
2.121
2.6.2 Compound Statements
The terms compound statement and block both refer to a collection of statements
that are enclosed in braces to form a single unit. Compound statements have the
form
{ ObjectDeclaration?... Statement?... }
The power of compound statements derives from the fact that one may be placed
anywhere the syntax calls for a statement. Thus any statement that controls other
statements is able to control units of logic of any complexity.
When control passes into a compound statement, two things happen. First, space
is reserved on the stack for the storage of local variables that are declared at the
head of the block. Then the executable statements are processed.
2020 2 - Embedded C
2.122
2.6.3 The if Statement
if (ExpressionList)
Statement1
The if statement
or
if (ExpressionList)
Statement1
else
Statement2
G2 > 100
G2
isGreater();
G2 <= 100
2 - Embedded C 2020
2.123
A 3-wide median filter can be designed using if-else conditional statements.
False(); True();
2020 2 - Embedded C
2.124
2.6.4 The switch Statement
case ConstantExpression:
default:
The terminating colons are required; they heighten the analogy to ordinary
statement labels. Any expression involving only numeric and character constants
and operators is valid in the case prefix.
After evaluating ExpressionList, a search is made for the first matching case
prefix. Control then goes directly to that point and proceeds normally from there.
The switch Other case prefixes and the default prefix have no effect once a case has been
statement uses
case and default selected; control flows through them just as though they were not even there. If
prefixes no matching case is found, control goes to the default prefix, if there is one. In
the absence of a default prefix, the entire compound statement is ignored and
control resumes with whatever follows the switch statement. Only one default
prefix may be used with each switch.
2 - Embedded C 2020
2.125
If it is not desirable to have control proceed from the selected prefix all the way
to the end of the switch block, break statements may be used to exit the block. The break
statement is used to
break statements have the form exit a block
break;
Some examples may help clarify these ideas. Assume Port A is specified as an
output, and bits 3, 2, 1, and 0 are connected to a stepper motor. The switch
statement will first read Port A and AND the data with 0x0000000F (GPIOA_PDOR
& 0x0000000F). If the result is 5, then Port A is set to 6 and control is passed to
the end of the switch (because of the break). Similarly for the other 3
possibilities.
2020 2 - Embedded C
2.126
This next example shows that multiple tests can be performed for the same
condition.
The body of the switch is not a normal compound statement since local
declarations are not allowed in it or in subordinate blocks. This restriction
enforces the C rule that a block containing declarations must be entered through
its leading brace.
2 - Embedded C 2020
2.127
2.6.5 The while Statement
The while statement is one of three statements that determine the repeated
execution of a controlled statement. This statement alone is sufficient for all loop
control needs. The other two merely provide an improved syntax and an execute-
first feature. while statements have the form
In the example
i = 5;
while (i) array[--i] = 0;
elements 0 through 4 of array[] are set to zero. First i is set to 5. Then as long
as it is not zero, the assignment statement is executed. With each execution i is
decremented before being used as a subscript.
2020 2 - Embedded C
2.128
continue and break statements are handy for use with the while statement (also
helpful for the do and for loops). The continue statement has the form
The break statement (described earlier) may also be used to break out of loops.
It causes control to pass on to whatever follows the loop controlling statement.
If while (or any loop or switch) statements are nested, then break affects only
the innermost statement containing the break. That is, it exits only one level of
nesting.
2 - Embedded C 2020
2.129
2.6.6 The for Statement
The for statement also controls loops. It is really just an embellished while in
which the three operations normally performed on loop-control variables
(initialize, test, and modify) are brought together syntactically. It has the form
If more than one expression is given, the right-most expression yields the value
to be tested. If it yields false (zero), control passes on to whatever follows the
for statement. But, if it yields true (non-zero), Statement executes.
ExpressionList3 is then evaluated to adjust the control variable(s) for the next
pass, and the process goes back to step 2. For example,
J = 100;
yes
J < 1000
process();
no
J = J + 1;
2020 2 - Embedded C
2.130
Any of the three expression lists may be omitted, but the semicolon separators
must be kept. If the test expression is absent, the result is always true. Thus
for (;;)
{
...
break;
...
}
As with the while statement, break and continue statements may be used with
equivalent effects. A break statement makes control jump directly to whatever
follows the for statement. A continue skips whatever remains in the controlled
block so that the third ExpressionList3 is evaluated, after which the second
ExpressionList2 is evaluated and tested. In other words, a continue has the
same effect as transferring control directly to the end of the block controlled by
the for.
2 - Embedded C 2020
2.131
2.6.7 The do Statement
Statement is executed.
As with the while and for statements, break and continue statements may be
used. In this case, a continue causes control to proceed directly down to the
while part of the statement for another test of ExpressionList. A break makes
control exit to whatever follows the do statement.
I=100;
do
{
process();
I--;
} while (I > 0);
I = 100;
process();
I = I - 1;
yes
I > 0
no
2020 2 - Embedded C
2.132
The example of the five-element array could be written as
i = 4;
do
{
array[i] = 0;
--i;
} while (i >= 0);
i = 4;
or as
i = 4;
do
array[i--] = 0;
while (i >= 0);
or as
i = 5;
do
array[--i] = 0;
while (i);
The return statement is used within a function to return control to the caller.
Return statements are not always required since reaching the end of a function
always implies a return. But they are required when it becomes necessary to
return from interior points within a function or when a useful value is to be
returned to the caller. return statements have the form
The return
statement return ExpressionList?;
determines the value to be returned by the function. If absent, the returned value
is unpredictable.
2 - Embedded C 2020
2.133
2.6.9 Null Statements
The simplest C statement is the null statement. It has no text, just a semicolon
terminator. As its name implies, it does exactly nothing. Statements that do
Nulls statements
nothing can serve a purpose. As we saw previously, expressions in C can do (statements that do
nothing) can serve a
work beyond that of simply yielding a value. In fact, in C programs, all of the purpose in a
program
work is accomplished by expressions; this includes assignments and calls to
functions that invoke operating system services such as input/output operations.
It follows that anything can be done at any point in the syntax that calls for an
expression.
in which the ((UART2_S1 & TDRE) == 0) controls the execution of the null
statement following. The null statement is just one way in which the C language
follows a philosophy of attaching intuitive meanings to seemingly incomplete
constructs. The idea is to make the language as general as possible by having the
least number of disallowed constructs.
2020 2 - Embedded C
2.134
2.6.10 The goto Statement
where Name is the name of a label which must appear in the same function. It
must also be unique within the function.
short data[10];
void clear(void)
{
short n;
n = 0;
loop:
data[n] = 0;
n++;
if (n == 10)
goto done;
goto loop;
done:
// Semicolon needed because you can't have a label at the
// end of a compound statement
;
}
Notice that labels are terminated with a colon. This highlights the fact that they
are not statements but statement prefixes which serve to label points in the logic
as targets for goto statements. When control reaches a goto, it proceeds directly
from there to the designated label. Both forward and backward references are
allowed, but the range of the jump is limited to the body of the function
containing the goto statement.
goto statements cannot be used in functions which declare locals in blocks which
…should never be Because they violate the structured programming paradigm, goto statements
used
should not be used at all.
2 - Embedded C 2020
2.135
2.6.11 Missing Statements
2020 2 - Embedded C
2.136
2.7 Pointers
The ability to work with memory addresses is an important feature of the C
language. This feature allows programmers the freedom to perform operations
similar to assembly language. Unfortunately, along with the power comes the
potential danger of hard-to-find and serious run-time errors. In many situations,
array elements can be reached more efficiently through pointers than by
subscripting. It also allows pointers and pointer chains to be used in data
structures. Without pointers the run-time dynamic memory allocation and
deallocation using the heap would not be possible. We will also use a format
similar to pointers to develop mechanisms for accessing I/O ports. These added
degrees of flexibility are absolutely essential for embedded systems.
Addresses that can be stored and changed are called pointers. A pointer is really
just a variable that contains an address. Although they can be used to reach
Pointers are
variables that store objects in memory, their greatest advantage lies in their ability to enter into
addresses
arithmetic (and other) operations, and to be changed. Just like other variables,
pointers have a type. In other words, the compiler knows the format (8-bit, 16-
bit, 32-bit, unsigned, signed) of the data pointed to by the address.
Not every address is a pointer. For instance, we can write &var when we want
the address of the variable var. The result will be an address that is not a pointer
Not all addresses
are pointers since it does not have a name or a place in memory. It cannot, therefore, have its
value altered.
Other examples include an array or a structure name. As we shall see in the next
sections, an unsubscripted array name yields the address of the array, and a
structure name yields the address of the structure. But since arrays and structures
cannot be moved around in memory, their addresses are not variable. So,
although such addresses have a name, they do not exist as objects in memory
(the array does, but its address does not) and cannot, therefore, be changed.
2 - Embedded C 2020
2.137
A third example is a character string. A character string yields the address of the
character array specified by the string. In this case the address has neither a name
or a place in memory, so it too is not a pointer.
The syntax for declaring pointers is like that for variables except that pointers
are distinguished by an asterisk that prefixes their names. Listing 2.60 illustrates
several legitimate pointer declarations. Notice, in the third example, that we may
mix pointers and variables in a single declaration, i.e. the variable data and the Pointers are
pointer pt3 are declared in the same statement. Also notice that the data type of declared by placing
a * in front of the
a pointer declaration specifies the type of object to which the pointer refers, not pointer name
the type of the pointer itself. As we shall see, MX creates pointers containing 32-
bit unsigned absolute addresses.
The best way to think of the asterisk is to imagine that it stands for the phrase
"object at" or "object pointed to by". The first declaration in Listing 2.60 then
reads "the object at (pointed to by) pt1 is a 16-bit signed integer".
2020 2 - Embedded C
2.138
2.7.3 Pointer Referencing
We can use the pointer to retrieve data from memory or to store data into
memory. Both operations are classified as pointer references. The syntax for
using pointers is like that for variables except that pointers are distinguished by
an asterisk that prefixes their names. Figure 2.21 to Figure 2.33 illustrate several
legitimate pointer references. In the first figure, the global variables contain
unknown data (actually we know MX will zero global variables). The arrow
identifies the execution location. Assume addresses 0x1FFF007C through
0x1FFF0094 exist in RAM.
Example of
legitimate pointer
references in C address data
0x1FFF007C pt R2
0x1FFF0080 data R3
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
PC ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt
data
buffer
2 - Embedded C 2020
2.139
address data
0x1FFF007C pt R2
0x1FFF0080 data R3 0x1FFF007C
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
PC ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt
data
buffer
address data
0x1FFF007C pt R2 0x1FFF0088
0x1FFF0080 data R3 0x1FFF007C
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
PC str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt
data
buffer
2020 2 - Embedded C
2.140
address data
0x1FFF007C 0x1FFF0088 pt R2 0x1FFF0088
0x1FFF0080 data R3 0x1FFF007C
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
PC ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer
address data
0x1FFF007C 0x1FFF0088 pt R2 0x1FFF0088
0x1FFF0080 data R3 0x1FFF007C
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
PC ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer
2 - Embedded C 2020
2.141
address data
0x1FFF007C 0x1FFF0088 pt R2 0x1FFF0088
0x1FFF0080 data R3 0x1FFF0088
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
PC movw r2,#4660
str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF0088
0x1FFF0084 buffer[0] R4
0x1FFF0088 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
PC str r2,[r3]
ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer
2020 2 - Embedded C
2.142
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF0088
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
PC ldr r3,[r3]
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer 0x00001234
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF007C
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,pt
PC ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer 0x00001234
2 - Embedded C 2020
2.143
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF0088
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,pt
ldr r3,[r3]
PC ldr r2,[r3]
ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer 0x00001234
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF0088
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,pt
ldr r3,[r3]
ldr r2,[r3]
PC ldr r3,data
str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer 0x00001234
2020 2 - Embedded C
2.144
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 data R3 0x1FFF0080
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,pt
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
PC str r2,[r3]
bx lr
.bss
pt 0x1FFF0088
data
buffer 0x00001234
address data
0x1FFF007C 0x1FFF0088 pt R2 0x00001234
0x1FFF0080 0x00001234 data R3 0x1FFF0080
0x1FFF0084 buffer[0] R4
0x1FFF0088 0x00001234 buffer[1] SP
0x1FFF008C buffer[2]
0x1FFF0090 buffer[3]
int buffer[4];
0x1FFF0094 int data;
int *pt;
void main(void )
.text
main: {
ldr r3,pt pt = &buffer[1];
ldr r2,buffer+4 (*pt) = 0x1234;
str r2,[r3] data = (*pt);
ldr r3,pt }
ldr r3,[r3]
movw r2,#4660
str r2,[r3]
ldr r3,pt
ldr r3,[r3]
ldr r2,[r3]
ldr r3,data
str r2,[r3]
PC bx lr
.bss
pt 0x1FFF0088
data 0x00001234
buffer 0x00001234
2 - Embedded C 2020
2.145
The expression &buffer[1] returns the address of the second 32-bit element of
the buffer (0x1FFF0088). Therefore the line pt=&buffer[1]; makes pt point to
buffer[1].
2020 2 - Embedded C
2.146
2.7.4 Memory Addressing
The size of a pointer depends on the architecture of the CPU and the
implementation of the C compiler. The K64 employs an absolute memory
With a 32-bit CPU,
addressing is “flat” addressing scheme in which an effective address is composed simply of a single
and occurs with
32-bit addresses
32-bit unsigned value.
Types of memory in
object code from our .elf file and our program is allowed only to read the data.
the K64 Table 2.40 shows the various types of memory available in the K64
microcontroller. The RAM contains temporary information that is lost when the
power is shut off. This means that all variables allocated in RAM must be
explicitly initialized at run time by the software. If the embedded system includes
a separate battery for the RAM, then information is not lost when the main power
is removed. EEPROM is a technology that allows individual small sectors
(typically 4 KiB) to be erased and bytes individually written. Most
microcontrollers now have non-volatile Flash ROM as the main program
memory, which has bulk erasure (typically 4 KiB) and individual write
capability at the byte level. The one-time-programmable (OTP) ROM is a simple
non-volatile storage technology used in large volume products that can be
programmed only once by the semiconductor manufacturer.
2 - Embedded C 2020
2.147
2020 2 - Embedded C
2.148
In an embedded application, we usually put global variables, the heap, and local
The type of memory
dictates its usage variables in RAM because these types of information can change during
execution. When software is to be executed on a regular computer, the machine
instructions are usually read from a mass storage device (like a disk) and loaded
into memory. Because the embedded system usually has no mass storage device,
the machine instructions and fixed constants must be stored in non-volatile
memory. If there is both EEPROM and Flash on our microcontroller, we put
some fixed constants in EEPROM and some in Flash. If it is information that we
may wish to change in the future, we could put it in EEPROM. Examples include
language-specific strings, calibration constants, finite state machines, and
system ID numbers. This allows us to make minor modifications to the system
by reprogramming the EEPROM without throwing the chip away. For a project
with a large volume it will be cost effective to place the machine instructions in
OTPROM.
2 - Embedded C 2020
2.149
2.7.5 Pointer Arithmetic
A major difference between addresses and ordinary variables or constants has to
do with the interpretation of addresses. Since an address points to an object of
some particular type, adding one (for instance) to an address should direct it to
the next object, not necessarily the next byte. If the address points to integers,
then it should end up pointing to the next integer. But, since integers occupy four
bytes, adding one to an integer address must actually increase the address by
four. Likewise, if the address points to short integers, then adding one to an
address should end up pointing to the next short integer by increasing the address
by two. A similar consideration applies to subtraction. In other words, values Pointer arithmetic
takes into account
added to or subtracted from an address must be scaled according to the size of the size of the data
the objects being addressed. This is done automatically by the compiler, and being pointed to
saves the programmer a lot of thought and makes programs less complex since
the scaling need not be coded explicitly. The scaling factor for integers is four;
the scaling factor for short integers is two; the scaling factor for characters is
one. Therefore, character addresses do not receive special handling. It should be
obvious that when we define structures of other sizes, the appropriate factors
would have to be used.
When an address is operated on, the result is always another address of the same
type. Thus, if ptr is a signed 32-bit integer pointer, then ptr+1 also points to a Type is preserved in
pointer arithmetic
signed 32-bit integer.
2020 2 - Embedded C
2.150
2.7.6 Pointer Comparisons
One major difference between pointers and other variables is that pointers are
always considered to be unsigned. This should be obvious since memory
addresses are not signed. This property of pointers (actually all addresses)
ensures that only unsigned operations will be performed on them. It further
Pointers are always
unsigned means that the other operand in a binary operation will also be regarded as
unsigned (whether or not it actually is). In the following example, pt1 and pt2[5]
return the current values of the addresses. For instance, if the array pt2[]
contains addresses, then it would make sense to write:
which performs an unsigned comparison since pt1 and pt2 are pointers. Thus, if
pt2[5] contains 0x1FFFF000 and pt1 contains 0x1FFF1000, the expression will
yield false, since 0x1FFFF000 is a higher unsigned value than 0x1FFF1000.
The address of zero It makes no sense to compare a pointer to anything but another address or zero.
is reserved for NULL
– a pointer that C guarantees that valid addresses can never be zero, so that particular value is
doesn’t yet point to
anything useful in representing the absence of an address in a pointer.
2 - Embedded C 2020
2.151
2.7.7 A FIFO Queue Example
To illustrate the use of pointers we will design a two-pointer FIFO. The first-in
first-out circular queue (FIFO) is also useful for data flow problems. It is a very
common data structure used for I/O interfacing. The order preserving data
structure temporarily saves data created by the source (producer) before it is
processed by the sink (consumer). The class of FIFOs studied in this section will
be statically allocated global structures. Because they are global variables, it
means they will exist permanently and can be shared by more than one program.
The advantage of using a FIFO structure for a data flow problem is that we can
decouple the source and sink processes. Without the FIFO we would have to
produce 1 piece of data, then process it, produce another piece of data, then
process it. With the FIFO, the source process can continue to produce data
without having to wait for the sink to finish processing the previous data. This
decoupling can significantly improve system performance.
GetPt points to the data that will be removed by the next call to FIFO_Get(), and
PutPt points to the empty space where the data will be stored by the next call to
FIFO_Put(). If the FIFO is full when FIFO_Put() is called then the subroutine
should return a full error. Similarly, if the FIFO is empty when FIFO_Get() is
called, then the subroutine should return an empty error. The PutPt and GetPt
pointers must be wrapped back up to the top when they reach the bottom.
2020 2 - Embedded C
2.152
Four FIFO_Put()
operations…
queue
PutPt GetPt
queue
FIFO_Put(1) 1 GetPt
PutPt
queue
FIFO_Put(1) 1 GetPt
FIFO_Put(2) 2
PutPt
queue
FIFO_Put(1) 1 GetPt
FIFO_Put(2) 2
FIFO_Put(3)
3
PutPt
queue
FIFO_Put(1) 1 GetPt
FIFO_Put(2) 2
FIFO_Put(3)
3
FIFO_Put(4)
PutPt 4
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3)
FIFO_Put(4)
3 GetPt
4
PutPt
2 - Embedded C 2020
2.153
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3)
FIFO_Put(4)
3 GetPt
FIFO_Put(5) 4
5
PutPt
Figure 2.36 – FIFO example showing the wrapping of pointers – step 3 A FIFO_Put()
followed by a
FIFO_Get()
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4)
FIFO_Put(5) 4 GetPt
5
PutPt
Two FIFO_Put()
operations…
queue
FIFO_Put(1) PutPt FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4)
FIFO_Put(5) 4 GetPt
FIFO_Put(6) 5
6
queue
FIFO_Put(1) 7 FIFO_Get()->1
FIFO_Put(2) PutPt FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4)
FIFO_Put(5) 4 GetPt
FIFO_Put(6) 5
FIFO_Put(7)
6
2020 2 - Embedded C
2.154
queue
FIFO_Put(1) 7 FIFO_Get()->1
Two FIFO_Get() FIFO_Get()->2
FIFO_Put(2) PutPt
operations… FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4) FIFO_Get()->4
FIFO_Put(5)
FIFO_Put(6) 5 GetPt
FIFO_Put(7) 6
queue
FIFO_Put(1) 7 FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
PutPt FIFO_Get()->3
FIFO_Put(3)
FIFO_Put(4) FIFO_Get()->4
FIFO_Put(5) FIFO_Get()->5
FIFO_Put(6)
FIFO_Put(7) 6 GetPt
Two FIFO_Put()
operations…
queue
FIFO_Put(1) 7 FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
8
FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4)
PutPt FIFO_Get()->4
FIFO_Put(5) FIFO_Get()->5
FIFO_Put(6)
FIFO_Put(7) 6
FIFO_Put(8)
GetPt
queue
FIFO_Put(1) 7 FIFO_Get()->1
FIFO_Put(2) 8 FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
9
FIFO_Put(4) FIFO_Get()->4
FIFO_Put(5) PutPt FIFO_Get()->5
FIFO_Put(6)
FIFO_Put(7) 6
FIFO_Put(8)
GetPt
FIFO_Put(9)
2 - Embedded C 2020
2.155
Finally, four
FIFO_Get()
queue
FIFO_Get()->1
operations that
FIFO_Put(1) 7 GetPt
FIFO_Put(2) 8
FIFO_Get()->2 empty the queue
FIFO_Put(3) FIFO_Get()->3
9 FIFO_Get()->4
FIFO_Put(4)
FIFO_Put(5) PutPt FIFO_Get()->5
FIFO_Put(6) FIFO_Get()->6
FIFO_Put(7)
FIFO_Put(8)
FIFO_Put(9)
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
8 GetPt
FIFO_Put(3) FIFO_Get()->3
9 FIFO_Get()->4
FIFO_Put(4)
FIFO_Put(5) PutPt FIFO_Get()->5
FIFO_Put(6) FIFO_Get()->6
FIFO_Put(7) FIFO_Get()->7
FIFO_Put(8)
FIFO_Put(9)
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
9 GetPt FIFO_Get()->4
FIFO_Put(4)
FIFO_Put(5) PutPt FIFO_Get()->5
FIFO_Put(6) FIFO_Get()->6
FIFO_Put(7) FIFO_Get()->7
FIFO_Put(8) FIFO_Get()->8
FIFO_Put(9)
queue
FIFO_Put(1) FIFO_Get()->1
FIFO_Put(2) FIFO_Get()->2
FIFO_Put(3) FIFO_Get()->3
FIFO_Put(4) FIFO_Get()->4
FIFO_Put(5)
PutPt GetPt FIFO_Get()->5
FIFO_Put(6) FIFO_Get()->6
FIFO_Put(7) FIFO_Get()->7
FIFO_Put(8) FIFO_Get()->8
FIFO_Put(9) FIFO_Get()->9
There are two mechanisms to determine whether the FIFO is empty or full. A
simple method is to implement a counter containing the number of bytes
currently stored in the FIFO. FIFO_Get() would decrement the counter and
FIFO_Put() would increment the counter. The second method is to prevent the
FIFO from being completely full. For example, if the FIFO had 100 bytes
allocated, then the FIFO_Put() subroutine would allow a maximum of 99 bytes
to be stored. If there were already 99 bytes in the FIFO and another PUT were
called, then the FIFO would not be modified and a full error would be returned.
In this way if PutPt equals GetPt at the beginning of FIFO_Get(), then the FIFO
is empty. Similarly, if PutPt + 1 equals GetPt at the beginning of FIFO_Put(),
then the FIFO is full. Be careful to wrap the PutPt + 1 before comparing it to
GetPt. This second method does not require the length to be stored or calculated.
2020 2 - Embedded C
2.156
// Pointer implementation of the FIFO
#define FIFO_SIZE 10 // Max number of 8-bit data in the FIFO
void FIFO_Init(void)
{
// Make atomic, entering critical section
EnterCritical();
PutPt = GetPt = FIFO; // Empty when PutPt == GetPt
ExitCritical(); // End critical section
}
2 - Embedded C 2020
2.157
The EnterCritcial() macro is defined to save the state of the global interrupt
enable bit and disable interrupts. This prevents another thread from interfering
with the FIFO operation. The ExitCrtical() macro restores the state of the
global interrupt enable bit.
Since these routines have read / modify / write accesses to global variables the
three functions (FIFO_Init(), FIFO_Put(), FIFO_Get()) are themselves not re-
entrant. Consequently interrupts are temporarily disabled, to prevent one thread
You have to be
from re-entering these FIFO functions. One advantage of this pointer careful when
multiple threads are
implementation is that if you have a single thread that calls FIFO_Get() (e.g., the using the same
resource
main program) and a single thread that calls FIFO_Put() (e.g., the serial port
receive interrupt handler), then this FIFO_Put() function can interrupt this
FIFO_Get() function without loss of data. So in this particular situation,
interrupts would not have to be disabled. It would also operate properly if there
were a single interrupt thread calling FIFO_Get() (e.g., the serial port transmit
interrupt handler) and a single thread calling FIFO_Put() (e.g., the main
program.) On the other hand, if the situation is more general, and multiple
threads could call FIFO_Put() or multiple threads could call FIFO_Get(), then the
interrupts would have to be temporarily disabled as shown.
2020 2 - Embedded C
2.158
2.7.8 I/O Port Access
Even though the mechanism to access I/O ports technically does not fit the
definition of pointer, it is included in this section because it involves addresses.
The format used by the MX compiler fits the following model. The following
listing shows three 32-bit K64 I/O ports. The line FTM0_C5SC = 0x80; generates
a 32-bit I/O write operation to the port at address 0x4003800C. The FTM0_CNT on
the right hand side of the assignment statement generates a 32-bit I/O read
operation from the port at address 0x40038004. The FTM0_C5V on the left hand
side of the assignment statement generates a 32-bit I/O write operation from the
port at address 0x40038030. The FTM0_C5SC inside the while loop generates
repeated 32-bit I/O read operations until bit 7 is set.
It was mentioned earlier that the volatile modifier will prevent the compiler
from optimizing I/O statements, i.e., these examples would not work if the
compiler read FTM0_C5SC once, then used the same data over and over inside the
while loop.
To understand this syntax we break it into parts. Starting on the right is the
absolute address of the I/O port. For example the K64 FTM0_CNT register is at
location 0x40038004. The parentheses are necessary because the definition might
be used in an arithmetic calculation. For example the following two lines are
quite different:
In the second (incorrect) case the addition 0x01023 + 100 is performed on the
address, not the data. The next part of the definition is a type casting. C allows
2 - Embedded C 2020
2.159
you to change the type of an expression. For example (unsigned char volatile
*) specifies that 0x1023 is an address that points at an
8-bit unsigned char. The * at the beginning of the definition causes the data to
be fetched from the I/O port if the expression exists on the right-hand side of an
assignment statement. The * also causes the data to be stored at the I/O port if
the expression is on the left-hand side of the assignment statement. In this last
way, I/O port accesses are indeed similar to pointers.
For example the previous example could have been implemented as:
This function first sets the three I/O pointers then accesses the I/O ports
indirectly through the pointers.
You need to be careful when using pointer variables to I/O ports on the K64. If
a global pointer variable to an I/O port is uninitialised, the C startup code will
set it to zero. In C, the NULL pointer is defined as address 0. In the K64, the initial
stack pointer (held in Flash memory) has address 0. Therefore, if you
accidentally try and write to a dereferenced NULL pointer you will generate a
HardFault exception (the program will “crash”).
2020 2 - Embedded C
2.160
yields an integer value, even a general expression. Although arrays represent one
of the simplest data structures, they have wide-spread usage in embedded
systems.
Strings are similar to arrays with just a few differences. Usually, the array size
is fixed, while strings can have a variable number of elements. Arrays can
contain any data type (char, short, int, even other arrays) while strings are
usually ASCII characters terminated with a NULL (0) character. In general we
allow random access to individual array elements. On the other hand, we usually
process strings sequentially character by character from start to end. Since these
differences are a matter of semantics rather than specific limitations imposed by
the syntax of the C programming language, the descriptions in this section apply
equally to data arrays and character strings. String literals were discussed earlier;
in this section we will define data structures to hold our strings. In addition, C
has a rich set of predefined functions to manipulate strings.
data[9] = 0;
for instance, sets the tenth element of data to zero. The array subscript can be
any expression that results in a 32-bit integer.
The following for -loop clears 100 elements of the array data to zero:
2 - Embedded C 2020
2.161
Multidimensional Arrays
int array2D[ROWS][COLUMNS];
where ROWS and COLUMNS are constants. This defines a two-dimensional array.
Reading the subscripts from left to right, array2D is an array of length ROWS, each
element of which is an array of COLUMNS integers.
As programmers we may assign any logical meaning to the first and second
subscripts. For example we could consider the first subscript as the row and the
second as the column. Then, the statement:
ThePosition = position[3][5];
copies the information from the 4th row and 6th column into the variable
ThePosition.
If the array has three dimensions, then three subscripts are specified when
referencing. Again we may assign any logical meaning to the various subscripts.
For example we could consider the first subscript as the x coordinate, the second
subscript as the y coordinate and the third subscript as the z coordinate. Then,
the statement:
humidity[2][3][4] = 100;
2020 2 - Embedded C
2.162
2.8.2 Array Declarations
Just like any variable, arrays must be declared before they can be accessed. The
number of elements in an array is determined by its declaration. Appending a
constant expression in square brackets to a name in a declaration identifies the
name as the name of an array with the number of elements indicated. Multi-
dimensional arrays require multiple sets of brackets. The examples in Listing
2.66 are valid declarations:
Notice in the third example that ordinary variables may be declared together with
arrays in the same statement. In fact array declarations obey the syntax rules of
ordinary declarations, as described in previous sections, except that certain
names are designated as arrays by the presence of a dimension expression.
Notice the size of the external array, buffer[], is not given. This leads to an
important point about how C deals with array subscripts. The array dimensions
are only used to determine how much memory to reserve. It is the
programmer's responsibility to stay within the proper bounds. In particular,
you must not let the subscript become negative or above N-1, where N is the size
of the array.
Another situation in which an array's size need not be specified is when the array
elements are given initial values. In this case, the compiler will determine the
size of such an array from the number of initial values.
2 - Embedded C 2020
2.163
2.8.3 Array References
void Set(void)
{
x = data[0]; // set x equal to the first element of data
x = *data; // set x equal to the first element of data
pt = data; // set pt to the address of data
pt = &data[0]; // set pt to the address of data
x = data[3]; // set x equal to the fourth element of data
x = *(data + 3); // set x equal to the fourth element of data
pt = data + 3; // set pt to the address of the fourth element
pt = &data[3]; // set pt to the address of the fourth element
}
The previous examples suggest that pointers and array names might be used
interchangeably, and, in many cases, they may. C will let us subscript pointers
and also use array names as addresses. In the following example, the pointer pt
contains the address of an array of integers. Notice the expression pt[2] is
equivalent to *(pt+2):
void Set(void)
{
pt = data; // set pt to the address of data
data[2] = 5; // set the third element of data to 5
pt[2] = 5; // set the third element of data to 5
*(pt + 2) = 5; // set the third element of data to 5
}
2020 2 - Embedded C
2.164
It is important to realize that although C accepts unsubscripted array names as
addresses, they are not the same as pointers. In the following example, we cannot
place the unsubscripted array name on the left-hand-side of an assignment
statement:
void Set(void)
{
data = buffer; // illegal assignment
}
The array, like any object, has a fixed home in memory; therefore, its address
cannot be changed. We say that array is not an lvalue; i.e. it cannot be used on
the left side of an assignment operator (nor may it be operated on by increment
or decrement operators). It simply cannot be changed. Not only does this
assignment make no sense, it is physically impossible because an array address
is not a variable. There is no place reserved in memory for an array's address to
reside, only the elements.
Since a pointer may point to any element of an array, not just the first one, it
follows that negative subscripts applied to pointers might well yield array
references that are in bounds. This sort of thing might be useful in situations
where there is a relationship between successive elements in an array and it
becomes necessary to reference an element preceding the one being pointed to.
In the following example, data is an array containing time-dependent (or space-
dependent) information. If pt points to an element in the array, pt[-1] is the
previous element and pt[1] is the following one. The function calculates the
second derivative using a simple discrete derivative.
2 - Embedded C 2020
2.165
short *pt, data[100]; // a pointer and an array
void CalcSecondDerivative(void)
{
short d2Vdt2;
As we have seen, addresses (pointers, array names, and values produced by the
address operator) may be used freely in expressions. This one fact is responsible
for much of the power of C.
As with pointers, all addresses are treated as unsigned quantities. Therefore, only
unsigned operations are performed on them. Of all the arithmetic operations that
could be performed on addresses, only two make sense: displacing an address
by a positive or negative amount, and taking the difference between two
addresses. All others, though permissible, yield meaningless results.
2020 2 - Embedded C
2.166
2.8.7 String functions in string.h
This function finds the first occurrence of the byte c (converted to an unsigned
char) in the initial size bytes of the object beginning at block. The return value
is a pointer to the located byte, or a NULL pointer if no match was found.
2 - Embedded C 2020
2.167
Compare Two Blocks of Memory
int memcmp(const void* a1, const void* a2, size_t size);
The function memcmp compares the size bytes of memory beginning at a1 against
the size bytes of memory beginning at a2. The value returned has the same sign
as the difference between the first differing pair of bytes, a1[n]-a2[n]
(interpreted as unsigned char objects, then promoted to int). If the contents of
the two blocks are equal, memcmp returns 0.
The memcpy function copies size bytes from the object beginning at src into the
object beginning at dst. The behaviour of this function is undefined if the two
arrays src and dst overlap. The value returned by memcpy is the value of dst.
memmove copies the size bytes at src into the size bytes at dst, even if those two
blocks of space overlap. In the case of overlap, memmove is careful to copy the
original values of the bytes in the block at src, including those bytes which also
belong to the block at dst. The value returned by memmove is the value of dst.
This function copies the value of c (converted to an unsigned char) into each of
the first size bytes of the object beginning at block. It returns the value of block.
2020 2 - Embedded C
2.168
The remaining functions are string-handling routines.
Concatenate Strings
char* strcat(char* dst, const char* src);
Assuming the two pointers are directed at two null-terminated strings, strcat
will append a copy of the string pointed to by pointer src, placing it at the end
of the string pointed to by pointer dst. The pointer dst is returned. It is the
programmer's responsibility to ensure the destination buffer is large enough.
This function has undefined results if the strings overlap.
Assuming the two pointers are directed at two null-terminated strings, strcmp
will return a negative value if the string pointed to by s1 is lexicographically less
than the string pointed to by s2. The return value will be zero if they match, and
positive if the string pointed to by s1 is lexicographically greater than the string
pointed to by s2. A consequence of the ordering used by strcmp is that if s1 is
an initial substring of s2, then s1 is considered to be “less than” s2.
2 - Embedded C 2020
2.169
Copy a String
char* strcpy(char* dst, const char* src);
The string function strcspn (string complement span) will compute the length
of the maximal initial substring within the string pointed to by string that has
no characters in common with the string pointed to by stopset. For example the
following call returns the value 5:
A common application of this routine is parsing for tokens. The first parameter
is a line of text and the second parameter is a list of delimiters (e.g., space,
semicolon, colon, star, return, tab and linefeed). The function returns the length
of the first token (i.e., the size of label).
The string function strlen returns the length of the string pointed to by pointer
string. The length is the number of characters in the string not counting the null-
termination.
2020 2 - Embedded C
2.170
Append Characters from a String
char* strncat(char* dst, const char* src, size_t size);
This function is similar to strcat. Assuming the two pointers are directed at two
null-terminated strings, strncat will append a copy of the string pointed to by
pointer src, placing it the end of the string pointed to by pointer dst. The
parameter size limits the number of characters, not including the null, that will
be copied. The pointer dst is returned. It is the programmer's responsibility to
ensure the destination buffer is large enough. The behaviour of strncat is
undefined if the strings overlap.
This function is similar to strcmp. Assuming the two pointers are directed at two
null-terminated strings, strncmp will return a negative value if the string pointed
to by s1 is lexicographically less than the string pointed to by s2. The return
value will be zero if they match, and positive if the string pointed to by s1 is
lexicographically greater than the string pointed to by s2. The parameter size
limits the number of characters, not including the null, that will be compared.
For example, the following function call will return a zero because the first 5
characters are the same:
2 - Embedded C 2020
2.171
Locate Characters in a String
char* strpbrk(const char* string, const char* stopset);
This function strpbrk (string pointer break) will search the string pointed to by
string for the first instance of any of the characters in the string pointed to by
stopset. A pointer to the found character is returned. If the search fails to find
any characters of the string pointed to by stopset in the string pointed to by
string, then a null pointer is returned. For example the following call returns a
pointer to the colon:
The function strrchr will search the string pointed to by string from the right
for the first instance of the character in c. A pointer to the found character is
returned. If the search fails to find an occurrence of the character c (converted to
a char) in the string pointed to by string, then a null pointer is returned. For
example the following calls set pt1 to point to the 'm' in movw and pt2 to point to
the second 'm' in ;comment:
Notice that strchr searches from the left while strrchr searches from the right.
2020 2 - Embedded C
2.172
Get Span of Character Set in String
size_t strspn(const char* string, const char* skipset);
The strspn (string span) function returns the length of the maximal initial
substring of string that consists entirely of characters that are members of the
set specified by the string skipset. The order of the characters in skipset is not
important.
In the following example the second string contains the valid set of hexadecimal
digits.
The function call will return 6 because there is a valid 6-digit hexadecimal string
at the start of the line.
Locate Substring
char* strstr(const char* haystack, const char* needle);
The function strstr will search the string pointed to by haystack from the left
for the first instance of the string pointed to by needle. A pointer to the found
substring within the first string is returned. If the search fails to find a match,
then a null pointer is returned. For example, the following call sets pt to point to
the 'm' in movw:
2 - Embedded C 2020
2.173
2.8.8 A FIFO Queue Example using Indices
2020 2 - Embedded C
2.174
The following FIFO implementation uses two indices and a counter.
void FIFO_Init(void)
{
PutI = GetI = Size = 0; // Empty when Size==0
}
Size++;
FIFO[PutI++] = data; // Put data into FIFO
if (PutI == FIFO_SIZE)
PutI = 0; // Wrap
return 1; // Successful
}
2 - Embedded C 2020
2.175
2.9 Structures
A structure is a collection of variables that share a single name. In an array, each
element has the same format. With structures we specify the types and names of
each of the elements or members of the structure. The individual members of a
structure are referenced by their subname. Therefore, to access data stored in a
structure, we must give both the name of the collection and the name of the
element. Structures are one of the most powerful features of the C language. In
the same way that functions allow us to extend the C language to include new
operations, structures provide a mechanism for extending the data types. With
structures we can add new data types derived from an aggregate of existing
types.
struct theport
{
// 0 for I/O, 1 for in only, -1 for out only
int mode;
// pointer to its output address
uint32_t volatile* outAddress;
// pointer to its input address
uint32_t volatile* inAddress;
// pointer to its data direction register
uint32_t volatile* ddr;
};
The above declaration does not create any variables or allocate any space.
Therefore to use a structure we must define a global or local variable of this type.
The tagname (theport) along with the keyword struct can be used to define
variables of this new data type:
2020 2 - Embedded C
2.176
The previous line defines the four variables and allocates 16 bytes for each
variable. If you knew you needed just three copies of structures of this type, you
could have defined them as:
struct theport
{
int mode;
uint32_t volatile* outAddress;
uint32_t volatile* inAddress;
uint32_t volatile* ddr;
} PortA, PortB, PortC;
Definitions like the above are hard to extend, so to improve code reuse we can
use typedef to actually create a new data type (called port in the example below)
that behaves syntactically like char, int, short etc.
struct theport
{
int mode; // 0 for I/O, 1 for in only, -1 for out only
uint32_t volatile* outAddress; // out address
uint32_t volatile* inAddress; // in address
uint32_t volatile* ddr; // data direction register
};
Once we have used typedef to create port, we don't need access to the name
theport anymore. Consequently, some programmers use the following short-
cut:
typedef struct
{
int mode; // 0 for I/O, 1 for in only, -1 for out only
uint32_t volatile* outAddress; // out address
uint32_t volatile* inAddress; // in address
uint32_t volatile* ddr; // data direction register
} port;
2 - Embedded C 2020
2.177
2.9.2 Accessing Members of a Structure
We need to specify both the structure name (name of the variable) and the
member name when accessing information stored in a structure. The following
examples show accesses to individual members:
The syntax can get a little complicated when a member of a structure is another
structure as illustrated in the next example:
typedef struct
{
int x1, y1; // starting point
int x2, y2; // starting point
char color; // color
} line;
typedef struct
{
line L1, L2; // two lines
char direction;
} path;
path p; // global
void Setup(void)
{
line myLine;
path q;
p.L1.x1 = 5; // black line from 5,6 to 10,12
p.L1.y1 = 6;
p.L1.x2 = 10;
p.L1.y2 = 12;
p.L1.color = 255;
p.L2={5, 6, 10, 12, 255}; // black line from 5,6 to 10,12
p.direction = -1;
myLine = p.L1;
q = {{0, 0, 5, 6, 128}, {5, 6, -10, 6, 128}, 1};
q = p;
}
2020 2 - Embedded C
2.178
The local variable declaration line myLine; will allocate 17 bytes on the stack
while path q; will allocate 35 bytes on the stack. In actuality most C compilers
in an attempt to maintain addresses on word boundaries will actually allocate 20
and 44 bytes respectively. In particular, the K64 executes faster out of external
memory if 32-bit accesses occur on word-aligned addresses. For example, a 32-
bit data access to an external odd address requires two bus cycles, while a 32-bit
data access to an external word-aligned address requires only one bus cycle.
There is no particular odd-address speed penalty for K64 internal addresses
(internal RAM or Flash). Notice that the expression p.L1.x1 is of the type int,
the term p.L1 has the type line, while just p has the type path. The expression q
= p; will copy the entire 35 bytes that constitute the structure from p to q.
Just like any variable, we can specify the initial value of a structure at the time
of its definition:
2 - Embedded C 2020
2.179
To place a structure in Flash memory, we define it as a global constant. In the
following example the structure fsm[3] will be allocated and initialized in Flash
memory. The linked structure of a finite state machine is a good example of a
Flash-based structure.
TState FSM[3] =
{
{
0x34, 2000, // stop 1 ms
{0xFF, 0xF0, 0x27, 0x00},
{0x51, 0xA0, 0x07, 0x00},
{Turn, Stop, Turn, Bend}
},
{
0xB3, 5000, // turn 2.5 ms
{0x80, 0xF0, 0x00, 0x00},
{0x00, 0x90, 0x00, 0x00},
{Bend, Stop, Turn, Turn}
},
{
0x75, 4000, // bend 2 ms
{0xFF, 0x0F, 0x01, 0x00},
{0x12, 0x05, 0x00, 0x00},
{Stop, Stop, Turn, Stop}
}
};
2020 2 - Embedded C
2.180
2.9.4 Using pointers to access structures
Just like other variables we can use pointers to access information stored in a
structure. The syntax is illustrated in the following examples:
void Setup(void)
{
path* ppt;
ppt = &p; // pointer to an existing global variable
ppt->L1.x1 = 5; // black line from 5,6 to 10,12
ppt->L1.y1 = 6;
ppt->L1.x2 = 10;
ppt->L1.y2 = 12;
ppt->L1.color = 255;
ppt->L2 = {5, 6, 10, 12, 255};
ppt->direction = -1;
(*ppt).direction = -1;
}
2 - Embedded C 2020
2.181
As an another example of pointer access, consider the finite state machine
controller for the fsm[3] structure shown previously. The state machine is
illustrated below, along with the program.
01010001
Stop XX0XX111 Turn
1010XXXX 0x34 1001XXXX 0xB3 rest
2000 rest 5000
XXXXXXX0
0XXXXXXX
00010010 Bend
XXXX0101 0x75
rest 4000
while(1)
{
// 1) output
GPIOA_PDOR = pt->out;
// Time (500 ns each) to wait
startTime = FTM0_CNT;
// 2) wait
while ((FTM0_CNT - startTime) <= pt->wait);
// 3) input
input = GPIOB_PDIR;
for (int i = 0; i < 4; i++)
if ((input & pt->andMask[i]) == pt->equMask[i])
{
// 4) next depends on input
pt = pt->next[i];
i = 4;
}
}
}
2020 2 - Embedded C
2.182
2.9.5 Passing Structures to Functions
Like any other data type, we can pass structures as parameters to functions.
Because most structures occupy a large number of bytes, it makes more sense to
pass the structure by reference rather than by value. In the following "call by
value" example, the entire 16-byte structure is copied on the stack when the
function is called:
When we use "call by reference", a pointer to the structure is passed when the
function is called.
port PortC =
{
0,
(uint32_t volatile *)(0x400FF080),
(uint32_t volatile *)(0x400FF090),
(uint32_t volatile *)(0x400FF094)
};
2 - Embedded C 2020
2.183
unsigned int Input(port* ppt)
{
return (*ppt->inAddress);
}
void main(void)
{
unsigned int myData;
MakeInput(&PortC);
MakeOutput(&PortC);
Output(&PortC, 0);
myData = Input(&PortC);
}
TNode* HeadPt;
HeadPt
100 200 300
0
2020 2 - Embedded C
2.184
In order to store more data in the structure, we will first create a new node then
link it into the list. The routine StoreData will return a true value if successful.
#include <stdlib.h>
Listing 2.79 – Code to add a node at the beginning of a linear linked list
In order to search the list we start at the HeadPt, and stop when the pointer
becomes NULL. The routine Search will return a pointer to the node if found, and
it will return a null-pointer if the data is not found.
pt = HeadPt;
while (pt)
{
if (pt->data == info)
return pt;
pt = pt->next; // link to next
}
return pt; // not found
}
2 - Embedded C 2020
2.185
To count the number of elements, we again start at the HeadPt, and stop when
the pointer becomes NULL. The routine Count will return the number of elements
in the list.
count = 0;
pt = HeadPt;
while (pt)
{
count++;
pt = pt->next; // link to next
}
return count;
}
Listing 2.81 – Code to count the number of nodes in a linear linked list
If we wanted to maintain a sorted list, then we can insert new data at the proper
place, in between data elements smaller and larger than the one we are inserting.
In the following figure we are inserting the element 250 in between elements
200 and 300.
after 250
HeadPt
100 200 300
0
2020 2 - Embedded C
2.186
There are 4 cases to consider. In case 1, the list is initially empty, and this new
element is the first and only one. In case 2, the new element is inserted at the
front of the list because it has the smallest data value. Case 3 is the general case
depicted in the previous figure. In this situation, the new element is placed in
between firstPt and secondPt. In case 4, the new element is placed at the end
of the list because it has the largest data value.
// case 1
if (HeadPt == 0)
{
newPt->next = HeadPt; // only element
HeadPt = newPt;
return 1;
}
// case 2
if (info <= HeadPt->data)
{
newPt->next = HeadPt; // first element in list
HeadPt = newPt;
return 1;
}
// case 3
firstPt = HeadPt; // search from beginning
secondPt = HeadPt->next;
while (secondPt)
{
if (info <= secondPt->data)
{
newPt->next = secondPt; // insert element here
firstPt->next = newPt;
return 1;
}
firstPt = secondPt; // search next
secondPt = secondPt->next;
}
// case 4
newPt->next = secondPt; // insert at end
firstPt->next = newPt;
return 1;
}
return 0; // out of memory
}
2 - Embedded C 2020
2.187
The following function will search and remove a node from the linked list. Case
1 is the situation in which an attempt is made to remove an element from an
empty list. The return value of zero signifies the attempt failed. In case 2, the
first element is removed. In this situation the HeadPt must be updated to now
point to the second element. It is possible the second element does not exist,
because the list originally had only one element. This is okay because in this
situation HeadPt will be set to NULL signifying the list is now empty. Case 3 is the
general situation in which the element at secondPt is removed. The element
before, firstPt, is now linked to the element after. Case 4 is the situation where
the element that was requested to be removed did not exist. In this case, the return
value of zero signifies the request failed.
// case 1
if (HeadPt == 0)
return 0; // empty list
// case 2
firstPt = HeadPt;
secondPt = HeadPt->next;
if (info == HeadPt->data)
{
HeadPt = secondPt; // remove first element in list
free(firstPt); // return unneeded memory to heap
return 1;
}
// case 3
while (secondPt)
{
if (secondPt->data == info)
{
firstPt->next = secondPt->next; // remove this one
free(secondPt); // return unneeded memory to heap
return 1;
}
firstPt = secondPt; // search next
secondPt = secondPt->next;
}
// case 4
return 0; // not found
}
Listing 2.83 – Code to remove a node from a sorted linear linked list
2020 2 - Embedded C
2.188
2.9.7 Example of a Huffman Code
When information is stored or transmitted there is a fixed cost for each bit. Data
compression and decompression provide a means to reduce this cost without loss
of information. If the sending computer compresses a message before
transmission and the receiving computer decompresses it at the destination, the
effective bandwidth is increased. In particular, this example introduces a way to
process bit streams using Huffman encoding and decoding. A typical application
is illustrated by the following flow diagram.
ASCII text
Four score Huffman bit stream
and seven
years ago... encode() 11001100110001100110000110...
The Huffman code is similar to the Morse code in that they both use short
patterns for letters that occur more frequently. In regular ASCII, all characters
are encoded with the same number of bits (8). Conversely, with the Huffman
code, we assign codes where the number of bits to encode each letter varies. In
this way, we can use short codes for letters like "e t a o i n" (that have a higher
probability of occurrence) and long codes for seldom used consonants like "j x
q z" (that have a lower probability of occurrence).
2 - Embedded C 2020
2.189
To illustrate the encode-decode operations, consider the following Huffman
code for the letters M, I, P and S. S is encoded as "0", I as "10", P as "110" and
M as "111". We can store a Huffman code as a binary tree.
0
S
1 0
I
1 0
P
1
M
MISSISSIPPI
Of course, this Huffman code can only handle 4 letters, while the ASCII code
has 128 possibilities, so it is not fair to claim we have an 80 to 21 bit saving.
Nevertheless, for information that has a wide range of individual probabilities of
occurrence, a Huffman code will be efficient.
2020 2 - Embedded C
2.190
// Huffman tree
TNode twentysixth= {'Q','Z',0};
TNode twentyfifth= {'X',0,&twentysixth};
TNode twentyfourth={'J',0,&twentyfifth};
TNode twentythird= {'K',0,&twentyfourth};
TNode twentysecond={'V',0,&twentythird};
TNode twentyfirst= {'B',0,&twentysecond};
TNode twentyth= {'P',0,&twentyfirst};
TNode ninteenth= {'Y',0,&twentyth};
TNode eighteenth= {'G',0,&ninteenth};
TNode seventeenth= {'F',0,&eighteenth};
TNode sixteenth= {'W',0,&seventeenth};
TNode fifteenth= {'M',0,&sixteenth};
TNode fourteenth= {'C',0,&fifteenth};
TNode thirteenth= {'U',0,&fourteenth};
TNode twelfth= {'L',0,&thirteenth};
TNode eleventh= {'D',0,&twelfth};
TNode tenth= {'R',0,&eleventh};
TNode ninth= {'H',0,&tenth};
TNode eighth= {'S',0,&ninth};
TNode seventh= {' ',0,&eighth};
TNode sixth= {'N',0,&seventh};
TNode fifth= {'I',0,&sixth};
TNode fourth= {'O',0,&fifth};
TNode third= {'A',0,&fourth};
TNode second= {'T',0,&third};
TNode root= {'E',0,&second};
2 - Embedded C 2020
2.191
// ********encode***************
// convert ASCII string to Huffman bit sequence
// input is a null-terminated ASCII string
// returns bit count if OK
// returns 0 if BitFIFO full
// returns 0xFFFF if illegal character
2020 2 - Embedded C
2.192
// ********decode***************
// convert Huffman bit sequence to ASCII
// output is a null-terminated ASCII string
// will remove from the BitFIFO until it is empty
// returns character count
2 - Embedded C 2020
2.193
2.10 Functions
We have been using functions throughout this document, but have put off formal
presentation until now because of their immense importance. The key to
effective software development is the appropriate division of a complex problem
into modules. A module is a software task that takes inputs and operates in a
well-defined way to create outputs. In C, functions are our way to create
modules. A small module may be a single function. A medium-sized module
may consist of a group of functions together with global data structures, collected
in a single file. A large module may include multiple medium-sized modules. A
hierarchical software system combines these software modules in either a top-
down or bottom-up fashion. We can consider the following criteria when we
decompose a software system into modules:
It is essential to divide a large software task into smaller, well-defined and easy
to debug modules.
2020 2 - Embedded C
2.194
Module global
variables
local operations
variables calls to other modules
decision structures
looping structures I/O
ports
exit point exit point
As a programmer we must take special care when dealing with global variables
and I/O ports. In order to reduce the complexity of the software we will limit
access to global variables and I/O ports.
2 - Embedded C 2020
2.195
When the function's name is written in an expression, together with the values it
needs, it represents the result that it produces. In other words, an operand in an
expression may be written as a function name together with a set of values upon
which the function operates. The resulting value, as determined by the function,
replaces the function reference in the expression. For example, in the expression:
the term FtoC(T + 2) names the function FtoC and supplies the variable T and
the constant 2 from which FtoC derives a value, which is then added to 4. The
expression effectively becomes:
2020 2 - Embedded C
2.196
// declaration input output
void Init(void); // none none
char InChar(void); // none 8-bit
void OutChar(char); // 8-bit none
short InSDec(void); // none 16-bit
void OutSDec(short); // 16-bit none
char Max(char, char); // two 8-bit 8-bit
int EMax(int, int); // two 32-bit 32-bit
void OutString(char*); // pointer to 8-bit none
char* alloc(int); // 32-bit pointer to 8-
bit
int Exec(void(*fnctPt)(void)); // function pointer 32-bit
InitUART();
char InChar();
void OutChar(char letter);
char UpCase(char letter);
InString(char* pt, unsigned int maxSize);
2 - Embedded C 2020
2.197
One of the powerful features of C is to define pointers to functions. A simple
example follows:
void Setup(void)
{
int data;
2020 2 - Embedded C
2.198
So why the first set of parentheses? By now you have noticed that in C
declarations follow the same syntax as references to the declared objects. Since
the asterisk and parentheses (after the name) are expression operators, an
evaluation precedence is associated with them. In C, parentheses following a
name are associated with the name before the preceding asterisk is applied to the
result. Therefore,
int *fp(int);
would be taken as
int *(fp(int));
The second way to declare a function is to fully describe it; that is, to define it.
Obviously every function must be defined somewhere. So if we organize our
source code in a bottom-up fashion, we would place the lowest level functions
first, followed by the function that calls these low level functions. It is possible
to define large projects in C without ever using a standard declaration (function
prototype). On the other hand, most programmers like the top-down approach
illustrated in the following example. This example includes three modules: the
LCD interface, the COP functions, and some Timer routines. Notice the function
names are chosen to reflect the module in which they are defined. If you are a
C++ programmer, consider the similarities between this C function call
LCD_Clear() and a C++ LCD class and a call to a member function LCD.Clear().
The *.h files contain function declarations and the *.c files contain the
implementations.
#include "LCD.h"
#include "COP.h"
#include "Timer.h"
void main(void)
{
char letter;
short n = 0;
COP_Init();
2 - Embedded C 2020
2.199
LCD_Init();
Timer_Init()
LCD_String("This is a LCD");
Timer_MsWait(1000);
LCD_Clear();
letter = 'a' - 1;
while(1)
{
if (letter == 'z')
letter = 'a';
else
letter++;
LCD_PutChar(letter);
Timer_MsWait(250);
if (++n == 16)
{
n = 0;
LCD_Clear();
}
}
}
Just like the function declaration, we begin the definition with the return_type,
which is the data type of the value the function returns. Some functions perform
the desired operations without returning a value. In this case, we can use void
or leave it blank. Name is the name of the function. The parameter list is a list
of zero or more names for the arguments that will be received by the function
when it is called. The parameter list is also known as the formal parameters of
the function. When a function is invoked, you pass a value to each parameter.
This value is referred to as an actual parameter or argument. Both the type and
name of each input formal parameter is required. MX passes the input parameters
from left to right on the stack. If the last parameter has a simple type, it is not
pushed but passed in a register. Function results are returned in registers, except
if the function returns a result larger than 32 bits. Functions returning a result
larger than 32 bits are called with an additional parameter. This parameter is the
address where the result should get copied.
2020 2 - Embedded C
2.200
Since there is no way in C to declare strings, we cannot declare formal arguments
as strings, but we can declare them as character pointers or arrays. In fact, C does
not recognize strings, but arrays of characters. The string notation is merely a
shorthand way of writing a constant array of characters.
Furthermore, since an unsubscripted array name yields the array's address and
since arguments are passed by value, an array argument is effectively a pointer
to the array. It follows that the formal argument declarations arg[] and *arg are
really equivalent. The compiler takes both as pointer declarations. Array
dimensions in argument declarations are ignored by the compiler since the
function has no control over the size of arrays whose addresses are passed to it.
It must either assume an array's size, receive its size as another argument, or
obtain it elsewhere.
The last, and most important, part of the function definition above is Compound
Statement. This is where the action occurs. Since compound statements may
contain local declarations, simple statements, and other compound statements, it
follows that functions may implement algorithms of any complexity and may be
written in a structured style. Nesting of compound statements is permitted
without limit.
2 - Embedded C 2020
2.201
2.10.3 Function Calls
Name(parameter list)
where Name is the name of the function to be called. The parameter list
specifies the particular input parameters used in this call. Each input parameter
is in fact an expression. It may be as simple as a variable name or a constant, or
it may be arbitrarily complex, including perhaps other function calls. Whatever
the case, the resulting value is pushed onto the stack where it is passed to the
called function.
C programs evaluate arguments in any order, but push them onto the stack in the
order left to right. MX allocates the stack space for the parameters at the start of
the code that will make the function call. Then the values are stored into the pre-
allocated stack position before it calls the function. The input parameters are
removed from the stack at the end of the function. The return parameter is
generally located in a register.
When the called function receives control, it refers to the first actual argument
using the name of the first formal argument. The second formal argument refers
to the second actual argument, and so on. In other words, actual and formal
arguments are matched by position in their respective lists. Extreme care must
be taken to ensure that these lists have the same number and type of arguments.
Function calls can appear in expressions. Since expressions are legal statements,
and since expressions may consist of only a function call, it follows that a
function call may be written as a complete statement. Thus the statement:
2020 2 - Embedded C
2.202
As a better example, consider:
which is also an expression. It calls add3() with the same arguments as before
but this time it assigns the returned value to y. It is a mistake to use an assignment
statement like the above with a function that does not return an output parameter.
The ability to pass one function a pointer to another function is a very powerful
feature of the C language. It enables a function to call any of several other
functions with the caller determining which subordinate function is to be called.
data = (*fp)(5);
return data;
}
void main(void)
{
int result;
2 - Embedded C 2020
2.203
2.10.4 Argument Passing
Let us take a closer look at the matter of argument passing. With respect to the
method by which arguments are passed, two types of subroutine calls are used
in programming languages – call by reference and call by value.
The call by reference method passes arguments in such a way that references to
the formal arguments become, in effect, references to the actual arguments. In
other words, references (pointers) to the actual arguments are passed, instead of
copies of the actual arguments themselves. In this scheme, assignment
statements have implied side effects on the actual arguments; that is, variables
passed to a function are affected by changes to the formal arguments. Sometimes
side effects are beneficial, and sometimes they are not. Since C supports only
one formal output parameter, we can implement additional output parameters
using call by reference. In this way the function can return parameters back using
the reference. The function FIFO_Get, shown below, returns two parameters. The
return parameter is an integer specifying whether or not the request was
successful, and the actual data removed from the queue is returned via the call
by reference. The calling program InChar passes the address of its local variable
data. The assignment statement *datapt = FIFO[GetI++]; within FIFO_Get will
store the return parameter into a local variable of InChar. Normally FIFO_Get
does not have the scope to access local variables of InChar, but in this case
InChar explicitly granted that right by passing a pointer to FIFO_Get.
char InChar(void)
{
char data;
while (!FIFO_Get(&data));
return data;
}
Listing 2.89 – Multiple output parameters using call by reference
2020 2 - Embedded C
2.204
When we use the call by value scheme, the values, not references, are passed to
functions. With call by value, copies are made of the parameters. Within a called
function, references to formal arguments see copied values on the stack, instead
of the original objects from which they were taken. At the time when the
computer is executing within FIFO_Put() of the example below, there will be
three separate and distinct copies of the 0x41 data (main, OutChar and FIFO_Put).
Size++;
FIFO[PutI++] = data; // Put data into FIFO
if (PutI == FIFO_SIZE)
PutI = 0; // Wrap
return -1; // Successful
}
void main(void)
{
char data = 0x41;
OutChar(data);
}
2 - Embedded C 2020
2.205
It is precisely because C uses call by value that we can pass expressions, not just
variables, as arguments. The value of an expression can be copied, but it cannot
be referenced since it has no existence in global memory. Therefore, call by
value adds important generality to the language.
Although the C language uses the call by value technique, it is still possible to
write functions that have side effects; but it must be done deliberately. This is
possible because of C's ability to handle expressions that yield addresses. Since
any expression is a valid argument, addresses can be passed to functions.
func(y = x + 1, 2 * y);
If the arguments are evaluated left to right, then the first argument has the value
x+1 and the second argument has the value 2*(x+1), but if the arguments are
evaluated right to left, then the first argument has the value x+1 and the second
argument has the value 2*y (whatever that may be). The order of evaluation of
arguments is an example of unspecified behaviour in the C language. This is only
an issue when the arguments consist of expressions that modify and use the same
object. The safe way to write the function call is:
y = x + 1;
func(y, 2 * y);
Occasionally, the need arises to write functions that work with a variable number
of arguments. An example is printf() in the ANSI C library. To write a function
with a variable number of arguments, you need to consult a reference on
advanced C programming.
2020 2 - Embedded C
2.206
2.10.5 Private versus Public Functions
void Timer_Init(void);
void Timer_MsWait(unsigned int time);
Listing 2.91 – Timer.h header file has public functions
The implementations of all functions are written in the Timer.c file. The
function TimerWait is private and can only be called by software inside the
Timer.c file. We can apply this same approach to private and public global
variables. Notice that in this case the global variable, TimerClock, is private and
cannot be accessed by software outside the Timer.c file.
// public function
void Timer_Init(void)
{
FTM0_MODE |= 0x01; // Enable timer
FTM0_SC |= 0x01; // timer/2 (500ns)
TimerClock = 2000; // 2000 counts per ms
}
// private function
static void TimerWait(unsigned short time)
{
FTM0_C5V = FTM0_CNT + TimerClock; // 1.00ms wait
FTM0_CnSC(5) &= 0x80; // clear C5F
while ((FTM0_CnSC(5) & 0x80) == 0);
}
// public function
void Timer_MsWait(unsigned short time)
{
for (; time > 0; time--)
TimerWait(TimerClock); // 1.00ms wait
}
2 - Embedded C 2020
2.207
2.10.6 Finite State Machine using Function Pointers
Now that we have seen how to declare, initialize and access function pointers,
we can create very flexible finite state machines. In the finite state machine
presented in Listing 2.74 and Listing 2.76, the output was a simple number that
is written to the output port. In the next example, we will implement the exact
same FSM, but in a way that supports much more flexibility in the operations
that each state performs. In fact, we will define a general C function to be
executed at each state. In this implementation the functions perform the same
output as the previous FSM.
01010001
Stop XX0XX111 Turn
1010XXXX 0x34 1001XXXX 0xB3 rest
2000 rest 5000
XXXXXXX0
0XXXXXXX
00010010 Bend
XXXX0101 0x75
rest 4000
Compare the following implementation to Listing 2.74, and see that the unsigned
char out; constant is replaced with a void (*cmdPt)(void); function pointer.
The three general functions DoStop(), DoTurn() and DoBend() are also added.
2020 2 - Embedded C
2.208
typedef const struct State
{
void (*cmdPt)(void); // function to execute
unsigned short wait; // Time (bus cycles) to wait
unsigned char andMask[4];
unsigned char equMask[4];
const struct State *next[4]; // Next states
} TState;
void DoStop(void)
{
GPIOA_PDOR = 0x34;
}
void DoTurn(void)
{
GPIOA_PDOR = 0xB3;
}
void DoBend(void)
{
GPIOA_PDOR = 0x75;
}
TState FSM[3] =
{
{
&DoStop, 2000, // stop 1 ms
{0xFF, 0xF0, 0x27, 0x00},
{0x51, 0xA0, 0x07, 0x00},
{Turn, Stop, Turn, Bend}
},
{
&DoTurn, 5000, // turn 2.5 ms
{0x80, 0xF0, 0x00, 0x00},
{0x00, 0x90, 0x00, 0x00},
{Bend, Stop, Turn, Turn}
},
{
&DoBend, 4000, // bend 2 ms
{0xFF, 0x0F, 0x01, 0x00},
{0x12, 0x05, 0x00, 0x00},
{Stop, Stop, Turn, Stop}
}
};
2 - Embedded C 2020
2.209
Compare the following implementation to Listing 2.76, and see that the
GPIOA_PDOR = pt->out; assignment is replaced with a (*pt->cmdPt)(); function
call. In this way, the appropriate function DoStop(), DoTurn() or DoBend() will
be called.
void Control(void)
{
PState pt;
unsigned char input;
unsigned short startTime;
while(1)
{
// 1) execute function
(*pt->cmdPt)();
// Time (500 ns each) to wait
startTime = FTM0_CNT;
// 2) wait
while ((FTM0_CNT - startTime) <= pt->wait);
// 3) input
input = GPIOB_PDIR;
for (int i = 0; i < 4; i++)
if ((input & pt->andMask[i]) == pt->equMask[i])
{
// 4) next depends on input
pt = pt->next[i];
i = 4;
}
}
}
2020 2 - Embedded C
2.210
2.10.7 Linked List Interpreter using Function Pointers
In the next example, function pointers are stored in a linked list. An interpreter
accepts ASCII input from a keyboard and scans the list for a match. In this
implementation, each node in the linked list has a function to be executed when
the operator types the corresponding letter. The linked list LL has three nodes.
Each node has a letter, a function and a link to the next node.
void CommandA(void)
{
OutString("\nExecuting Command a");
}
void CommandB(void)
{
OutString("\nExecuting Command b");
}
void CommandC(void)
{
OutString("\nExecuting Command c");
}
TNode LL[3] =
{
{'a', &CommandA, &LL[1]},
{'b', &CommandB, &LL[2]},
{'c', &CommandC, NULL}
};
2 - Embedded C 2020
2.211
void main(void)
{
PNode pt;
char string[40];
Compare the syntax of the function call, (*pt->cmdPt)();, in Listing 2.94, with
the syntax in this example, pt->fnctPt();. In the MX compiler, these two
expressions both generate code that executes the function.
2020 2 - Embedded C
2.212
macro processing
conditional compiling
implementation-dependent features
The preprocessor is controlled by directives which are not part of the C language.
Each directive begins with a # character and is written on a line by itself. Only
the preprocessor sees these directive lines since it deletes them from the code
stream after processing them.
1) To save time we can define a macro for long sequences that we will need
to repeat many times.
3) To make the software easy to change, we can define a macro such that
changing the macro definition automatically updates the entire software.
2 - Embedded C 2020
2.213
Macros define names which stand for arbitrary strings of text:
The Name part of a macro definition must conform to the standard C naming
conventions as described earlier. CharacterString begins with the first printable
character following Name and continues through to the last printable character
of the line or until a comment is reached.
#define size 10
will change:
short data[size];
into:
short data[10];
short data[size1];
2020 2 - Embedded C
2.214
The most common use of #define directives is to give meaningful names to
constants; i.e. to define so-called manifest constants. The use of manifest
constants in programs helps to ensure that code is portable by isolating the
definition of these elements in a single header file, where they need to be
changed only once.
void function(void)
{
...
ENTER_CRITICAL; // make atomic, entering critical section
// we have exclusive access to global variables
...
EXIT_CRITICAL; // exit critical section
}
2 - Embedded C 2020
2.215
2.11.2 Conditional Compiling
The preprocessing feature lets us designate parts of a program which may or may
not be compiled depending on whether or not certain symbols have been defined.
In this way it is possible to write into a program optional features which are
chosen for inclusion or exclusion by simply adding or removing #define
directives at the beginning of the program.
#ifdef Name
it looks to see if the designated name has been defined. If not, it throws away the
following source lines until it finds a matching
#else
or
#endif
directive. The #endif directive delimits the section of text controlled by #ifdef,
and the #else directive permits us to split conditional text into true and false
parts. The first part (#ifdef...#else) is compiled only if the designated name
is defined, and the second (#else...#endif) only if it is not defined.
#ifndef Name
directive. This directive also takes matching #else and #ifndef directives. In
this case, however, if the designated name is not defined, then the first
(#ifndef...#else) or only (#ifndef...#endif) section of text is compiled;
otherwise, the second (#else...#endif), if present, is compiled.
2020 2 - Embedded C
2.216
Nesting of these directives is allowed; and there is no limit on the depth of
nesting. It is possible, for instance, to write something like
#ifdef ABC
... // ABC
#ifndef DEF
... // ABC and not DEF
#else
... // ABC and DEF
#endif
... // ABC
#else
... // not ABC
#ifdef HIJ
... // not ABC but HIJ
#endif
... // not ABC
#endif
where the ellipses represent conditionally compiled code, and the comments
indicate the conditions under which the various sections of code are compiled.
#define Debug
int Sub(int j)
{
int i;
#ifdef Debug
GPIOC_PSOR = 0x01; // PC0 set when Sub is entered
#endif
i = j + 1;
#ifdef Debug
GPIOC_PCOR = 0x01; // PC0 cleared when Sub is exited
#endif
return i;
}
2 - Embedded C 2020
2.217
void ProgA(void)
{
int i;
#ifdef Debug
GPIOC_PSOR = 0x02; // PC1 set when ProgA is entered
#endif
i = Sub(5);
while (1)
{
i = Sub(i);
}
}
void ProgB(void)
{
int i;
i = 6;
...
#ifdef Debug
GPIOC_PCOR = 0x02; // PC1 cleared when ProgB is exited
#endif
}
The preprocessor also recognizes directives to include source code from other
files. The two directives
#include <Filename>
#include "Filename"
Filename follows the normal PC file specification format, including drive, path,
filename, and extension.
2020 2 - Embedded C
2.218
2.11.4 Implementation-Dependent Features
#pragma pack(push)
#pragma pack(1)
typedef union
{
uint8_t bytes[5];
struct
{
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
uint8_t byte4;
uint8_t byte5;
} packetStruct;
} TPacket;
The GCC compiler will then ensure that byte1, byte2, etc. are contiguous in
memory, rather than aligned on 32-bit boundaries. In this way, we can use the
union to access the same 5 bytes of memory via the array or by a unique name.
For example, the function attribute interrupt is used to indicate that the
specified function is an interrupt service routine. To declare an ISR for a UART,
you would use:
2 - Embedded C 2020
2.219
be assembly language code and is sent straight to the output of the compiler
exactly as it appears in the input. The second approach is to write an entire file
in assembly language, which may include global variables and functions. In MX,
we include assembly files by adding them to the project. Entire assembly files
can also be assembled separately then linked at a later time to the rest of the
program. The simple insertion method is discussed in this section.
__asm("CPSID f");
2020 2 - Embedded C
2.220
The following function runs with interrupts disabled.
void FIFO_Init(void)
{
INTR_OFF(); // make atomic, entering critical section
PutI=GetI=Size=0;// Empty when Size == 0
INTR_ON(); // end critical section
}
Of course, to make use of the __asm feature, we must let the compiler know about
the C variables modified by the instructions, the C expressions read by the
instructions, and the registers or other values that are changed by the instructions.
We also need to know how the compiler uses the CPU registers, how functions
are called, and how the operating system and hardware works. It will certainly
cause a programming error if your embedded assembly modifies the stack
pointer, SP, for example.
In GCC you can access a global or local variable directly using just its name:
int Time;
void Add1Time(void)
{
__asm (\
"ldr r3, %[input]\n\t"\
"adds r2, r3, #1\n\t"\
"str r3, %[input]\n\t"\
::[input] "m" (Time) \
: "r2", "r3");
}
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm
2 - Embedded C 2020
2.221
This means that if you utilise the CMSIS HAL for various low-level functions,
such as setting up and responding to interrupts or using the built-in SysTick,
then your software will have a high degree of portability across the range of
ARM® Cortex®-based microcontrollers.
There are many individual HALs that support special features of the ARM®
Cortex® architecture. For example, there is a HAL that supports the special
digital signal processing instructions of the Cortex® M4/M7/M33/M35P – we
can use this on the NXP K64 chips.
https://developer.arm.com/tools-and-software/embedded/cmsis
PMcL Hardware Abstraction Layers Index
2020 2 - Embedded C
5.1
5 Interrupts
Contents
2020 5 - Interrupts
5.2
Introduction
You are studying at your desk at home. The phone rings (an interrupt). You stop
An interrupt is a
request by another studying and answer the phone (you accept the interrupt). It is your friend, who
module for access
to CPU processing wants to know the URL for a particular Freescale datasheet relating to the K64
time
so she can look up some information required to complete a laboratory
assignment. You give her the URL (you process the interrupt request
immediately). You then hang up and go back to studying. Note that the additional
time it will take you to complete your study is miniscule, yet the amount of time
for your friend to complete her task may be significantly reduced (she didn’t
have to wait until you were free). This simple example clearly illustrates how
interrupts can drastically improve response time in a real-time system.
5.1 Exceptions
Exceptions are events that cause changes to program flow. When one happens,
the processor suspends the current executing task and executes a part of the
program called an exception handler. After the execution of the exception
handler is completed, the processor then resumes normal program execution. In
the ARM® architecture, interrupts are one type of exception.
5 - Interrupts 2020
5.3
5.2 Interrupts
An interrupt is an event triggered inside the microcontroller, usually by internal
or external hardware, and in some cases by software. The exception handler for
an interrupt is referred to as an interrupt service routine (ISR). On completion
of the ISR, software execution returns to the next instruction that would have
occurred without the interrupt.
An interrupt causes
the main thread to
be suspended, and
Hardware Busy Ready Busy the interrupt thread
is run
Hardware Hardware
needs carrying
service out task
Main Main Main
Thread
Saves Restores
execution execution
state state
Interrupt ISR
Thread
ISR
provides
service
Figure 5.1
2020 5 - Interrupts
5.4
5.2.1 Using Interrupts
Each potential interrupt source has a separate arm bit, e.g. RIE (the UART
receive interrupt enable bit). The software must set the arm bits for those devices
from which it wishes to accept interrupts, and deactivate the arm bits within
those devices from which interrupts are not to be allowed. After reset, all the
interrupt arm bits are set to deactivate the corresponding interrupt.
Each potential interrupt source has a separate flag bit, e.g. RDRF (the UART
receive data register full flag). The hardware sets the flag when it wishes to
request an interrupt. The software must clear the flag in the ISR to signify it has
handled the interrupt request, and to allow the device to again trigger an
interrupt.
There are a number of special registers in the MCU that contain the processor
status and define the operation states and interrupt/exception masking. Special
registers are not memory mapped, which means special assembly language
instructions are required to access them.
5 - Interrupts 2020
5.5
The following figure shows the hardware arrangement for interrupt generation.
interrupt
sources
RIE 31:1 0
PRIMASK I
RDRF
TIE
TDRE
UART INT interrupt
TIE LPTMR pending
NVIC
address vector
TIF
address
32
Figure 5.2
3. The main program is resumed when the ISR executes the EXC_RETURN
instruction:
Hardware pulls all the registers from the stack, including the PC, so that
the program continues from the point where it was interrupted.
2020 5 - Interrupts
5.6
5.2.3 Interrupt Polling
Some interrupts share the same interrupt vector. For example, the reception and
transmission of a byte via the UART leads to just one interrupt, and there is one
vector associated with it. In Figure 5.2, the two interrupt sources are ORed
together to create one interrupt request. In such cases, the ISR is responsible for
polling the status flags to see which event actually triggered the interrupt. Care
must be taken because both flags may be set, and only the hardware events that
are enabled must be serviced by the software.
For example, the UART shares an interrupt for transmit and receive operations.
Therefore, in the ISR, we would need code to respond to either of those events,
but only if the corresponding interrupt enable bit is enabled:
...
// Receive a character
if (UART0->C2 & UART_C2_RIE_MASK)
{
// Clear RDRF flag by reading the status register
if (UART0->S1 & UART_S1_RDRF_MASK)
// Do something with the received byte
...
}
// Transmit a character
if (UART0->C2 & UART_C2_TIE_MASK)
{
// Clear TDRE flag by reading the status register
if (UART0->S1 & UART_S1_TDRE_MASK)
{
// Get a new byte and transmit it
...
5 - Interrupts 2020
5.7
The vector table starts at memory address 0. The first entry is special – it is not
an address but the initial value of the stack pointer. It is needed because some
exceptions such as the NMI could happen as the processor just comes out of reset
and before any other initialization steps are executed.
Memory Exception
Address Vectors Number
0x0000_03FC IRQ #239 255
0x0000_0048 IRQ #2 18
0x0000_0044 IRQ #1 17
0x0000_0040 IRQ #0 16
0x0000_003C SysTick 15
0x0000_0038 PendSV 14
0x0000_0034 Reserved 13
0x0000_0030 Debug Monitor 12
0x0000_002C SVC 11
0x0000_0028 Reserved 10
0x0000_0024 Reserved 9
0x0000_0020 Reserved 8
0x0000_001C Reserved 7
0x0000_0018 Usage Fault 6
0x0000_0014 Bus Fault 5
0x0000_0010 MemManage Fault 4
0x0000_000C HardFault 3
0x0000_0008 NMI 2
0x0000_0004 Reset 1
0x0000_0000 Initial value of SP 0
2020 5 - Interrupts
5.8
Memory Memory
Address RAM Address RAM
Execution continues at the address pointed to by the vector for the highest-
A higher priority
exception pre-empts priority interrupt that was pending at the beginning of the interrupt sequence –
a currently
executing exception this is the interrupt service routine. If an interrupt source of higher priority occurs
handler – this is
called a nested during execution of the ISR, the ISR will itself be interrupted – this is called
exception
interrupt nesting.
The body of an interrupt service routine varies according to the source of the
interrupt. For an interrupt service routine written to handle external events, they
typically respond to the interrupt by retrieving or sending external data, e.g. the
reception of a byte of data via the UART is normally handled via an ISR which
places the received byte into a FIFO for later processing by the main function.
5 - Interrupts 2020
5.9
5.4.1 Declaring Interrupt Service Routines in C for Generic Processors
In GNU C, you use function attributes to declare certain things about functions
called in your program which help the compiler optimize calls and check your
code more carefully. You can also use attributes to control memory placement,
code generation options or call/return conventions within the function being
annotated. Many of these attributes are target-specific. For example, many
targets support attributes for defining interrupt handler functions, which
typically must follow special register usage and return conventions.
In the GNU Compiler Collection (GCC) for ARM® processors, the function
attribute interrupt is used to indicate that the specified function is an
interrupt service routine. For example, to declare an ISR for a UART, you would
use:
void __attribute__ ((interrupt)) UART_ISR(void)
{
/* code goes here */
}
The interrupt function attribute for the ISR is really only needed for
previous generations of ARM® processors, since the Cortex®-M has a special
hardware instruction for exception return.
2020 5 - Interrupts
5.10
5.4.2 Declaring Interrupt Service Routines in C for ARM® Cortex®-M Processors
The use of the EXC_RETURN value for triggering exception returns allows
exception handlers (including interrupt service routines) to be written as normal
C functions.
5 - Interrupts 2020
5.11
5.4.1 Specifying an ISR Address in the Vector Table
The vector number for a particular interrupt source is given in the comment,
along with its name, which are documented in Table 3-5 of the K64 Sub-Family
Reference Manual.
2020 5 - Interrupts
5.12
If you delve into the functions, which in turn call CMSIS provided functions,
you will eventually come to some assembly language. The assembly language
instruction cpsie stands for Change Processor State Interrupt Enable. The i
parameter refers to the single-bit PRIMASK register. Some other
implementations of disabling/enabling global interrupts use the f parameter
which refers to the single-bit “fault mask” register FAULTMASK. This register is
similar to PRIMASK, but it also blocks the HardFault exception.
The above functions are meant to be used in pairs. The return value of
DisableGlobalIRQ(void) is the current value of the PRIMASK register. This
should be used in the call to EnableGlobalIRQ(uint32_t primask). The reason
for this paired nature and the local storage of the PRIMASK register is so that
paired calls can be nested.
priMask = DisableGlobalIRQ();
FreedomInit();
EnableGlobalIRQ(priMask);
while (1)
{
/* Main loop */
}
5 - Interrupts 2020
5.13
5.5.1 Interrupt Latency
Interrupts cannot disturb an instruction in progress, and thus are only recognized
between the execution of two instructions (apart from special instructions on the
K64 which are designed to be interrupted). Therefore the maximum latency from
interrupt request to completion of the hardware response consists of the
execution time of the slowest instruction plus the time required to complete the
memory transfers required by the hardware response.
In many cases, rather than simply disabling all interrupts to carry out a certain
time-sensitive task, you only want to disable interrupts with priority lower than
a certain level. In this case, you write the required masking priority level to the
BASEPRI register.
When you enable an interrupt source in your application, you get to decide on
its priority level (0-15). Some of the exceptions (reset, NMI and HardFault) have
fixed priority levels. Their priority levels are represented with negative numbers
to indicate that they are of higher priority than other exceptions.
2020 5 - Interrupts
5.14
Microcontroller
Cortex-M processor
Peripheral NMI
Processsor
NVIC Core
Peripherals
IRQs System
Exceptions
I/O Port
SysTick timer
I/O Port
To support this, the NVIC contains programmable registers for interrupt enable
control, pending status, and read-only active status bits.
5 - Interrupts 2020
5.15
5.7.1 Pending Status
The pending status of the interrupts are stored in programmable registers in the
NVIC. When an interrupt input of the NVIC is asserted, it causes the pending
status of the interrupt to be asserted. The pending status remains high even if the
interrupt request is de-asserted.
The pending status means it is put into a state of waiting for the processor to
serve the interrupt. In some cases, the processor serves the request as soon as an
interrupt becomes pending. However, if the processor is already serving another
interrupt of higher or equal priority, or if the interrupt is masked by one of the
interrupt masking registers (e.g. PRIMASK), the pended request will remain until
the other interrupt service routine is finished, or when the interrupt masking is
cleared.
When the processor starts to process an interrupt request, the pending status of
the interrupt is cleared automatically.
The pending status of interrupts are stored in interrupt pending status registers,
which are accessible from software code. Therefore, you can clear the pending
status of an interrupt or set it manually. If an interrupt arrives when the processor
is serving another higher-priority interrupt and the pending status is cleared
before the processor starts responding to the pending request, the request is
cancelled and will not be served.
The pending status of an interrupt can be set even when the interrupt is disabled.
In this case, when the interrupt is enabled later, it can be triggered and get served.
In some cases this might not be desirable, so in this case you will have to clear
the pending status manually before enabling the interrupt in the NVIC.
2020 5 - Interrupts
5.16
5.7.2 NVIC Registers for Interrupt Control
There are a number of registers in the NVIC for interrupt control (exception type
16 up to 255). By default, after a system reset, all interrupts:
The Interrupt Enable register is programmed through two addresses. To set the
enable bit, you need to write to the NVIC’s Set Enable Register, NVICISERx; to
clear the enable bit, you need to write to the NVIC’s Clear Enable Register
NVICICERx. In this way, enabling or disabling an interrupt will not affect other
interrupt enable states. The NVICISERx / NVICICERx registers are 32-bits wide;
each bit represents one interrupt input.
5 - Interrupts 2020
5.17
Interrupt Pending Registers
The interrupt-pending status can be accessed through the Interrupt Set Pending
(NVICISPx) and Interrupt Clear Pending (NVICICPx) registers. Similarly to the
enable registers, there is more than one pending ISP and ICP register.
The values of the pending status registers can be changed by software, so you
can cancel a current pended exception through the NVICICPx register, or generate
software interrupts through the NVICISPx register.
The parameter passed to these functions is the interrupt request number (defined
in the MK64F12.h file) corresponding to Table 3-5 of the K64 Sub-Family
Reference Manual.
2020 5 - Interrupts
5.18
EXAMPLE 5.1 Real-Time Interrupt using the Low Power Timer
The code below shows a simple scheme that shows the duration of the ISR and
the timing operation of the main loop.
Code to generate
and respond to real- const gpio_pin_config_t PORTD_GPIO_PIN_CONFIG =
time interrupts {
.pinDirection = kGPIO_DigitalInput,
.outputLogic = 0U
};
5 - Interrupts 2020
5.19
/*! @brief Initializes PORTD bits 1-0.
*
*/
void PORTD_Init(void)
{
// Enable clock gate for Port D to enable pin routing
CLOCK_EnableClock(kCLOCK_PortD);
2020 5 - Interrupts
5.20
/*! @brief Low power timer interrupt handler.
*
*/
void LPTMR0_IRQHandler(void)
{
// Acknowledge interrupt, clear interrupt flag
LPTMR0->CSR |= LPTMR_CSR_TCF_MASK;
// Set bit 0
GPIOD->PSOR = 0x00000001;
// Software handshake – means LPTMR interrupt happened
if (Ack == 1)
Ack = 0;
// Number of interrupts
Count++;
// Clear bit 0
GPIOD->PCOR = 0x00000001;
}
/*!
* @brief Main function
*/
int main(void)
{
BOARD_InitPins();
BOARD_InitBootClocks();
// Globally disable interrupts while we set up
uint32_t priMask = DisableGlobalIRQ();
PORTD_Init();
LPTMR_Init();
// Interrupt counter
Count = 0;
// Foreground is ready
Ack = 1;
// Globally enable interrupts
EnableGlobalIRQ(priMask);
for (;;)
{
if (Ack == 0)
{
Ack = 1;
// Toggle bit 1
GPIOD->PTOR = 0x00000002;
}
}
}
Note the name of the low power timer interrupts service routine, void
LPTMR0_IRQHandler(void), is the same as that already listed in the vector table
in startup_mk64f12.c. The linker will automatically override the “weak”
pre-declared ISR with our one. Thus, our ISR is “injected” into the vector table.
5 - Interrupts 2020
5.21
The figure below gives a flow chart of what is happening.
PTD[0]=1
0
Ack
1
Ack
1 Ack = 1
0 Ack = 0
toggle PTD[1]
Count++
PTD[0]=0
main
exc_return
ISR
2020 5 - Interrupts
5.22
Background
Processing exc_return exc_return exc_return
Figure 5.6
The main program performs the necessary initialization and then enters the
“background” portion of the program, which is often nothing more than a simple
loop that processes non-critical tasks and waits for interrupts to occur. Examples
of background processing include: processing data from an input device,
creating data for an output device, making calculations based on analog-to-
digital conversion results, determining the next digital-to-analog output, and
updating a display seen by human eyes.
5 - Interrupts 2020
5.23
An interrupt-driven
input routine
RDRF set InChar
Read data
from input
FIFO_Put FIFO_Get
yes yes
FIFO FIFO
full? empty?
no no
Put FIFO Get
buffer
Return data
Error to caller Error
exc_return return
Figure 5.7
2020 5 - Interrupts
5.24
The structure for interrupt-driven character transmission is similar, except for
one minor detail which must be resolved. Output device interrupt requests come
in two varieties – those that request an interrupt on the transition to the ready
state, and those that request an interrupt when they are in the ready state.
When the background thread puts the first byte into the FIFO buffer, the
output device is idle and already in the “ready” state, so no interrupt
request from the output device is about to occur. The output ISR will not
be invoked and the data will not be removed from the buffer.
Output devices need The background thread checks the output busy flag every time it writes data into
to be kick started
the buffer. If the device is busy, then a device ready interrupt is expected and
nothing needs to be done; otherwise, the background thread arms the output and
calls SendData to “kick start” the output process.
The SendData routine is responsible for retrieving the data from the buffer and
outputting it. If there is no more data in the buffer, then it must disarm the output
to prevent further interrupts.
5 - Interrupts 2020
5.25
The flowchart given below illustrates the process.
Kick starting an
interrupt-driven
OutChar SendData TDRE set output routine for a
device that requests
FIFO_Get
FIFO_Put interrupts on
yes yes
FIFO
full?
FIFO
empty?
transitioning from
busy to ready
no no
Put FIFO Get
buffer
Call SendData
Error
Write data
to output Error
yes
error?
no
error?
no
output yes
yes
device
busy? Disarm
output
exc_return
no
Arm return Foreground thread (ISR)
output
Call SendData
(Kick Start)
return
Figure 5.8
2020 5 - Interrupts
5.26
5.9.2 Output Device Interrupt Request on Ready
In this case, an output device sets its interrupt request flag when it is idle and
ready for output (this will be the case after a reset condition, too). This means
that upon initially arming the interrupt for such a device, an ISR will be invoked
immediately. In the context of serial port transmission this creates two problems:
The technique to handle this type of interrupt is to modify both the OutChar
routine and the ISR. The UART transmit interrupt is armed after every FIFO_Put
(if the UART transmit interrupt were already armed, then rearming would have
no effect). If the transmit FIFO is empty, then the ISR should disarm the transmit
interrupt.
An interrupt-driven
output routine for
devices that request Init OutChar TDRE set
an interrupt when
FIFO_Init FIFO_Get
they are ready FIFO_Put
Init yes yes
FIFO FIFO
full? empty?
no no
TIE = 0
Put FIFO Get
buffer
return Error
Write data
to output Error
Initialization
yes
error?
no
error?
no
yes
TIE = 1
TIE = 0
return
exc_return
Background thread (main)
Foreground thread (ISR)
Figure 5.9
5 - Interrupts 2020
5.27
2020 5 - Interrupts
5.28
Therefore, if the C code reads:
NbBytes++;
and NbBytes is byte-sized, then the compiler generates the following code:
Since we are not using a real-time operating system (which would inherently
support a multithreaded program by providing interthread communication
mechanisms), one way of protecting the integrity of shared global variables is to
disable interrupts during the critical section. This is a simple and acceptable
method of protecting a critical section for a small embedded system.
It is important not to disable interrupts too long so as not to affect the dynamic
performance of other threads. There is a problem however – consider what would
happen if you simply add an “interrupt disable” at the beginning and an
“interrupt enable” at the end of a critical section:
A problem with
__disable_irq(); // disable interrupts
disabling and
NbBytes++; // critical section
enabling interrupts
__enable_irq(); // enable interrupts
to make a critical
section What if interrupts were in a disabled state on entry into the critical section?
Unfortunately, we have enabled them on exiting the critical section! What we
need to do is save the state of the interrupts (enabled or disabled) before we enter
the critical section, and restore that state on exiting.
5 - Interrupts 2020
5.29
5.10.1 Critical Sections in C for the ARMv7-M
In C, a way of implementing critical sections that preserves the interrupt state
and allows nesting of critical sections (e.g. through function calls) is by declaring
the following macros:
// Save status register and disable interrupts C macros for
#define EnterCritical() \ entering and exiting
do {\ a critical section
uint8_t SR_reg_local;\
__asm ( \
"MRS R0, FAULTMASK\n\t" \
"CPSID f\n\t" \
"STRB R0, %[output]" \
: [output] "=m" (SR_reg_local)\
:: "r0");\
if (++SR_lock == 1u) {\
SR_reg = SR_reg_local;\
}\
} while(0)
Firstly, the \ character that appears at the end of each line is C’s way of extending
a single expression across more than one line.
Secondly, the do {...} while(0) construct is the only construct in C that you
can use to #define a multistatement operation, put a semicolon after, and still
use within an if statement. It also lets you declare local variables inside the block
created with the braces. The multiple statements that appear between the braces
{...} are only executed once due to the while(0).
Thirdly, there are two global variables used by the macros, which are defined as:
volatile uint8_t SR_reg; // Current value of the FAULTMASK register
volatile uint8_t SR_lock = 0x00U; // Lock
2020 5 - Interrupts
5.30
The basic idea of the code is:
5 - Interrupts 2020
5.31
You can use the macros, with nesting, as shown in the example below:
void function(void)
{
EnterCritical();
...
EnterCritical();
...
ExitCritical();
...
ExitCritical();
}
To reiterate – whenever two (or more) threads share a global variable, you must
protect access to that variable by operating in a critical section. The macros are
not robust and you must guarantee that EnterCritical() and ExitCritical()
occur in nested pairs. Be careful in your code that you do not enter a critical
section inside a function and then exit that function without a corresponding
ExitCritical(). Such a situation may arise when there are multiple exit points
from a function:
void function(void)
{
EnterCritical();
...
if (error)
return; // Error! We have not “called” ExitCritical()
...
ExitCritical();
}
priMask = DisableGlobalIRQ();
...
EnableGlobalIRQ(priMask);
is that they make the code more readable and portable.
5.11 References
Yiu, J.: The Definitive Guide to ARM® Cortex®-M3 and ARM Cortex®-M4
Processors, Newnes, 2014. ISBN-13: 978-0-12-408082-9
2020 5 - Interrupts
6.1
Contents
Introduction
The K64 has several timer modules:
Module Description
Programmable delay block (PDB) The PDB provides controllable delays
from either an internal or an external
trigger, or a programmable interval
tick, to the hardware trigger inputs of
ADCs and/or generates the interval
triggers to DACs, so that the precise
timing between ADC conversions
and/or DAC updates can be achieved.
FlexTimer modules (FTM) The FTM is an eight channel timer
that supports input capture, output
compare, and the generation of PWM
signals.
Periodic interrupt timers (PIT) The PIT module is an array of 4 timers
that can be used to raise interrupts and
trigger DMA channels.
Low-power timer (LPTMR) The LPTMR can be configured to
operate as a time counter with
optional prescaler, or as a pulse
counter with optional glitch filter,
across all power modes, including the
low-leakage modes. It can also
continue operating through most
system reset events, allowing it to be
used as a time of day counter.
Carrier modulator timer (CMT) The CMT module provides a means to
generate the protocol timing and
carrier signals for a variety of
encoding schemes used in infrared
remote controls.
Real-time clock (RTC) The RTC operates off an independent
power supply and 32 kHz crystal
oscillator and has a 32-bit seconds
counter with a 32-bit alarm.
IEEE 1588 timers The IEEE 1588 standard provides
accurate clock synchronization for
distributed control nodes for
industrial automation applications.
Figure 6.1
The “input capture / output compare” block is just a register called CnV, where
The timer has a
n is the channel number, that gets loaded with the current value of CNT for an free-running counter
and a value counter
input capture event, and which holds a desired value of CNT to trigger an output for each channel
compare event.
There are numerous control registers used to set up the FTM module. Only a few
are needed to interact with the FTM once it has been set up for a particular
application. A complete description of the FTM can be found in Chapter 43 of
NXP’s K64 Sub-Family Reference Manual.
Output compare can be used to create square waves, generate pulses, implement
time delays, and execute periodic interrupts. You can use output compare
together with input capture to measure period and frequency over a wide range
and with varying resolution.
A channel set up as an output compare channel will trigger an output action when
the output compare register is equal to the free-running timer. A block diagram
of the output compare action is shown below:
A simplified output
compare block
diagram CNT
16-bit
Free-Running
Timer
Pin
PTn Output Compare interrupt
Logic
CnV
Figure 6.2
A compare result output action can be set up using the Channel Status and
Control Register, CnSC, for the relevant channel. The options are:
Output compare Action
actions
One simple application of the output compare feature is to create a fixed timer.
Timers are useful in situations where you start an operation, wait a certain
amount of time, and then stop the operation. Usually the process looks like this:
You can also use timers to detect timeout conditions. For example, you turn on
a motor and then start a timer. You expect the speed of the motor to increase,
and if the speed doesn’t exceed a threshold before a timer times out, then you
might turn the motor off and notify an operator. In these cases, you start an
operation then monitor the process to see if conditions are met before the timer
expires:
Let delay be the number of cycles you wish to wait. The steps to start a timer
are:
This method will only work for values of delay that fall between a minimum
value (the time it takes to implement steps 1 to 3) and 65536. It will function
properly even if CNT rolls over from 0xFFFF to 0, since the 16-bit addition is really
a modulo 0x10000 addition.
void FTM0_Init(void)
{
// Set up FTM0
...
// Initialize NVIC
...
}
// Enable interrupts
__enable_irq();
}
Note that the FTM0 ISR has taken on the pre-declared name of FTM0_IRQHandler
which will be automatically placed in the vector table by the linker.
A channel can be set up as an input capture channel. We can use input capture
to measure the period or pulse width of 3.3V CMOS signals. The input capture
system can also be used to trigger interrupts on rising or falling transitions of
external signals. A simplified block diagram of a channel set up for input capture
is shown below:
A simplified input
capture block
diagram
CNT
16-bit
Free-Running
Timer
Pin
PTn Input Capture interrupt
Logic
CnV
Figure 6.3
The input capture edge detection circuits can be set up using the CnSC register.
The options are:
+3.3 V
FTM0_CH1
0V
PulseWidth
void FTM0_Init(void)
{
// Set up FTM0
...
// Initialize NVIC
...
}
void FTM0_IRQHandler(void)
{
// Value of CNT at rising edge
static uint16_t rising;
// No measurement yet
Done = false;
// Enable interrupts
__enable_irq();
}
Note that the FTM0 ISR has taken on the pre-declared name of FTM0_IRQHandler
which will be automatically placed in the vector table by the linker.
Figure 6.4
A PIT generates triggers at periodic intervals, when enabled. The timer loads the
start value as specified in the LDVAL register, counts down to 0 and then loads the
respective start value again. Each time the timer reaches 0, it will generate a
trigger pulse and set the interrupt flag.
Note that an interrupt will occur (if enabled) when the CVAL register reaches zero
and the next clock “tick” reloads the start value as specified in the LDVAL register.
For example, to create a timer with a period of 1000 “ticks” of the module clock,
the LDVAL register needs to be loaded with 999.
On the FRDM-K64 board, the RTC is powered via the USB and not a battery.
Therefore, it does not have the ability to keep the time when powered off.
The RTC unit relies on an external 32.768 kHz crystal for its timekeeping. The
crystal must have “load” capacitors connected to it to function properly. The K64
has the ability to select internal load capacitors (and therefore minimise external
hardware). The selection of the internal load capacitors is accomplished by bits
in the RTC Control register, RTC_CR. However, for the FRDM-K64 board, the
load capacitors are external:
K64 OSC32K
Rf
C1 X1 C2
Figure 6.5
7 Concurrent Software
Contents
Introduction
A program is a list of instructions for the computer to execute. A thread is an
A thread is an
executing program, executing program, including the current values of the program counter, registers
with a context
and variables. A thread has an execution state (such as running, ready, waiting)
and a saved thread context when not running. Conceptually, each thread has its
own CPU. In reality, of course, the real CPU switches back and forth from thread
to thread.
The execution of the main program is called the background thread. In most
embedded applications, the background thread executes a loop that never ends.
This thread can be broken (execution suspended, then restarted) by foreground
Simple embedded threads (interrupt service routines). These threads are run using a simple
systems are
foreground / algorithm. The ISR of an input device is invoked when new input is available.
background systems
The ISR of an output device is invoked when the output device is idle and needs
more data. Last, the ISR of a periodic task is run at a regular rate. The main
program runs in the remaining intervals. Many embedded applications are small
in size, and static in nature, so this configuration is usually adequate.
The limitation of a single background thread comes as the size and complexity
of the system grows. Projects where the software modules are loosely coupled
(independent) more naturally fit a multiple background thread configuration.
Systems that implement a thread scheduler still may employ regular I/O driven
interrupts. In this way, the system supports multiple foreground threads and
multiple background threads.
7.1 Threads
A thread is the execution of a software task that has its own stack and registers.
Since each thread has a separate stack, its local variables are private, which
means it alone has access.
Each thread has its
own registers and
stack
Thread 1 Thread 2 Thread 3
R0 Stack R0 Stack R0 Stack
... ... ...
R12 R12 R12
SP SP SP
LR LR LR
PC PC PC
Figure 7.1
Global
Figure 7.2
In summary, a thread:
Thread states
create thread
Figure 7.3
A thread is in the ready state if it is ready to run but waiting for its turn.
A thread is in the waiting state when it is waiting for some external event like
I/O (keyboard input available, printer ready, I/O device available). If a thread
communicates with other threads, then it can be waiting for an input message or
waiting for another thread to be ready to accept its output message. If a thread
wishes to output to the serial port, but another thread is currently outputting, it
will wait. If a thread needs information from a FIFO (calls FIFO_Get), then it will
wait if the FIFO is empty (because it cannot retrieve any information). On the
other hand, if a thread outputs information to a FIFO (calls FIFO_Put), then it
will wait if the FIFO is full (because it cannot save its information).
In the figure below, thread 5 is running, threads 1 and 2 are ready to run, and
threads 3 and 4 are waiting because a FIFO is empty.
WaitOnFullPt = NULL
Figure 7.4
If a thread is ready, it may be granted control of the CPU by the OS at any time.
Conversely, while running, the OS may stop the thread executing and make it
ready. We therefore need a way for the scheduler to save and restore the state of
a thread. A thread control block (TCB) is used to store the information about
each thread.
6) Priority;
TCB structure
TCB of a TCB of a
running thread ready thread
K70 saved SP saved SP
thread ID thread ID
R0 TCB link TCB link
... stack area stack area
R12
SP
LR
RunPt RunPt
PC R0-R12
SP, LR, PC
local variables local variables
return pointers return pointers
Figure 7.5
The running thread uses the actual registers, while the other threads have their
register values saved on the stack.
7.2 Schedulers
A scheduler is an OS component that has responsibility for switching threads
between states. A scheduler has to implement two aspects of this operation. One
A scheduler is
responsible for aspect is to save the currently running thread’s state in its TCB and to restore the
changing the
running thread state of the next thread to run (the process of changing threads, which is also
called a context switch). The other aspect is when the scheduler actually changes
threads, and what it does with waiting threads.
void main(void)
{
OS_AddThread(&ProgA);
OS_AddThread(&ProgA);
OS_AddThread(&ProgB);
OS_Start(TIMESLICE); // doesn't return
}
A circular linked list allows the scheduler to run all three threads equally.
Round-robin
scheduling
RunPt
SP
ProgA ProgB
PC
Figure 8.1
This example illustrates the difference between a program (e.g. ProgA and ProgB)
and a thread (e.g. Thread 1, Thread 2 and Thread 3). Notice that Threads 1 and
2 both execute ProgA. There are many applications where the same program is
being executed multiple times.
A priority scheduler assigns each thread a priority number (e.g. 0 is the highest,
Priority scheduling 15 is the lowest). Two or more threads can have the same priority. A priority 1
thread is run only if no priority 0 threads are ready to run. Similarly, we run a
priority 2 thread only if no priority 0 or priority 1 threads are ready. If all threads
have the same priority, then the scheduler reverts to a round-robin system. The
advantage of priority is that we can reduce the latency (response time) for
important tasks by giving those tasks a high priority. The disadvantage is that on
a busy system, low-priority threads may never be run. This situation is called
starvation.
reloads CVR to
RVR
create a periodic timer
Figure 7.6
There is a current value CVR and a reload value RVR. When the SysTick counter
is enabled, the CVR decrements every clock cycle. If it reaches zero, it will then
load the value from RVR and continue. If the SysTick interrupt is enabled, it will
generate an interrupt when it reloads the value.
There is a Control and Status Register (CSR) that allows us to control and check
the status of the SysTick timer. To generate a periodic interrupt using SysTick,
we need to:
Suppose we have three statically allocated threads that are each allowed to
execute for 125 ms in a round-robin fashion. Even though there are three threads,
there are only two programs to run, ProgA and ProgB. We will have two threads
executing the same program, ProgA, and one thread executing ProgB. The code
for these programs is shown below.
void ProgA(void)
{
int i;
i = 5;
while (1)
{
i = Inc(i);
}
}
void ProgB(void)
{
int i;
i = 6;
while (1)
{
i = Inc(i);
}
}
Even though the threads have not yet been allowed to run, they are created with
an initial stack area that “looks like” the thread has been suspended by the K64
exception mechanism (i.e. the stack looks the same as if an interrupt has
occurred). When a thread is launched for the first time, it will execute the
program specified by the value in the .stackedPC location.
The main() function initialises the low-level hardware and calls OS_Init() and
OS_Start() to start multithreading.
int main(void)
{
Board_Init();
// Initialise OS - sets up SysTick
OS_Init();
// Call OS to start multitasking - never returns
OS_Start(Threads);
}
void SysTick_Handler(void)
{
// Save current context
__asm (\
"str r7, %[input]\n\t"\
".align 4\n\t"\
::[input] "m" (*RunPtr) \
: );
Note that the SysTick ISR has taken on the pre-declared name of
SysTick_Handler which will be automatically placed in the vector table by the
linker.
The assembly language with the comment “Save current context” does exactly
that – it saves the current value of the stack pointer (held in R7) into the thread
control block structure (using *RunPtr).
It then increments its own internal counter Count, and advances the RunPtr
through the linked list to get the next thread control block – this is round-robin
scheduling.
Program execution will now continue from wherever the thread was interrupted.
The OS_Init() function initialises the SysTick timer for a 125 ms interval:
void OS_Init(void)
{
// Disable interrupts
__disable_irq();
// Enable interrupts
__enable_irq();
Listing 7.1 shows how semaphores can be used to guarantee that a thread will
have uninterrupted access to its critical code section. That is, there is mutual
exclusion of other threads.
void p1(void)
{
while (1)
{
...
OS_Wait(&Mutex);
// critical code of p1
...
OS_Signal(&Mutex);
// remainder of p1
...
}
Mutual exclusion }
between threads
void p2(void)
{
while (1)
{
...
OS_Wait(&Mutex);
// critical code of p2
...
OS_Signal(&Mutex);
// remainder of p2
...
}
}
void main(void)
{
// Mutually excluded threads
Mutex = 1;
OS_AddThread(&p1);
OS_AddThread(&p2);
There are two separate functions p1 and p2, each with a critical section of code.
Each of these critical sections are protected between OS_Wait and OS_Signal
operations. A semaphore Mutex is initialised to 1 at the beginning of the main
Listing 7.2 shows how two threads p1 and p2 can synchronise their operations
with each other.
void p1(void)
{
while (1)
{
// some amount of code
...
OS_Wait(&Proceed);
// remainder of p1
...
}
}
void main(void)
{
// Synchronized threads
Proceed = 0;
OS_AddThread(&p1);
OS_AddThread(&p2);
The consumer, at its own speed, removes items from the buffer. Of course the
consumer cannot extract items from an empty buffer nor can the producer
deposit items into a full buffer.
SpaceAvailable This has an initial value of the size of the empty buffer.
BufferAccess This controls access to the buffer so that only one thread,
producer or consumer, can gain access at one time.
void main(void)
{
// producer and consumer threads
OS_AddThread(&Producer);
OS_AddThread(&Consumer);
8 Interfacing
Contents
2020 8 - Interfacing
8.2
Introduction
An embedded system is normally designed to interact with the external world.
They sometimes need to provide a human-machine interface for simple input /
output operations. They also may need to measure analog quantities and output
analog quantities. The following sections look at various techniques of
interfacing to our microcontroller.
Simple switch
interfaces
+3.3 V +3.3 V +3.3 V
Figure 8.1
CMOS digital logic can use either pull-up or pull-down resistors, and the supply
is typically 1.8 V, 2.5 V or 3.3V. In Figure 8.1 (a), a pull-up resistor is used to
convert the mechanical signal into an electrical signal. When the switch is open,
the input port is pulled to +3.3 V. When the switch is closed, the input port is
forced to 0V.
8 - Interfacing 2020
8.3
Figure 8.1 (b) shows a pull-down circuit. When the switch in this circuit is open,
the input is pulled to 0 V. When the switch is closed, the output is forced to
+3.3 V. Notice the logic level of the switch input is reversed in the pull-down
interface as compared to the pull-up case.
All ports on the K64 support both internal pull-ups and pull-downs. That is,
either of the first two circuits in Figure 8.1 could be implemented on the K64
without the resistor, as shown in Figure 8.1 (c).
The software initialization for using a port sets the pull enable (PE) bit in the
PORTx_PCRn register to enable pull-up or pull-down. For each port pin that is
enabled for pull-up or pull-down, the corresponding pull select (PS) bit in the
PORTx_PCRn register determines if it is pull-up (1) or pull-down (0).
Suppose we wish to initialize Port A for the circuit shown in Figure 8.1 (c). The
software below will initialize Port A with the appropriate pull-up and pull-down.
// Port A Bit 0 is connected through a switch to 0 V
// and uses internal pull-up
// Port A Bit 1 is connected through a switch to +3.3 V
// and uses internal pull-down
void PortA_Init(void)
{
// Enable clock gate for Port A to enable pin routing
SIM->SCGC5 |= SIM_SCGC5_PORTA_MASK;
2020 8 - Interfacing
8.4
8.1.2 Hardware Debouncing Using a Capacitor
Switch bounce Most inexpensive switches mechanically “bounce” when touched and when
causes multiple
input changes on an released. Typical bounce times range from 1 ms to 25 ms. Ideally, the switch
input pin resistance is zero (actually about 0.1 ) when closed and infinite when open.
This gives rise to the following switch timing:
Switch timing
showing bounce on
touch and release touch release
bounce
+
actual noise
pin
voltage bounce
+
noise
model of open
pin
voltage closed
5 ms 5 ms
Figure 8.2
Hence, the electrical output “bounces” when using inexpensive switches and
circuits having just a pull-up or pull-down resistor. It may or may not be
important to debounce the switch. For example, if we are entering data via a
keyboard, then we want to record only individual key presses. On the other hand,
if the switch position specifies some static condition, and the operator sets the
switch before turning on the microcontroller, then debouncing is not necessary.
8 - Interfacing 2020
8.5
A hardware method to debounce a switch places a capacitor across the switch to
limit the rise time, followed by an inverter with hysteresis. With this circuit there
is a significant delay from the release of the switch until the fall of the output.
A hardware circuit
that removes switch
+3.3 V bounce
R 1 k 74HC14
K64
vi vo
input port
22
C 10 F
Figure 8.3
If the input switch is closed, its resistance will be about 0.1 , and the output of
the 74HC14 will be high (logic 1). If the input switch is open, its resistance will
be infinite, and the output of the 74HC14 will be low (logic 0). The 22 is used
to limit the discharge current when the switch is pressed (which causes sparks
that produce carbon deposits to build up until the switch no longer works).
2020 8 - Interfacing
8.6
The touch timing with and without the capacitor is shown below:
Switch touch
bounce is removed
by the capacitor
touch
without C
+3.3 V
vi with C +2.0 V
0V
5 ms
with C
+3.3 V
vo without C
0V
5 ms
Figure 8.4
Notice that there is minimal delay between the touching of the switch and the
transition of the Schmitt inverter output. This is because the capacitor is quickly
discharged through the 22 resistor.
8 - Interfacing 2020
8.7
The voltage rise during a bounce interval when the switch is open is given by:
vt VOH 1 e t RC (9.2)
The capacitor is chosen such that the input voltage does not exceed the input
high threshold voltage of the Schmitt trigger during the bouncing.
without C +3.3 V
+2.0 V
with C 0V
t =0
We choose C so that the voltage rise doesn’t pass the Schmitt trigger input high Timing used to
calculate the
threshold of VT 2 V until 5 ms has passed: capacitor value
VT VOH 1 e t RC
VT
1 e t RC
VOH
V t
ln 1 T
VOH RC
t
C
V
R ln 1 T
VOH
5 10 3
2
1 10 3 ln 1
3.3
5.367 μF
Therefore, choose C 10 μF .
2020 8 - Interfacing
8.8
The release timing with and without the capacitor is shown below:
Switch release
bounce is also
removed by the
capacitor release
without C
+3.3 V
vi +2.0 V
with C 0V
5 ms
with C
+3.3 V
vo
0V
without C
5 ms
Figure 8.5
There is a significant delay from the release of the switch until the fall of the
output, since the capacitor charges up slowly through the 1 k resistor.
8 - Interfacing 2020
8.9
Hysteresis is required on the inverter logic gate because the capacitor causes the
“logic” input to rise very slowly. Thus, while the input voltage is in the transition
region between “low” and “high”, a regular logic gate will be operating in its
linear region, and the output will be undefined. Furthermore, any noise on the
input whilst in the transition region would cause a regular gate to toggle with the
noise. The hysteresis removes the extra transitions that might occur with a
regular gate:
Timing showing why
a logic gate with
release hysteresis is used
instead of a regular
assume logic gate
noisy
+3.3 V regular
logic gate
vi transition
0V region
+3.3 V
74HC04
output 0V
+3.3 V
74HC14
output 0V
Figure 8.6
2020 8 - Interfacing
8.10
8.1.3 Software Debouncing
PTA3 +3.3 V
0V
10 ms 10 ms
Figure 8.7
8 - Interfacing 2020
8.11
EXAMPLE 8.3 Software Debouncing – Simple Time Delay
In this example, the microcontroller is dedicated to the interface and does not
perform any other functions while the routines are running. The routine waits for
the switch to be pressed (PTA3 low) and returns 10 ms after the switch is pressed.
void WaitRelease(void)
{
// Loop here until switch is released
while ((GPIOA->PDIR & 0x00000008) == 0);
void PortA_Init(void)
{
// Enable clock gate for Port A to enable pin routing
SIM->SCGC5 |= SIM_SCGC5_PORTA_MASK;
2020 8 - Interfacing
8.12
void FTM_Init(void)
{
// Enable clock gate to FTM0 module
SIM->SCGC6 |= SIM_SCGC6_FTM0_MASK;
8 - Interfacing 2020
8.13
EXAMPLE 8.4 Software Debouncing - Waiting for Stability
In this example, the microcontroller reads the current value of the switch. If the
switch is currently bouncing, it will wait for stability.
read switch
old=switch
start timer
delay is over
10 ms wait
same
old==switch
different
return(old)
A return value of 0 means pressed (PTA3 = 0), and 1 means not pressed
(PTA3 = 1). Notice that the software always waits in a “do nothing” loop for 10
ms. This inefficiency can be eliminated by placing the switch I/O in a foreground
interrupt-driven thread.
2020 8 - Interfacing
8.14
uint32_t ReadPTA3(void)
{
uint32_t old;
void PortA_Init(void)
{
// As before...
}
void FTM_Init(void)
{
// As before...
}
8 - Interfacing 2020
8.15
EXAMPLE 8.5 Software Debouncing - Interrupts
This example simply counts the number of times the switch is pressed. The IC0
interrupt occurs immediately after the switch is pressed and released. Because
the IC0 handler disarms itself, the bounce will not cause additional interrupts.
The OC1 interrupt occurs 10 ms after the switch is pressed and 10 ms after the
switch is released. At this time the switch position is stable (no bounce).
occurs on occurs 10 ms
CHF0 set press and CHF1 set after press
release and release
pressed
EXC_RETURN switch
Count++ released
EXC_RETURN
The first IC0 interrupt occurs when the switch is first touched. The first OC1
interrupt occurs 10 ms later. At this time the global variable Count is
incremented. The second IC0 interrupt occurs when the switch is released. The
second OC1 interrupt does not increment the Count but simply rearms the input
capture system. The initialization routine initializes the system with IC0 armed
and OC1 disarmed.
+3.3 V
2020 8 - Interfacing
8.16
// Counts the number of button pushes
// Button connected to PTA3 = Ch0 of FTM0
void PortA_Init(void)
{
// Enable clock gate for Port A to enable pin routing
SIM->SCGC5 |= SIM_SCGC5_PORTA_MASK;
void FTM_Init(void)
{
// Disable interrupts
__disable_irq();
// Enable interrupts
__enable_irq();
}
8 - Interfacing 2020
8.17
void FTM0_IRQHandler(void)
{
uint8_t channelNb;
Note that the FTM0 ISR has taken on the pre-declared name of FTM0_IRQHandler
which will be automatically placed in the vector table by the linker.
2020 8 - Interfacing
8.18
EXAMPLE 8.6 Software Debouncing – Interrupts with Low Latency
The latency of the previous example is defined as the time when the switch is
touched until the time when the count is incremented. Because of the delay
introduced by the OC1 interrupt, the latency is 10 ms. If we assume the switch is
not bouncing (currently being touched or released) at the time of the
initialization, we can reduce this latency to less than 1 μs by introducing a global
Boolean variable called SwitchPushed. If SwitchPushed is false, then the switch
is currently not pushed and the software is searching for a touch. If SwitchPushed
is true, then the switch is currently pushed and the software is searching for a
release.
// Counts the number of button pushes
// Button connected to PTA3 = Ch0 of FTM0
void FTM_Init(void)
{
// Disable interrupts
__disable_irq();
8 - Interfacing 2020
8.19
// Enable interrupts
__enable_irq();
}
2020 8 - Interfacing
8.20
Note that the FTM0 ISR has taken on the pre-declared name of FTM0_IRQHandler
which will be automatically placed in the vector table by the linker.
Now the latency is simply the time required for the microcontroller to recognize
and process the input capture interrupt. Assuming there are no other interrupts,
this time is less than 50 cycles.
8 - Interfacing 2020
8.21
A simplified ADC
block diagram
AD0
Result
Analog Analog to Registers interrupt
AD1 Sample
Digital
& Hold RA
Converter
AD2 MUX RB
AD23
Figure 8.8
The analog multiplexer (MUX) is just an analog switch that connects one of the The analog
multiplexer is just a
analog channels to the sample-and-hold (S/H) block. switch
2020 8 - Interfacing
8.22
The sample-and-hold circuit consists of a sample capacitor and a buffer. It is
important to take into account the characteristics of the S/H block in the design
The sample and
hold block is used to of the analog interface hardware external to the microcontroller. The external
charge a sample
capacitor to a hardware’s output resistance and the sample capacitor form a first-order lowpass
voltage very close to
filter, also known as a single time constant (STC) circuit, or just a lowpass RC
the applied analog
voltage circuit. This first-order filter determines the amount of time that is needed to
charge the sample capacitor to a voltage that is almost equal to the true analog
voltage.
Analog input circuit,
showing external
resistance and output microcontroller
sample capacitor resistance analog input pin
that form a first- sample
order lowpass filter Rs Ro capacitor
external
analog v s vi Ri Avi C vo
voltage
Figure 8.9
The voltage held on the sample capacitor is fed into the analog-to-digital
converter. There are many types of ADC – the type used in the K64 series of
microcontrollers is a successive approximation architecture (SAR). It functions
by comparing the stored analog sample voltage with a series of digitally
generated analog voltages. By following a binary search algorithm, the ADC
locates the approximating voltage that is nearest to the sampled voltage.
Further Information
A complete description of the ADC module can be found in Chapter 35 of
Freescale’s K64 Sub-Family Reference Manual.
8 - Interfacing 2020
8.23
A simple DAC can also be made from a pulse width modulated (PWM)
waveform. If a PWM waveform is passed through a lowpass filter, and the PWM
has a sufficiently high frequency, then the output of the filter will be a smooth
analog waveform corresponding to the average value of the PWM taken over
many periods.
2020 8 - Interfacing
8.24
8.3.1 Pulse Width Modulator
A pulse width modulator is a device which varies the duty cycle (the “on time”
versus “total time”, in percent) of a square wave. They can be used to turn
transistors on and off in an external circuit to drive devices such as DC motors
and 3-phase AC motors. They can also be used to create a simple digital-to-
analog converter.
In the K64, PWM waveforms are generated by the FlexTimer module (FTM). A
conceptual block diagram of the PWM functionality is shown below:
A simplified PWM
block diagram CNT
Pin
Counter Duty
Logic
PTxn
CnV
Period
MOD + 1
Figure 8.10
Each of the 8 channels of the FTM can be set up as a PWM. Each channel uses
The PWM uses a the common 16-bit counter CNT, and the common modulus value held in MOD.
duty value and a
period value For each channel, the counter compares to two values: a duty value held in CnV;
and the common period value held in MOD. In its simplest mode of operation,
known as edge-aligned PWM, the output is set high when the counter equals the
period value, and the output is set low when the counter equals the duty value.
Figure 8.11
8 - Interfacing 2020
8.25
Various constant duty cycle waveforms are shown below:
Various constant
duty cycle PWM
waveforms
0% duty cycle CnV = 0
MOD = 255
Figure 8.12
The generation of fixed duty cycle square waves is only one application of the
PWM function. It is more generally used to modulate the pulse width of the
square wave. Such a waveform is shown below:
A PWM waveform
that approximates a
sine wave
Figure 8.13
2020 8 - Interfacing
8.26
An external filter is usually not required when the device being driven provides
an inherent filtering function. For example, a DC motor, which exhibits
mechanical inertia, cannot respond to the rapid fluctuations of the PWM
waveform – but it can respond to the slowly varying “average” value of the
waveform. In this case, the DC motor speed would be seen to vary sinusoidally.
There may also be an audible “hum” or “”buzz” due to the high frequency
components being within the range of human hearing (20 Hz –
20 kHz). If such a hum is undesirable, then the designer can increase the
frequency of the PWM wave so that the high frequency components are out of
audio range.
Generation of an
analog voltage via a
PWM output microcontroller
PWM output pin
Ro R
PWM C v o analog
waveform v PWM
waveform
PWM block
microcontroller
digital common pin
Figure 8.14
Further Information
A complete description of the FlexTimer Module can be found in Chapter 40 of
Freescale’s K64 Sub-Family Reference Manual.
8 - Interfacing 2020
9.1
9 Fixed-Point Processing
Contents
Introduction
Most microprocessors are fixed-point devices – they only have support for
Fixed-point means
‘integer’ arithmetic with integers. For example, the ARM® Cortex®-M3 processor does
not have a floating-point unit (FPU). The Cortex®-M4 is a relatively special
MCU because it has the option to include hardware that directly supports single-
precision floating-point numbers – but at the expense of increased cost and
power consumption. PC processors since the 80486DX (released in 1989) have
a “math coprocessor on chip”, and all subsequent generations have included an
FPU. This is why PCs are fast, and expensive – a large proportion of the die area
and power consumption of the CPU is taken up by the FPU.
If you do not use the FPU then compiled code can be used on another Cortex-M
microcontroller product that does not have FPU support, such as the M3.
Floating-point operations can be emulated in software on a fixed-point processor
using special maths libraries, but the resulting overhead results in programs that
run 40-100 times slower than a program that uses just fixed-point operations.
Fixed-point Therefore, when cost, power consumption and speed (i.e. time) is of primary
calculations are
important when time importance in a design, it is necessary to perform arithmetic operations using a
is important
fixed-point processor. We therefore need to examine processing techniques that
use integers but provide an interpretation of the resulting numbers as having
fractional parts.
9.1 Q Notation
Fixed-point calculations are capable of performing fractional mathematics if an
implied binary point is used in the interpretation of the integer used to represent
a fractional quantity. In accordance with accepted digital signal processing
(DSP) notation, we use what is called “Q notation”. The “Q” stands for quotient,
or a number with a fractional part.
Most quantities in signal processing use either 16 bits or 32 bits for their
representation. To express a fractional part, an implied binary point is required
for each quantity. It is up to us as designers to keep track of these implied binary
points throughout any and all calculations. For each quantity, we express its
fractional part with the notation mQn where n is an integer ranging from 0 to 16
for 16-bit quantities or 0-32 for 32-bit quantities. The m tells how many bits are
used in total, either 16 or 32. The n tells how many bits are to the right of the
implied binary point.
Just like a decimal point, a binary point interprets digits to the right of it as being
negative powers of the base. A comparison of a decimal number and its
equivalent binary number is given below:
Comparison of a
decimal number and
Decimal number Equivalent binary number equivalent binary
number
5.625 101.101
Figure 9.1
Mapping integers to
fractional quantities
1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1
Figure 9.2
From this, it should be apparent that to interpret a register value as a mQn value,
we simply divide the raw value by 2 n . To store a fractional number in mQn
notation, we multiply it by 2 n and truncate or round the answer to an integer.
This inherent round-off error cannot be prevented.
For example, if we wished to store the number 5.628 in 16Q3 notation, we get:
5.628 as a 32Q29
number
1 0 1 1 0 1 0 0 0 0 0 1 1 0 0 0 1 0 0 1 0 0 1 1 0 1 1 1 0 1 0 0
1 0 1 1 0 1 0 0 0 0 0 1 1 0 0 0 1 0 0 1 0 0 1 1 0 1 1 1 0 1 0 0
29
Interpreted as a Q29 number = 3021509492 / 2 = 3021509492 / 536870912 = 5.6279999999
Figure 9.3
The reason we can’t store this number exactly is because when we multiply
5.628 by successive powers of two to obtain an integer, the last digits form a
cyclic pattern, that will never reach a multiple of 10:
5.628 2 11 .256
11 .256 2 22 .512
22 .512 2 45 .024
45 .024 2 90 .048 (9.2)
90 .048 2 180 .096
180 .096 2 360 .192
etc.
This shouldn’t really worry us, because a 32Q29 number has a resolution of
2 29 1.8626451 10 9 . The error in storing the above number as shown is
therefore less than 0.00000003 %.
As an aside, we should not forget that using floating-point numbers does not
increase our accuracy. Accuracy is determined purely by the number of bits, not
in the way the number is stored. It shocks some people to find that floating-point
units cannot store the number 0.1, precisely because of the problem stated above.
However, the floating-point number can get very close to 0.1 in the same way
that we can get very close to 5.628.
We can also express numbers using a base other than 2. For example, suppose
we say that the number 1000 is to be interpreted as 1. We say that the number
has 1000 as a base, or unity value, and that 1000 = 1 per unit (p.u.). The number
5.628 in this method would be represented as 5628, which is exact. Why don’t
we use this method over Q notation? The answer is because other numbers can
now not be represented exactly. Remember – the fundamental limit in accuracy
is set by the number of bits, and not how they are interpreted.
It should be noted that Q notation is just representing numbers with bases that
are multiples of two. For example a 16Q3 number is a number with a base or
p.u. value of 8.
Multiplying two numbers together changes the base or “per unit” value. For
example, consider the following multiplication:
Multiplication
changes the base
value
1 0 1 1 0 1 x Register value = 45
Interpreted as a 6Q3 number = 45 / 2 3 = 45 / 8 = 5.625
0 1 0 1 0 Register value = 10
Interpreted as a 5Q2 number = 10 / 2 2 = 10 / 4 = 2.5
Figure 9.4
Two things happen – 1) the length of the result is equal to the sum of the lengths
of the two multiplicands and 2) the Q notation of the result is equal to the sum
of the individual Q notations.
The ARM® Cortex®-M4 processor has two multiply instructions – one giving a
32-bit result, and one giving a 64-bit result. The C compiler will not
automatically increase the result length – two 32-bit operands will theoretically
give a 64-bit result for multiplication, but the C compiler will use the instruction
with a 32-bit result to preserve “type”. Even if we could arrange for a 64-bit
result (we can with assembly language), we can’t multiply the result by another
number, because that would involve a 64-bit x 32-bit multiplication which is not
directly supported by a 32-bit CPU. We have to emulate what a floating-point
unit would do – normalise. This means the 64-bit result must be converted back
to a 32-bit number that has some arbitrary Q notation. For example, if we wished
to convert a result from a 64Q5 number (base 32) to a 32Q3 (base 8) number,
we shift it right 2 bits (divide by 4 which is the amount the base has changed),
and only keep the lower 32 bits. We should note that in shifting, we inevitably
9.3.2 Division
9.3.3 Addition
Additions must be performed with numbers of the same Q notation. If they are
Normalisation different, then normalisation to the larger base is required. For example, to add
before addition
a 6Q3 number and a 5Q2 number, we have to shift the 5Q2 number to the left by
one to create a 6Q3 number before adding:
1 0 1 1 0 1 + 1 0 1 1 0 1 + Register value = 45
Interpreted as a Q3 number = 45 / 2 3 = 45 / 8 = 5.625
= Register value = 20
0 1 0 1 0 0 1 0 1 0 0 Interpreted as a Q3 number = 20 / 2 3 = 20 / 8 = 2.5
1 0 0 0 0 0 1 Register value = 65
Interpreted as a Q3 number = 65 / 2 3 = 65 / 8 = 8.125
Figure 9.5
9.3.4 Subtraction
Similarly, subtraction requires normalisation of the bases so that the larger base
is common.
We will develop the equations that K64 software could need to implement a
digital scale. Assume the range of a position measurement system is 0 to
3 m, and the system uses the K64’s ADC to perform the measurement. We will
assume that the ADC has been put into single-ended 10-bit mode so that the
digital output varies from 0 to 1023. Suppose also that the analog input range is
0 to +3.3 V. Let x be the distance to be measured in metres, Vin be the analog
voltage in volts and N be the 10-bit digital ADC output. Then the equations that
relate the variables are:
Thus:
From this equation, we can see that the smallest change in distance that the ADC
can detect is about 0.003 m. In other words, the distance must increase or
decrease by 0.003 m for the digital output of the ADC to change by at least one
number. It would be inappropriate to save the distance as an integer, because the
only integers in this range are 0, 1, 2 and 3. To save power, we decide not to use
the K64’s FPU and therefore the distance data will be saved in fixed-point
format. Decimal fixed-point is chosen because the distance data for this distance-
meter will be displayed for a human to read. A fixed-point resolution of 0.001 m
could be chosen, because it matches the resolution determined by the hardware.
The table below shows the performance of the system with the resolution set to
0.001 m. The table shows us that we need to store the fixed-point number in a
signed or an unsigned 16-bit variable.
x (m) Vin (V) N I
internal Approximation
distance analog input ADC input representation (41 * N + 7) / 14
0 0.000 0 0 0
0.003 0.003 1 3 3
0.600 0.660 205 600 600
1.500 1.650 512 1500 1499
3.000 3.300 1023 3000 2996
I = (3000 * N) / 1024;
because when N is greater than 21, 3000*N exceeds the range of a 16-bit
unsigned integer. If possible, we try to reduce the size of the integers. In this
case, an approximate calculation can be performed without overflow
I = (41 * N) / 14;
You can add one-half of the divisor to the dividend to implement rounding. In
this case:
I = (41 * N + 7) / 14;
The addition of “7” has the effect of rounding to the closest integer.
No overflow occurs with this equation using unsigned 16-bit maths, because the
maximum value of 41 * N is 41943. If you cannot rework the problem to
eliminate overflow, the best solution is to use promotion. Promotion is the
process of performing the operation in a higher precision. For example, in C we
cast the input as unsigned long, and cast the result as unsigned short:
The other type of error we may experience with fixed-point arithmetic is called
drop out. Drop out occurs after a right shift or a divide, and the consequence is
that an intermediate result loses its ability to represent all of the values. It is very
important to divide last when performing multiple integer calculations. If you
divided first:
I = 41 * (N / 14);
The display algorithm for the unsigned decimal fixed-point number with 0.001
resolution is simple:
31 16 15 0
Whole part Fractional part
Figure 9.6
This is the method used by Sony (original Playstation) and Nintendo (DS,
Gamecube, Gameboy Advance) in their 3D graphics engines to achieve fast
processing performance without an FPU.
With all operands using the same notation, addition and subtraction no longer
require pre-alignment of operands. Multiplication and division, however, still
require some adjustment or else the result will not have the same notation as the
operands. Remember that when you multiply two fixed-point operands together,
their Q notations add:
63 48 47 32 31 16 15 0
Discarded Whole part Fractional part Discarded Product
Figure 9.7
Discarding the least significant 16 bits simply causes some loss of precision;
discarding the most significant 16 bits requires imposing a maximum magnitude
restriction on the operands to avoid overflow.
When you divide one 32Q16 fixed-point operand by another, we require the
result to be a 32Q16 number. We therefore need a 64Q32 dividend, since:
We create a 64Q32 dividend by sign extending the original 32Q16 dividend, and
then left-shifting by 16 bits. The division is then done with a 64-bit dividend and
a 32-bit divisor, to give a 32-bit quotient:
Using integer
division to produce a
63 48 47 32 31 16 15 0 fixed-point quotient
Sign-extended Whole part Fractional part Filled with 0's Dividend
31 16 15 0
Whole part Fractional part Quotient
Figure 9.8
In this case, the variables y, y1, y2, x, x1, and x2 are all 32Q16 fixed-point
integers, and we need to express the constants in 32Q16 fixed-point format. The
value 0.0532672 is approximated by 0.0532672 65536 3491 . The
value 0.0506038 will be approximated by 0.0506038 65536 3316 . Lastly,
the value 0.9025 will be approximated by 0.9025 65536 59146 . The
fixed-point implementation of this digital filter is:
t1 = -3491 * (int64_t)x1;
t2 = 3316 * (int64_t)y1;
t3 = -59146 * (int64_t)y2;
t4 = t1 + t2 - t3;
y = x + x2 + (int32_t)(t4 >> 16);
Note that since we are using C types, we need to allocate space for a 64-bit
product, and thus the 32-bit integer variables are promoted and sign-extended to
64-bits using a typecast. If we did not do this, then the multiplication of two 32-
bit quantities may overflow the 32-bit storage space.
To evaluate the square root of a number, we can use Newton’s method to solve
the equation:
f x R x 2 0 (9.7)
If we have an estimate of the square root, x* , then we can use the above formula
to determine an h to add to x* , which will hopefully be a better estimate of the
square root. We therefore seek an h that satisfies:
f x* h 0
f x* hf x* 0
f x*
(9.9)
h
f x*
x* x* h
(9.10)
lim x* x
n
Applying the above analysis to Eq. (9.7) gives a formula for the new estimate of
the square root as:
f x*
x* x*
f x*
R x*2
x*
2 x*
R x*
x*
2 x* 2
x* R
2 2 x*
R
x*
*
x (9.11)
2
This is easily performed on an integer processor and involves only one division,
one addition and a shift, which is very efficient.
When calculating an RMS value, we can calculate Eq. (9.11) once every sample
time, and use the previous RMS value as the initial estimate. In many instances
we don’t need to iterate more than once since the previous RMS value will
always be a good estimate of the current RMS value.
If we understand
fixed-point
techniques, we can C maths libraries provide square root routines, but when we understand their
optimize
performance
operation, we can optimise our code for performance.
return mag;
}
The function above will return an approximate result since the number of
iterations is fixed. This may be acceptable in certain applications – otherwise the
error between the square of the current root estimate and the original number to
be squared can be used to terminate the iterations.
1. The initial estimate of the magnitude may exceed the range of a uint16_t.
2. Division by zero is not tested for or handled. The ARM® Cortex®-M4 can
produce an exception (usage fault) on division by zero, so we would need
to write an exception handler.
For the square root algorithm, we can perform the following analysis. The
relative error in the square root “answer”, x* , is to be less than a certain value,
, so we have the relation:
x x*
(9.12)
x*
x x* 2 2 (9.13)
2
x
*
x*2 R (9.14)
so that:
x 2 2 xx* R
2 (9.15)
R
x 2 2 xx* R R 2 (9.16)
If we make the approximation that our iterated value is close to the real answer,
then:
xx* R (9.17)
x 2 R R 2 (9.18)
To keep the mathematics integer based, suppose that the allowed relative error
is expressed to the resolution of 1%:
e
e is an integer (9.19)
100
Then we have:
e2
x RR
2
(9.20)
10000
Multiplying both sides by 10000 gives a relationship that uses integers only that
we can use in C code to halt the iterative process:
do
{
// Do an iteration to find a new sqrt value
...
// Find error
error = sqrt * sqrt - sqr;
// Take absolute error
if (error < 0)
error = -error;
} while ((error * 10000) > (sqr * tolerance * tolerance));
Contents
Introduction
A real-time operating system (RTOS) for an embedded system simplifies the
design of real-time software by allowing the application to be divided into
multiple threads managed by the RTOS. The kernel of an embedded RTOS needs
to support multithreading, pre-emption, and thread priority. The RTOS will also
provide services to threads for communication, synchronization and
coordination. A RTOS is to be used for a “hard” real-time system – i.e. threads
have to be performed not only correctly but also in a timely fashion.
Operating systems for larger computers (such as the PC) are non-real-time
operating systems and usually provide a much larger range of application
services, such as memory management and file management which normally do
not apply to embedded systems.
10.1.1 Threads
A thread is a simple program that thinks it has the CPU all to itself. The design
process for a real-time application involves splitting the work to be done into
threads which are responsible for a portion of the problem. Each thread is
assigned a priority, its own set of CPU registers and its own stack area.
Each thread is typically an infinite loop that can be in one of four states: READY,
RUNNING, WAITING or INTERRUPTED.
WAITING
A thread is READY when it can execute but its priority is less than the current
running thread. A thread is RUNNING when it has control of the CPU. A thread
is WAITING when the thread suspends itself until a certain amount of time has
elapsed, or when it requires the occurrence of an event: waiting for an I/O
operation to complete, a shared resource to be available, a timing pulse to occur
etc. Finally, a thread is INTERRUPTED when an interrupt occurred and the CPU
is in the process of servicing the interrupt.
When the multithreading kernel decides to run a different thread, it simply saves
the current thread’s context (CPU registers) in the current thread’s context
storage area (the thread control block, or TCB). Once this operation is
performed, the new thread’s context is restored from its TCB and the CPU
resumes execution of the new thread’s code. This process is called a context
switch. Context switching adds overhead to the application.
10.1.3 Kernel
The kernel is the part of an OS that is responsible for the management of threads
(i.e., managing the CPU’s time) and for communication between threads. The
fundamental service provided by the kernel is context switching.
10.1.4 Scheduler
The scheduler is the part of the kernel responsible for determining which thread
will run next. Most real-time kernels are priority based. Each thread is assigned
a priority based on its importance. Establishing the priority for each thread is
application specific. In a priority-based kernel, control of the CPU will always
be given to the highest priority thread ready to run. In a preemptive kernel, when
a thread makes a higher priority thread ready to run, the current thread is pre-
empted (suspended) and the higher priority thread is immediately given control
of the CPU. If an interrupt service routine (ISR) makes a higher priority thread
ready, then when the ISR is completed the interrupted thread is suspended and
the new higher priority thread is resumed.
Time
ISR
High-Priority Thread
10.2 Reentrancy
A reentrant function can be used by more than one thread without fear of data
corruption. A reentrant function can be interrupted at any time and resumed at a
later time without loss of data. Reentrant functions either use local variables (i.e.,
CPU registers or variables on the stack) or protect data when global variables are
used. An example of a reentrant function is shown below:
Since copies of the arguments to strcpy() are placed on the thread's stack, and
the local variable is created on the thread’s stack, strcpy() can be invoked by
multiple threads without fear that the threads will corrupt each other's pointers.
swap() is a simple function that swaps the contents of its two arguments. Since
Temp is a global variable, if the swap() function gets preempted after the first line
by a higher priority thread which also uses the swap() function, then when the
low priority thread resumes it will use the Temp value that was used by the high
priority thread.
You can make swap() reentrant with one of the following techniques:
Use a semaphore.
Thread priorities are said to be static when the priority of each thread does not
change during the application's execution. Each thread is thus given a fixed
priority at compile time. All the threads and their timing constraints are known
at compile time in a system where priorities are static.
Thread priorities are said to be dynamic if the priority of threads can be changed
during the application's execution; each thread can change its priority at run time.
This is a desirable feature to have in a real-time kernel to avoid priority
inversions.
Priority inversion is a problem in real-time systems and occurs mostly when you
use a real-time kernel. Priority inversion is any situation in which a low priority
thread holds a resource while a higher priority thread is ready to use it. In this
situation the low priority thread prevents the high priority thread from executing
until it releases the resource.
disabling interrupts,
using semaphores.
The easiest and fastest way to gain exclusive access to a shared resource is by
disabling and enabling interrupts, as shown in the pseudocode:
Disable interrupts;
Access the resource (read/write from/to variables);
Reenable interrupts;
Kernels use this technique to access internal variables and data structures. In
fact, kernels usually provide two functions that allow you to disable and then
enable interrupts from your C code: OS_EnterCritical() and
OS_ExitCritical(), respectively. You need to use these functions in tandem,
as shown below:
void Function(void)
{
OS_EnterCritical();
·
· /* You can access shared data in here */
·
OS_ExitCritical();
}
You must be careful, however, not to disable interrupts for too long because this
affects the response of your system to interrupts. This is known as interrupt
latency. You should consider this method when you are changing or copying a
Index Mutual Exclusion PMcL
If you use a kernel, you are basically allowed to disable interrupts for as much
time as the kernel does without affecting interrupt latency. Obviously, you need
to know how long the kernel will disable interrupts.
10.4.2 Semaphores
the semaphore is initialized. The waiting list of threads is always initially empty.
the first thread that requested the semaphore (First In First Out).
Some kernels have an option that allows you to choose either method when the
semaphore is initialized. For the first option, if the readied thread has a higher
priority than the current thread (the thread releasing the semaphore), a context
switch occurs (with a preemptive kernel) and the higher priority thread resumes
execution; the current thread is suspended until it again becomes the highest
priority thread ready to run.
Listing 10.1 shows how you can share data using a semaphore. Any thread
needing access to the same shared data calls OS_SemaphoreWait(), and when the
thread is done with the data, the thread calls OS_SemaphoreSignal(). Both of
these functions are described later. You should note that a semaphore is an object
that needs to be initialized before it is used; for mutual exclusion, a semaphore
is initialized to a value of 1. Using a semaphore to access shared data doesn't
affect interrupt latency. If an ISR or the current thread makes a higher priority
thread ready to run while accessing shared data, the higher priority thread
executes immediately.
OS_ECB* SharedDataSemaphore;
Semaphores are especially useful when threads share I/O devices. Imagine what
would happen if two threads were allowed to send characters to a printer at the
same time. The printer would contain interleaved data from each thread. For
instance, the printout from Thread 1 printing "I am Thread 1!" and Thread
2 printing "I am Thread 2!" could result in:
In this case, use a semaphore and initialize it to 1 (i.e., a binary semaphore). The
rule is simple: to access the printer each thread first must obtain the resource's
semaphore.
Acquire semaphore
SEMAPHORE PRINTER
Acquire semaphore
THREAD 2
"I am Thread 2!"
The above example implies that each thread must know about the existence of
the semaphore in order to access the resource. There are situations when it is
better to encapsulate the semaphore. Each thread would thus not know that it is
actually acquiring a semaphore when accessing the resource. For example, the
UART port may be used by multiple threads to send commands and receive
responses from a PC:
THREAD 1 Packet_Put()
DRIVER UART
THREAD 2 Packet_Put()
Semaphore
Each thread that needs to send a packet to the serial port has to call this function.
The semaphore is assumed to be initialized to 1 (i.e., available) by the
communication driver initialization routine. The first thread that calls
Packet_Put() acquires the semaphore, proceeds to send the packet, and waits for
a response. If another thread attempts to send a command while the port is busy,
this second thread is suspended until the semaphore is released. The second
thread appears simply to have made a call to a normal function that will not
return until the function has performed its duty. When the semaphore is released
by the first thread, the second thread acquires the semaphore and is allowed to
use the serial port.
10
Buffer_Request() Buffer_Release()
Buffer manager
THREAD 1 THREAD 2
Assume that the buffer pool initially contains 10 buffers. A thread would obtain
a buffer from the buffer manager by calling Buffer_Request(). When the buffer
is no longer needed, the thread would return the buffer to the buffer manager by
calling Buffer_Release(). The pseudocode for these functions is shown in
Listing 10.3.
BUF* Buffer_Request(void)
{
BUF* ptr;
Acquire a semaphore;
Disable interrupts;
ptr = BufFreeList;
BufFreeList = ptr->next;
Enable interrupts;
return (ptr);
}
The buffer manager will satisfy the first 10 buffer requests because there are 10
keys. When all semaphores are used, a thread requesting a buffer is suspended
until a semaphore becomes available. Interrupts are disabled to gain exclusive
access to the linked list (this operation is very quick). When a thread is finished
with the buffer it acquired, it calls Buffer_Release() to return the buffer to the
buffer manager; the buffer is inserted into the linked list before the semaphore is
released. By encapsulating the interface to the buffer manager in
Buffer_Request() and Buffer_Release(), the caller doesn't need to be
concerned with the actual implementation details.
A deadlock, also called a deadly embrace, is a situation in which two threads are
each unknowingly waiting for resources held by the other. Assume thread T1 has
exclusive access to resource R1 and thread T2 has exclusive access to resource
R2. If T1 needs exclusive access to R2 and T2 needs exclusive access to R1, neither
thread can continue. They are deadlocked. The simplest way to avoid a deadlock
is for threads to:
Most kernels allow you to specify a timeout when acquiring a semaphore. This
feature allows a deadlock to be broken. If the semaphore is not available within
a certain amount of time, the thread requesting the resource resumes execution.
Some form of error code must be returned to the thread to notify it that a timeout
occurred. A return error code prevents the thread from thinking it has obtained
the resource. Deadlocks generally occur in large multithreading systems, not in
embedded systems.
10.5 Synchronization
A thread can be synchronized with an ISR (or another thread when no data is
being exchanged) by using a semaphore as shown in Figure 10.6.
Signal Wait
ISR THREAD
Signal Wait
THREAD THREAD
Note that, in this case, the semaphore is drawn as a flag to indicate that it is used
to signal the occurrence of an event (rather than to ensure mutual exclusion, in
which case it would be drawn as a key). When used as a synchronization
mechanism, the semaphore is initialized to 0. Using a semaphore for this type of
synchronization is called a unilateral rendezvous. A thread initiates an I/O
operation and waits for the semaphore. When the I/O operation is complete, an
ISR (or another thread) signals the semaphore and the thread is resumed.
Depending on the application, more than one ISR or thread could signal the
occurrence of the event.
Two threads can synchronize their activities by using two semaphores, as shown
in Figure 10.7. This is called a bilateral rendezvous. A bilateral rendezvous is
Signal Wait
THREAD THREAD
Wait Signal
For example, two threads are executing as shown in Listing 10.4. When the first
thread reaches a certain point, it signals the second thread (1) then waits for a
return signal (2). Similarly, when the second thread reaches a certain point, it
signals the first thread (3) and waits for a return signal (4). At this point, both
threads are synchronized with each other. A bilateral rendezvous cannot be
performed between a thread and an ISR because an ISR cannot wait on a
semaphore.
void Thread1(void)
{
for (;;)
{
Perform operation 1;
Signal thread #2; (1)
Wait for signal from thread #2; (2)
Continue operation 1;
}
}
void Thread2(void)
{
for (;;)
{
Perform operation 2;
Signal thread #1; (3)
Wait for signal from thread #1; (4)
Continue operation 2;
}
}
When using global variables, each thread or ISR must ensure that it has exclusive
access to the variables. If an ISR is involved, the only way to ensure exclusive
access to the common variables is to disable interrupts. If two threads are sharing
data, each can gain exclusive access to the variables either by disabling and
enabling interrupts or with the use of a semaphore (as we have seen). Note that
a thread can only communicate information to an ISR by using global variables.
A thread is not aware when a global variable is changed by an ISR, unless the
ISR signals the thread by using a semaphore or unless the thread polls the
contents of the variable periodically. To correct this situation, you should
consider using either a message mailbox or a message queue.
A waiting list is associated with each mailbox in case more than one thread wants
to receive messages through the mailbox. A thread desiring a message from an
empty mailbox is suspended and placed on the waiting list until a message is
received. Typically, the kernel allows the thread waiting for a message to specify
a timeout. If a message is not received before the timeout expires, the requesting
thread is made ready to run and an error code (indicating that a timeout has
occurred) is returned to it. When a message is deposited into the mailbox, either
the highest priority thread waiting for the message is given the message (priority
based) or the first thread to request a message is given the message (First-In-
First-Out, or FIFO). Figure 10.8 shows a thread depositing a message into a
mailbox. Note that the mailbox is represented by an I-beam and the timeout is
represented by an hourglass. The number next to the hourglass represents the
number of clock ticks the thread will wait for a message to arrive.
Mailbox
POST WAIT
THREAD THREAD
10
Initialize the contents of a mailbox. The mailbox initially may or may not
contain a message.
Get a message from a mailbox if one is present, but do not suspend the
caller if the mailbox is empty (ACCEPT). If the mailbox contains a
message, the message is extracted from the mailbox. A return code is
used to notify the caller about the outcome of the call.
As with the mailbox, a waiting list is associated with each message queue, in
case more than one thread is to receive messages through the queue. A thread
desiring a message from an empty queue is suspended and placed on the waiting
list until a message is received. Typically, the kernel allows the thread waiting
for a message to specify a timeout. If a message is not received before the timeout
expires, the requesting thread is made ready to run and an error code (indicating
a timeout has occurred) is returned to it. When a message is deposited into the
queue, either the highest priority thread or the first thread to wait for the message
is given the message. Figure 10.9 shows an ISR (Interrupt Service Routine)
depositing a message into a queue. Note that the queue is represented graphically
Queue
POST WAIT
ISR 10 THREAD
Interrupt 0
Get a message from a queue if one is present, but do not suspend the
caller if the queue is empty (ACCEPT). If the queue contains a message,
the message is extracted from the queue. A return code is used to notify
the caller about the outcome of the call.
10.7 Interrupts
An interrupt is a hardware mechanism used to inform the CPU that an
asynchronous event has occurred. When an interrupt is recognized, the CPU
saves all of its context (i.e., registers) and jumps to a special subroutine called
an Interrupt Service Routine, or ISR. The ISR processes the event, and upon
completion of the ISR, the program returns to:
Time
Thread
ISR1
ISR2
ISR3
Interrupt 1
Interrupt 2
Interrupt 3
Interrupt latency
Interrupt response is defined as the time between the reception of the interrupt
and the start of the user code that handles the interrupt. The interrupt response
time accounts for all the overhead involved in handling an interrupt.
A system's worst case interrupt response time is its only response time. Your
system may respond to interrupts in 50ms 99 percent of the time, but if it
responds to interrupts in 250ms the other 1 percent, you must assume a 250ms
interrupt response time.
Interrupt recovery is defined as the time required for the processor to return to
the interrupted code. Interrupt recovery in a foreground / background system
simply involves restoring the processor's context and returning to the interrupted
thread. Interrupt recovery is given by Eq. (10.4).
Figure 10.11 and Figure 10.12 show the interrupt latency, response, and recovery
for a foreground / background system and a preemptive kernel, respectively.
Time
Interrupt Request
Background Background
Time
Interrupt Request
Interrupt Recovery
Thread 1 Thread 1
Interrupt Recovery
Although ISRs should be as short as possible, there are no absolute limits on the
amount of time for an ISR. One cannot say that an ISR must always be less than
100 ms, 500 ms, or l ms. If the ISR code is the most important code that needs
to run at any given time, it could be as long as it needs to be. In most cases,
however, the ISR should recognize the interrupt, obtain data or a status from the
interrupting device, and signal a thread to perform the actual processing. You
should also consider whether the overhead involved in signalling a thread is
more than the processing of the interrupt. Signalling a thread from an ISR (i.e.,
through a semaphore, a mailbox, or a queue) requires some processing time. If
processing your interrupt requires less than the time required to signal a thread,
you should consider processing the interrupt in the ISR itself and allowing higher
priority interrupts to be recognized and serviced.
A clock tick is a special interrupt that occurs periodically. This interrupt can be
viewed as the system's heartbeat. The time between interrupts is application
specific and is generally between 1 and 200 ms. The clock tick interrupt allows
a kernel to delay threads for an integral number of clock ticks and to provide
timeouts when threads are waiting for events to occur. The faster the tick rate,
the higher the overhead imposed on the system.
All kernels allow threads to be delayed for a certain number of clock ticks. The
resolution of delayed threads is one clock tick; however, this does not mean that
its accuracy is one clock tick.
Figure 10.13 through Figure 10.15 are timing diagrams showing a thread
delaying itself for one clock tick. The shaded areas indicate the execution time
for each operation being performed. Note that the time for each operation varies
to reflect typical processing, which would include loops and conditional
statements (i.e., if/else, switch, and ?:). The processing time of the Tick ISR
has been exaggerated to show that it too is subject to varying execution times.
20 ms
Tick Interrupt
Tick ISR
All higher
priority threads
Call to delay 1 tick (20 ms) Call to delay 1 tick (20 ms) Call to delay 1 tick (20 ms)
Delayed thread
t1 t3
(19 ms) t2 (27 ms)
(17 ms)
Case 1 (Figure 10.13) shows a situation where higher priority threads and ISRs
execute prior to the thread, which needs to delay for one tick. The thread attempts
to delay for 20ms but because of its priority, it actually executes at varying
intervals. This causes the execution of the thread to jitter.
20 ms
Tick Interrupt
Tick ISR
All higher
priority threads
Call to delay 1 tick (20 ms) Call to delay 1 tick (20 ms) Call to delay 1 tick (20 ms)
Delayed thread
t1 t3
(6 ms) t2 (27 ms)
(19 ms)
Case 2 (Figure 10.14) shows a situation where the execution times of all higher
priority threads and ISRs are slightly less than one tick. If the thread delays itself
just before a clock tick, the thread will execute again almost immediately!
Because of this, if you need to delay a thread at least one clock tick, you must
specify one extra tick. In other words, if you need to delay a thread for at least
five ticks, you must specify six ticks!
20 ms
Tick Interrupt
Tick ISR
All higher
priority threads
Call to delay 1 tick (20 ms) Call to delay 1 tick (20 ms)
Delayed thread
t2
t1 (26 ms)
(40 ms)
Case 3 (Figure 10.15) shows a situation in which the execution times of all higher
priority threads and ISRs extend beyond one clock tick. In this case, the thread
that tries to delay for one tick actually executes two ticks later and misses its
deadline. This might be acceptable in some applications, but in most cases it
isn't.
Avoid using floating-point maths (if you must, use single precision).
Because each thread runs independently of the others, it must be provided with
its own stack area (RAM). As a designer, you must determine the stack
requirement of each thread as closely as possible (this is sometimes a difficult
undertaking). The stack size must not only account for the thread requirements
(local variables, function calls, etc.), it must also account for maximum interrupt
nesting (saved registers, local storage in ISRs, etc.). Depending on the target
processor and the kernel used, a separate stack can be used to handle all interrupt-
level code. This is a desirable feature because the stack requirement for each
Unless you have large amounts of RAM to work with, you need to be careful
how you use the stack space. To reduce the amount of RAM needed in an
application, you must be careful how you use each thread's stack for:
interrupt nesting,
You should consider using a real-time kernel if your application can afford the
extra requirements: extra cost of the kernel, more ROM/RAM, and 2 to 4 percent
additional CPU overhead.