Embedded Systems in Mechatronics

Chapter 3
Embedded Programming and Firmware Engineering

Bachelor Program in Mechatronics Engineering
Embedded Systems in Mechatronics   •   Bachelor

Embedded Programming and Firmware Engineering

Introduction

Embedded firmware is the software that gives a mechatronic product its behaviour. It reads sensors, processes information, controls actuators, communicates with other devices, and responds to faults. Unlike general-purpose application software, firmware operates under constraints imposed by limited memory, real-time deadlines, electrical interfaces, energy budgets, safety requirements, and hardware dependencies.

A motor controller, robotic arm, autonomous vehicle, smart actuator, or industrial sensor may contain several processors. Each processor executes firmware whose quality directly affects system performance and safety. A program that is logically correct but too slow, poorly synchronized, vulnerable to memory corruption, or difficult to update is not a reliable engineering solution.

This chapter develops the foundations of professional embedded programming in C and C++. It connects language features to microcontroller hardware and introduces modular design, device drivers, state machines, coding standards, version control, build systems, and cross-compilation.

Learning Objectives

After studying this chapter, the reader should be able to:

  1. Select suitable C/C++ data types for embedded applications.
  2. Apply bitwise operations to registers, flags, and packed data.
  3. Explain pointers, memory regions, stack use, and dynamic allocation risks.
  4. Design modular firmware using interfaces and separation of concerns.
  5. Develop simple device drivers and finite-state machines.
  6. Apply coding standards, defensive programming, and review practices.
  7. Explain version-control, build-system, and cross-compilation workflows.
  8. Analyse firmware reliability, timing, maintainability, and safety.

1. Firmware as a Hardware–Software System

Firmware is neither purely hardware nor purely software. Its behaviour depends on the processor instruction set, compiler, linker, memory map, peripheral registers, interrupt architecture, clock configuration, and electrical environment. The same C statement can produce different machine code on different microcontrollers.

A typical embedded software stack contains:

LayerMain responsibilityExample
ApplicationProduct behaviour and control logicSpeed control, safety policy
ServicesReusable system functionsTiming, logging, queues
Hardware-abstraction layerStable interface to hardwareGPIO, UART, ADC functions
Device driversPeripheral-specific operationI²C sensor driver
Board-support packageBoard pins, clocks, startupPin assignments
HardwareProcessor and peripheralsMCU, sensors, drivers

The stack is a design aid rather than an absolute rule. A small product may combine layers, while a complex product may separate them into independently tested components. The central principle is to prevent application logic from depending unnecessarily on register-level details.

Layered embedded firmware architecture Application logic Services: timing, queues, diagnostics Hardware-abstraction interfaces Device drivers and board support Microcontroller and peripherals
Layered embedded firmware architecture
Figure: Layered embedded firmware architecture

Figure: A layered architecture that isolates application behaviour from hardware details.

2. C and C++ Data Types in Embedded Systems

A data type determines how a value is represented, how much storage it occupies, and which operations are permitted. Embedded developers must distinguish the mathematical meaning of a value from its binary representation.

The standard integer types uint8_t, int16_t, uint32_t, and similar types from <stdint.h> communicate intended width. Their use is preferable to assuming that int has a particular size. The exact width types are available when the implementation supports them; otherwise, the build should fail clearly rather than silently changing behaviour.

Signed integers represent positive and negative values, whereas unsigned integers represent non-negative values modulo a power of two. Unsigned arithmetic is useful for counters and registers but can create surprising results when a negative value is converted or when a counter wraps around. Comparisons between signed and unsigned operands should therefore be reviewed carefully.

The bool type is appropriate for logical conditions. Floating-point types can be useful in calibration, estimation, and control, but their execution time, precision, code size, and hardware support must be considered. Fixed-point arithmetic may be preferable where deterministic execution or low cost is important.

The volatile qualifier tells the compiler that an object may change for reasons not visible in the current code, such as hardware registers or variables shared with an interrupt routine. It does not make an operation atomic, does not provide mutual exclusion, and does not replace synchronization.

The const qualifier expresses that code should not modify an object through a particular access path. It improves interfaces and may permit storage in read-only memory. Enumerations provide meaningful names for states and events, while structures group related fields. Bit-fields can be compact but may have implementation-dependent layout and should not be used casually for hardware register maps.

3. Bitwise Operations and Register Programming

Microcontroller peripherals are commonly controlled through registers. Individual bits may enable a function, report a status condition, select a mode, or acknowledge an interrupt. The principal bitwise operators are:

OperatorMeaningTypical use
&Bitwise ANDTest or clear selected bits
``Bitwise OR
^Bitwise exclusive ORToggle or compare bit patterns
~Bitwise complementInvert a mask
<<Left shiftConstruct a field or mask
>>Right shiftExtract a field

For a register R and mask M, setting bits is normally written R |= M, and clearing bits is written R &= ~M. Testing whether any selected bit is set uses (R & M) != 0. These expressions must use a mask with the same intended width as the register, and operator precedence should be made explicit with parentheses.

A field occupying bits p through p + w − 1 can be updated by first clearing its mask and then inserting a shifted value. The value must be range-checked before insertion. Directly assigning a complete register is unsafe when unrelated bits are reserved or controlled by another function.

Example:

#define MODE_MASK   (0x03u << 4)
#define MODE_SHIFT  4u

uint32_t reg = PERIPHERAL->CONTROL;
uint32_t mode = 2u;
reg = (reg & ~MODE_MASK) | ((mode << MODE_SHIFT) & MODE_MASK);
PERIPHERAL->CONTROL = reg;

The exact register semantics must come from the manufacturer’s reference manual. Some registers are write-one-to-clear, some are read-only, and some require a prescribed access sequence. A generic read–modify–write operation is not automatically correct.

4. Pointers, Memory, and Resource Management

A pointer stores an address. If T p is a pointer to an object of type T, then p accesses the object and &x obtains the address of x. Pointer arithmetic is defined within an array object and advances in units of the pointed-to type. Dereferencing a null, uninitialized, dangling, or incorrectly aligned pointer is undefined behaviour.

Typical memory regions include:

  • Flash or ROM: program instructions and constants.
  • Read–write static memory: global and static variables.
  • Stack: function parameters, return information, and automatic variables.
  • Heap: dynamically allocated objects, if enabled.
  • Memory-mapped I/O: addresses associated with peripheral registers.

The linker script assigns sections to physical memory and reports their sizes. A successful compilation does not prove that the image fits in flash or that the stack is large enough.

Dynamic allocation through malloc, new, and related operations can introduce fragmentation, nondeterministic execution time, leaks, and failure paths. Many safety-critical or deeply embedded systems prohibit heap use after initialization. If dynamic allocation is necessary, allocation policy, ownership, maximum size, failure handling, and lifetime must be explicit.

Resource ownership should be visible in the interface. A function that returns a buffer should specify who releases it; a driver should state whether a handle is exclusive; and an interrupt callback should define which data are shared. RAII in C++ can make ownership safer, but destructors and allocation must still meet timing and memory constraints.

A robust firmware design also considers alignment, endianness, integer overflow, array bounds, and concurrent access. A multi-byte value shared between main code and an interrupt may require atomic access or a critical section. Declaring it volatile alone does not prevent a torn read.

5. Functions, Modules, and Interfaces

Modularity divides a system into components with clear responsibilities. A module normally exposes a small public interface in a header and hides implementation details in a source file. Namespaces and classes can provide stronger encapsulation in C++.

A useful module has high cohesion and low coupling. Cohesion means that its functions belong to one coherent responsibility; coupling means that it depends on other modules. A sensor module should convert raw measurements and report validity, while the control algorithm should not need to know which I²C registers produced the measurement.

Interfaces should specify inputs, outputs, units, valid ranges, blocking behaviour, error reporting, and concurrency assumptions. Avoid hidden global state. Where practical, pass a context structure so that several instances of a driver can coexist and tests can substitute a fake implementation.

Example interface:

typedef enum {
    SENSOR_OK,
    SENSOR_NOT_READY,
    SENSOR_BUS_ERROR,
    SENSOR_INVALID_DATA
} sensor_status_t;

typedef struct {
    uint16_t raw;
    int32_t temperature_milli_deg_c;
} sensor_sample_t;

sensor_status_t sensor_read(sensor_sample_t *sample);

The interface separates status from measurement and makes failure visible. It should also document whether sample may be null, whether the call waits, and how the raw value is scaled.

6. Device Drivers

A device driver translates generic software requests into hardware-specific actions. A driver commonly contains initialization, configuration, data transfer, status reporting, error recovery, and shutdown functions.

Driver development begins with the datasheet and reference manual. The engineer identifies electrical requirements, reset values, timing constraints, register access rules, interrupt behaviour, and error conditions. The implementation should then be divided into a low-level bus layer and a device-specific layer. This allows the device logic to be tested with a simulated bus.

A robust driver should:

  1. Validate configuration arguments.
  2. Establish a known reset state.
  3. Check acknowledgements and status flags.
  4. Apply timeouts rather than waiting forever.
  5. Preserve or document register bits not owned by the driver.
  6. Report errors with enough information for diagnosis.
  7. Recover or fail safely when communication is lost.

Polling is simple and predictable for slow devices, but it can waste processor time. Interrupt-driven operation improves responsiveness but introduces synchronization complexity. Direct memory access can transfer data with low CPU involvement, but buffers, cache coherency, and completion ordering must be managed.

7. Cooperative Scheduling and State Machines

Many embedded applications can be organized as an event loop. Each task performs a bounded amount of work and returns control quickly. A timer, interrupt, or communication event sets a flag or places an event in a queue.

A finite-state machine represents behaviour using states, events, transitions, actions, and guards. For example, a motor controller might contain IDLE, STARTING, RUNNING, and FAULT. A transition from STARTING to RUNNING occurs only when feedback confirms a safe speed; a transition to FAULT occurs when an overcurrent event is received.

State machines avoid deeply nested conditionals and make exceptional behaviour explicit. Each state should define which inputs are accepted, which outputs are safe, and what happens on timeout. The design must prevent impossible states and should define a safe default for unknown or corrupted state values.

A simple implementation is:

typedef enum { IDLE, STARTING, RUNNING, FAULT } state_t;
typedef enum { START_CMD, SPEED_OK, OVERCURRENT, STOP_CMD } event_t;

void controller_step(state_t *state, event_t event)
{
    switch (*state) {
    case IDLE:
        if (event == START_CMD) *state = STARTING;
        break;
    case STARTING:
        if (event == SPEED_OK) *state = RUNNING;
        else if (event == OVERCURRENT) *state = FAULT;
        break;
    case RUNNING:
        if (event == STOP_CMD) *state = IDLE;
        else if (event == OVERCURRENT) *state = FAULT;
        break;
    case FAULT:
        /* Require an explicit reset policy. */
        break;
    default:
        *state = FAULT;
        break;
    }
}

The action of stopping a motor should not be hidden solely in a state assignment. Safety outputs should be controlled explicitly and tested independently.

8. Real-Time Behaviour and Interrupts

A real-time system is judged by whether actions occur within required deadlines, not merely by average speed. Important properties include worst-case execution time, interrupt latency, scheduling jitter, and bounded blocking.

Interrupt service routines should be short. They may capture a timestamp, clear a hardware condition, store data in a buffer, and signal a task. Lengthy calculations, blocking calls, dynamic allocation, and complex logging generally belong outside the interrupt context.

Shared data require a concurrency design. Options include atomic types, disabling interrupts for a very short critical section, lock-free single-producer/single-consumer buffers, or an RTOS synchronization primitive. The chosen mechanism must match the data width, processor architecture, and interrupt priorities.

Timing analysis should use measured worst-case values and documented assumptions. A design that works in a laboratory may fail when logging, communication traffic, or an unusual interrupt sequence increases load.

9. Coding Standards, Testing, and Defensive Engineering

Coding standards reduce variation and make defects easier to detect. Common practices include one clear responsibility per function, explicit conversions, limited scope for variables, meaningful names, documented units, controlled use of macros, and no unexplained constants. Standards such as MISRA C or MISRA C++ are widely used where safety and predictability are important, although compliance requires project-specific interpretation and documented deviations.

Static analysis can identify unreachable code, suspicious conversions, uninitialized variables, possible null dereferences, and rule violations. Compiler warnings should be enabled at a high level and treated as defects unless a reviewed exception exists.

Testing should occur at several levels:

LevelPurpose
Unit testVerify a function or module in isolation
Integration testVerify interfaces between modules
Hardware-in-the-loopExercise real hardware with controlled inputs
System testVerify complete product behaviour
Fault-injection testVerify response to abnormal conditions

A testable design separates pure calculations from hardware access. A control law, packet parser, or state transition function can often be tested on a host computer. Boundary values, invalid packets, timeout paths, counter wraparound, and sensor disconnection deserve explicit tests.

10. Version Control and Build Systems

Version control provides a history of source code, configuration, reviews, and releases. A professional workflow uses small, purposeful commits; descriptive messages; code review; tagged release versions; and reproducible build inputs. Binary build outputs should not normally be stored as the primary source of truth, although release artifacts must be archived with identifying metadata.

A build system describes how source files become an executable image. It should manage compiler options, include paths, libraries, generated files, linker scripts, startup code, and post-build actions such as binary conversion or checksum generation. Incremental builds improve speed, while clean builds expose hidden dependencies.

A useful firmware release records the source revision, compiler version, optimization settings, target board, linker script, configuration values, and dependency versions. Without this information, an image may be impossible to reproduce or diagnose.

11. Cross-Compilation Toolchains

Embedded programs are usually developed on a host computer but executed on a different target processor. A cross-compilation toolchain includes a compiler, assembler, linker, standard libraries, debugger, binary utilities, and often a flashing or programming tool.

The compiler translates source into target instructions. The linker combines object files and places sections according to the target memory map. Startup code initializes memory, sets the stack, copies initialized data, clears zero-initialized data, and eventually calls the application entry point. The linker map is a valuable engineering document because it reveals memory consumption and symbol placement.

Compilation options must be selected deliberately. The target architecture and floating-point ABI must match the hardware and libraries. Optimization can improve timing and size but may complicate debugging and expose undefined behaviour. Debug information should be retained for development even when it is removed from the production image.

A reproducible build should be automated in continuous integration. It should compile from a clean checkout, run static analysis and unit tests, produce a map and image, and identify the exact source revision. Signing and secure boot may be required when firmware authenticity and update security are important.

12. Applied Case: Illustrative Motorized Conveyor Controller

Background

Consider an illustrative conveyor module driven by a brushless motor. The controller receives a start command, reads an encoder, regulates speed, and stops the motor when a guard switch opens or overcurrent is detected.

Problem

An initial implementation directly manipulated registers from the application loop. It used blocking sensor reads, global variables shared with interrupts, and no explicit fault state. Under heavy communication traffic, the motor occasionally continued running briefly after a guard opening.

Analysis

The failure had several contributing causes. The guard input was processed only after a blocking transaction. The interrupt updated a multi-byte encoder count while the main loop read it. Error handling returned a generic failure that the application ignored. The absence of a state machine also made it unclear whether a stop command had priority over a new start command.

Approach

The redesign used four modules: a GPIO driver, an encoder driver, a motor-output driver, and a controller state machine. The guard input generated a high-priority interrupt that disabled the motor-output enable signal immediately and queued a GUARD_OPEN event. The controller entered FAULT, recorded a diagnostic code, and required a deliberate reset sequence.

The encoder driver used a documented atomic snapshot method. The motor driver owned its output registers and provided motor_disable() as an explicit operation. Communication and logging were moved out of interrupt service routines. Timeouts were added to all bus transactions, and unit tests covered every state transition and fault path.

Outcome

The redesigned firmware made the safety response independent of communication workload, made ownership of hardware registers explicit, and allowed the controller logic to be tested on a host computer. Hardware-in-the-loop tests then verified actual response under sensor disconnection, bus errors, and repeated guard transitions.

Learning Points

The case demonstrates that reliability is architectural. A faster processor would not by itself solve hidden blocking, unclear ownership, or missing fault states. Safety-related actions need explicit priority, bounded timing, diagnostic evidence, and verification on representative hardware.

13. Security, Ethics, and Contemporary Practice

Firmware is part of a product’s security boundary. Unsafe update mechanisms, exposed debug ports, weak authentication, and unvalidated network data can permit unauthorized control. Secure development should include threat modelling, input validation, least privilege, protected keys, signed updates, controlled debug access, and vulnerability-response procedures.

Engineers also have ethical responsibilities. A known defect affecting physical safety must be reported and managed rather than concealed. Traceability, review records, test evidence, and change control support accountability. Applicable standards may include functional-safety, cybersecurity, electromagnetic-compatibility, and product-specific regulatory requirements; the relevant jurisdiction and product category determine exact obligations.

AI-assisted coding can accelerate documentation, test generation, and boilerplate development, but generated code must be reviewed, compiled with strict warnings, analysed, tested, and checked against hardware documentation. An AI system does not establish correctness, ownership, licensing status, or safety compliance. Engineers remain responsible for the deployed firmware.

Best-Practice Checklist

  • Define timing, memory, safety, and update requirements before implementation.
  • Use fixed-width types when representation matters.
  • Treat compiler warnings and static-analysis findings seriously.
  • Keep interrupt routines short and bounded.
  • Make ownership, units, ranges, and error behaviour explicit.
  • Isolate hardware access behind tested interfaces.
  • Prefer deterministic allocation and avoid uncontrolled heap use.
  • Use explicit state machines for operational and fault behaviour.
  • Automate builds, tests, analysis, and release identification.
  • Preserve evidence that the firmware was reviewed and verified.

Chapter Summary

Embedded firmware connects mechatronic algorithms to physical hardware under constraints of time, memory, reliability, and safety. Fixed-width data types, careful conversions, bitwise operations, and correct pointer use are fundamental to trustworthy low-level programming. Memory must be understood in terms of flash, static storage, stack, heap, and memory-mapped registers.

Modular interfaces separate application intent from device-specific mechanisms. Drivers should validate inputs, enforce timeouts, preserve register ownership, and report faults. State machines make normal and abnormal behaviour explicit, while bounded event processing and disciplined interrupt use support real-time performance.

Professional firmware engineering extends beyond source code. Coding standards, static analysis, layered testing, version control, reproducible build systems, linker-map inspection, and cross-compilation toolchains create traceable and maintainable products. Security, ethical responsibility, and careful review of AI-assisted code are now part of competent embedded practice.

Key Terms

Firmware: Software stored in or closely associated with electronic hardware.

Cross-compilation: Building software on one computer for execution on another processor.

Volatile: A qualifier indicating that an object may change outside the visible program flow.

Pointer: An object that stores an address.

Memory-mapped I/O: Hardware registers accessed through processor address space.

Bit mask: A pattern used to select particular bits.

Device driver: Software that operates a hardware peripheral through a defined interface.

HAL: Hardware-abstraction layer that hides hardware-specific details.

Interrupt service routine: A function executed in response to an interrupt.

Atomic operation: An operation that appears indivisible to relevant concurrent observers.

State machine: A model based on states, events, transitions, guards, and actions.

Watchdog: A timer that can reset or fault a system if software fails to service it.

Stack: Memory used for active function calls and automatic storage.

Heap: Memory region used for dynamic allocation.

Linker script: Configuration that assigns program sections and symbols to target memory.

Static analysis: Automated examination of source or binaries without normal execution.

Toolchain: Compiler, linker, libraries, debugger, and utilities used to create firmware.

Race condition: A defect in which outcome depends on uncontrolled timing between concurrent activities.

Undefined behaviour: Program execution for which the language standard imposes no requirements.

Traceability: The ability to connect requirements, implementation, tests, and released artifacts.

Review Questions

Short Answer Questions

  1. Why are fixed-width integer types useful in embedded firmware?
  2. What is the difference between setting and clearing bits in a register?
  3. Why does volatile not guarantee atomic access?
  4. What responsibilities normally belong to a device driver?
  5. What information should be recorded for a reproducible firmware release?

Long Answer Questions

  1. Explain how memory regions, pointers, and dynamic allocation affect embedded reliability.
  2. Describe a layered firmware architecture and discuss the benefits and trade-offs of hardware abstraction.
  3. Explain how a finite-state machine can represent both normal operation and fault handling in a mechatronic controller.
  4. Compare polling, interrupt-driven I/O, and DMA with respect to timing, complexity, and resource use.
  5. Discuss how coding standards, static analysis, testing, version control, and build automation work together in a professional firmware process.

Analytical / Application Questions

  1. Design the states, events, and fault transitions for a robotic gripper that must detect open, closing, gripping, released, and emergency-stop conditions.
  2. A peripheral register contains unrelated status bits and a two-bit configuration field. Explain how you would update only the configuration field without altering status bits, and identify assumptions requiring confirmation from the reference manual.
  3. Analyse a firmware design in which an interrupt updates a shared 32-bit counter while the main loop reads it on an 8-bit microcontroller. Propose two safe designs and discuss their timing implications.

Multiple Choice Questions with Answers

Question 1

Which qualifier indicates that a hardware register may change independently of the current program flow?

A. const

B. volatile

C. static

D. extern

Correct Answer: B. volatile

Question 2

Which expression sets all bits selected by mask M in register R?

A. R &= M

B. R &= ~M

C. R |= M

D. R ^= ~M

Correct Answer: C. R |= M

Question 3

What is the primary purpose of a linker script in an embedded build?

A. To define variable naming rules

B. To assign program sections to target memory

C. To generate unit-test cases

D. To encrypt source files

Correct Answer: B. To assign program sections to target memory

Question 4

Which activity is generally most appropriate inside a short interrupt service routine?

A. Waiting for a peripheral indefinitely

B. Performing dynamic memory allocation

C. Capturing data and signalling deferred work

D. Printing a lengthy diagnostic report

Correct Answer: C. Capturing data and signalling deferred work

Question 5

What is a major risk of uncontrolled dynamic memory allocation in a long-running embedded system?

A. Improved type safety

B. Memory fragmentation and nondeterministic failure

C. Automatic register preservation

D. Guaranteed constant execution time

Correct Answer: B. Memory fragmentation and nondeterministic failure

Question 6

What does a finite-state machine explicitly represent?

A. Only processor clock frequency

B. States, events, transitions, and actions

C. Only memory addresses

D. Compiler optimization levels

Correct Answer: B. States, events, transitions, and actions

Question 7

Why should application logic normally avoid direct manipulation of peripheral registers?

A. Registers cannot be accessed by C

B. It increases coupling and reduces portability and testability

C. It always reduces execution speed

D. It prevents all interrupts

Correct Answer: B. It increases coupling and reduces portability and testability

Question 8

Which practice best supports reproducible firmware builds?

A. Changing compiler versions without recording them

B. Building only from a developer’s local workspace

C. Recording source revision, toolchain, options, and dependencies

D. Deleting linker maps after each build

Correct Answer: C. Recording source revision, toolchain, options, and dependencies

Question 9

What does cross-compilation mean?

A. Running two operating systems simultaneously

B. Building software on one platform for execution on another

C. Compiling only header files

D. Converting binary data into analog signals

Correct Answer: B. Building software on one platform for execution on another

Question 10

Why is a high-priority guard interrupt useful in the conveyor-controller case?

A. It allows logging to continue indefinitely

B. It makes the safety response less dependent on communication workload

C. It removes the need for a fault state

D. It guarantees that all bus transactions succeed

Correct Answer: B. It makes the safety response less dependent on communication workload

Embedded Systems in Mechatronics
Chapter 3 of 15