Design Patterns for accessing hardware
Probably the most distinguishing property of embedded systems is that they must access hardware directly. The whole point of an embedded system is that the software is embedded in a “smart device” that provides some specific kind of service, and that requires accessing hardware. Broadly, software-accessible hardware can be categorized into four kinds: infrastructure, communications, sensors and actuators.
Infrastructure hardware refers to the computing infrastructure and devices on which the software is executing. This includes not only the CPU and memory, but also storage devices, timers, input devices such as keyboards, knobs and buttons, user output devices (printers, displays and lights), ports and interrupts.
De modo geral, uma configuracao pode envolver a manipulacao de apenas um bit ou um conjunto de bits.
It is exceedingly common to have hardware that uses bit fields to specify commands to the hardware or to return data. A bit field is a continuous block of bits (one or more) within an addressable memory element (e.g., a byte or word) that together has some semantic meaning to the hardware device.
For example, an 8-bit byte might be mapped to a hardware device into four different fields:
0 0 0000 00
The bits represent the following information to or from the memory-mapped hardware device:
| Bit range | Access | Name | Description |
|---|---|---|---|
| 0 | Write only | Enable bit | 0 = disable device, 1 = enable device |
| 1 | Read only | Error status bit | 0 = no error, 1 = error present |
| 2-5 | Write only | Motor speed | range 0000 = no speed to 1111 (16d) top speed |
| 6-7 | Write only | LED color | 00 (0d) = OFF, 01 (1d) = GREEN, 10 (2d) = YELLOW, 11 (3d) = RED |
Bit fields are manipulated with C’s bit-wise operators & (bit-wise AND), | (bit-wise OR), ~ (bit-wise NOT), ^ (bit-wise XOR), >> (right shift) and << (left-shift), in a technique called Read-Modify-Write.
The Read-Modify-Write operation
A read-modify-write operation is a class of atomic operations, such as test-and-set, fetch-and-add, and compare-and-swap that both read a memory location and write a new value into it simultaneously, either with a completely new value or some function of the previous value.
Any instruction that specifies a file register as part of the instruction performs a Read-Modify-Write (RMW) operation. The register is read, the data is modified, and the result is stored according to either the Working register, or the originating file register, depending on the state of the destination designator. A read operation is performed on a register even if the instruction writes to that register.
The read-modify-write operation ensures that you modify only the specific bits in a system register that you want to change.
Individual bits in a system register control different system functionality. Modifying the wrong bits in a system register might cause your program to behave incorrectly.
Using the bitwise Operators to access bits
To perform a read-modify-write operation on a microcontroller register in C, you must first read the current value, then modify it using bitwise operators like & (AND), | (OR), and ^ (XOR) to change specific bits, and finally write the new value back to the register. This sequence is critical because some registers, especially I/O ports, return a value based on the pin state, not the last value written, so directly assigning a new value may not work as expected
Most machines provide a variety of operations for manipulating individual bits of a word or other addressable units, often referred to as “bit twiddling”. They are based upon Boolean operations.
In addition to bitwise logical operations, most machines provide a variety of shifting and rotation functions. The most basic operations are a logical shift, where the bits of a word are shifted left or right. On one end, the bit shifted out is lost. On the other end, a zero is shifted in. Logical shifts are useful primarily for isolating fields within a word.
When we do low-level programming, we’ll often need to store information as single bits or collections of bits.
Using the bitwise operators, we can extract or modify data that’s stored in a small number of bits.
The procedure is then done the following way:
Step 1: Read the current value
Get the current value of the register into a temporary variable. You do this by creating a pointer to the register’s memory address and dereferencing it:
// Define the register address and type
volatile unsigned int *register_ptr = (volatile unsigned int *)0xXXXXXXXX; // Replace with the actual address
// Read the current value
unsigned int temp_value = *register_ptr;
Step 2: Modify the value
This is done using one of the several bitwise operations in C.
Setting a bit
Suppose we want to set bit 4 of i. (we’ll assume that the leftmost = or most significant – bit is numbered 15 and the least significant is number 0).
The easiest way to set bit 4 is to or the value of i with the constant 0x0010 ( a “mask” that contains a 1 bit in position 4):
i = 0x000; /* is is 0000000000000000*/
i |= 0x0010; /* i is now 000000000010000*/
i |= 1 << j; /* sets bit j */
Clearing a bit
To clear bit 4 of i, we’d use a mask with a 0 bit in position 4 and 1 bits everywhere else:
i = 0x00ff; /* i is now 0000000011111111 */
i &= ~0x0010; /*i is now 0000000011101111 */
Using the same idea, we can easily write a statement that clears a bit whose position is stored in a variable:
i &= ~(1 << j); /* clears bit j*/
Testing a bit
The following if statement tests whether bit 4 of i is set:
if(i & 0x0010) /* tests bit 4 */
To test whether bit j is set, we’d use the following statement:
if(i & 1 << j) ... /* tests bit j */
Toggle a bit
// Define the mask for the bit you want to toggle (e.g., bit 3)
#define TOGGLE_MASK (1 << 3)
temp_value = temp_value ^ TOGGLE_MASK;
Step 3: Write the new value
Write the modified value back to the register
*register_ptr = temp_value;
A full example of these steps can be summarised at the following example, where the bit 5 of a register which address is 0x40001000 is set:
// Assume you want to set bit 5 of a register at address 0x40001000
volatile unsigned int *gpio_port_b = (volatile unsigned int *)0x40001000;
unsigned int mask = (1 << 5);
// Read, modify, and write
*gpio_port_b = *gpio_port_b | mask;
A register holds a certain configuration, and any modification results in a new configuration. In general terms, this is the usual technique to manipulate bits in a microcontroller.
Read-only or write-only registers
Some registers, or bits within a register, can be read-only or write-only. For write-only registers, read-modify-write operations such as |=, &= and ^= cannot be used. In this case, a shadow copy of the register’s content should be held in a variable in RAM to maintain the current state of the write-only register. An example of a write-only register using a shadow copy
timerRegValue
follows:
/* initiaise timer write-only register */
timerRegValue = TIMER_INTERRUPT;
*pTimerReg = timerRegValue
After the shadow copy and timer register have been initialised, subsequent writes to the register are performed by first modifying the shadow copy timerRegValue and then writing the new value to the register. For example,
timerRegValue |= TIMER_ENABLE;
*pTimerReg = timerRegValue;
Bit fields
A bit field is a field of one or more bits within a larger integer value. Bitfields are useful for bit manipulations and are supported within a struct by C language compilers.
struct
{
uint8_t bit0 : 1;
uint8_t bit1 : 1;
uint8_t bit2 : 1;
uint8_t bit3 : 1;
uint8_t nibble : 4;
} foo;
Bits within a bitfield can be individually set, tested, cleared and toggled without affecting the state of the other bits outside the bitfield.
To test bits using the bitfield, use code such as the following:
if(foo.bit0)
{
//do something
}
if(foo.nibble == 0x03)
{
//do something else
}
To set a bit using a bitfield, use this code:
foo.bit1 = 1;
and use code such as the following to set multiple bits in a bitfield:
foo.nibble = 0xC;
To clear a bit using the bitfield, use this code:
foo.bit2 = 0;
and to toggle a bit using the bitfield, use this:
foo.bit3 = ~foo.bit3; // or !foo.bit3;
There are a couple of problems with bit fields in C. First, bit ordering is compiler- and processor- dependent. Some compilers start from the least significant bit, while others start from the most significant bit. In some cases, the compiler may require enclosing the bitfield within a union; doing this makes the bitfield code portable across ANSI C compilers. Further, the compiler may enforce byte padding rules, meaning that it may not map onto a memory-mapped device as you expect.
Even worse, because most CPUs must write a byte or word at a time, bit fields may not be written in an atomic step, leading to thread safety issues if separate mutexes are used for different bit fields.
Another potential problem with using bit fields is that it is impossible to cast between a scalar and a user-defined struct. Thus, the following is disallowed:
typedef struct _statusBits
{
unsigned enable : 1;
unsigned errorStatus : 1;
unsigned motorSpeed : 4;
unsigned LEDColor: 2;
} statusBits;
statusBits status;
unsigned char f;
f = 0xF0;
status = (statusBits) f;
Bitmasks are more efficient than bitfields in certain instances. Specifically, a bitmask is usually a better way to initialise several bits in a register.
Setting and clearing bits using a bitfield is no faster than using a bitmask; with some compilers, it can be slower to use a bitfield. One benefit of using bitfields is that individual bitfields may be declared volatile or const. This is useful when a register is writeable but contains a few read-only bits.
Bit-band operations
Bit-banding was introduced in the ARM Cortex-M architecture primarily to address common challenges in embedded systems where real-time performance, memory efficiency, and safe access to shared resources are critical. While bit-banding offers several advantages, it also comes with a few disadvantages and limitations, particularly in terms of flexibility and practicality in certain situations.
There is no native support of bit-band operations in C/C++ languages. For example, C compilers do not understand that the same memory can be accessed using two different addresses, and they do not know that accesses to the bit-band alias will only access the LSB of the memory location.
To use the bit-band feature in C, the simplest solution is to separately declare the address and the bit-band alias of a memory location.
To modify a bit by bit-banding method we need to calculate the alias address and the banding address of this bit.
Bit-banding provides a solution by allowing atomic manipulation of individual bits without the need to copy data in CPU register. This ensures that bit-level operations are safe, efficient, and deterministic, which is essential for real-time performance. It helps prevent race conditions that might otherwise occur when multiple tasks attempt to modify the same bit at the same time.
Traditionally, to ensure that bit-level modifications (like setting a flag or toggling a GPIO pin) are done safely without being interrupted (e.g., by an interrupt or another task), developers would use critical sections — temporarily disabling interrupts or locking access. However, this approach increases complexity and can affect real-time behaviour by introducing delays or priority inversions.
Bit-banding eliminates the need for critical sections for bit manipulations, allowing individual bits to be set or cleared atomically in a single cycle. This helps maintain real-time performance without the drawbacks of disabling interrupts or using locks.
To use bit-banding, you need to calculate the bit-band alias address manually . This involves applying a specific formula to derive the alias address, which can add complexity and potential for errors in the code. For simple applications, this may not be an issue, but in more complex systems, the calculations can be cumbersome, especially when many different bits need to be accessed across various addresses.
Bit-banding is not Always the Most Efficient. For simple operations on individual bits within a word, traditional bitwise operations (e.g., using AND, OR, XOR) may be more efficient in terms of code readability, size, and performance. For example, clearing or setting multiple bits at once may be easier and faster with a direct bitwise operation, avoiding the need to calculate and access multiple bit-band alias addresses. Bit-banding shines when atomic operations are required on a single bit, but if atomicity isn’t needed, the overhead of using bit-banding might not be justified.
The advantage of memory optimization comes with the following disadvantages: because Bit-banding is not directly supported by most high-level programming languages like C or C++ developers need to calculate the alias address manually or use custom macros to perform bit-banding. This adds extra steps to the development process, which can increase code complexity and maintenance effort. If your project involves manipulating many different bits across different memory locations, managing all the bit-band alias addresses can become a burden. Each bit requires an alias address calculation, which can clutter the code and make it harder to maintain and debug in large applications. Bit-banding provides no direct hardware feedback mechanism. It works silently, which means that debugging issues related to bit-banding might be more challenging since there is no built-in status indicator.
However, bit-banding is not present in all ARM architectures, but only in CortexM3 and CortexM4; this limitation leads to the inability to port code to other architectures, reducing its attractiveness to developers who work on multiple platforms and need portability.
What about my Arduino?
As Arduino boards are designed around a well-established microcontroller, either from the old ATMEL versions or the most recent STM32 and Renesas vendors, the same rules described earlier apply, especially when the programmer is writing bare-metal code in one of the hardware-oriented programming languages, such as C, C++ or Assembly.
However, the Wiring library provides some functions that wrap this complexity.
Using function
bitSet()
Sets (writes a 1 to) a bit of a numeric variable at a specific position. Useful when doing low-level bit manipulation, especially when working with hardware registers, flags, or memory-mapped I/O.
Syntax:
Use the following function to set the bit state on the n position of the x variable:
bitSet(x, n)
where the parameters are
x : the numeric variable whose bit to set.
n: which bit to set, starting at 0 for the least-significant (rightmost) bit.
and returns he value of the numeric variable after the bit at position n is set.
Behind the scenes, the function does the same old trick:
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
Example code:
Modify a given byte x by turning its 5th bit to 1:
uint8_t x = 0b10000001; // initial byte
void setup() {
Serial.begin(9600);
int index = 5; // index of the bit to modify
x = bitSet(x, index-1);
Serial.print("The resulting byte is: ");
Serial.println(x, BIN);
}
void loop() {
}
Why use bitwise operations instead of a simple assignment in C?
Using bitwise operations instead of a simple assignment in C is essential when you work with hardware registers, flags, or structures where each bit has its own purpose.
Using the equality “=” overwrites the entire value of the register, meaning that this operation sets all bits to the values specified in the instruction. This includes both the bits that need to be intentionally configured and those that do not.
you completely replace the contents of the register.
This is dangerous because:
- Reserved bits must keep specific values.
- Some bits have special semantics (W1C, W1S, etc.).
- You may accidentally change flags you didn’t intend to touch.
If the written value matches the documented default value, that is,
REGISTER = 0;
//when the default value is zero
then those bits will effectively return to the default value.
The hardware does not “return to the default value on its own” just because the equality operator was used.
It is essential to build the complete register mask with all fields/bits set to the desired values (including the “reserved” ones, as instructed by the datasheet).
Bitwise operations let you change only certain bits, for example
REG |= MASK;
It sets only the 1-bits in the mask and keeps the rest of the register intact.
REG &= ~MASK;
It clears only the specified bits and does not affect the others.
This is exactly what you want when a register has multiple fields with different functions.
Using “|=” performs a cumulative OR operation: it sets to 1 only the bits you place as 1 in the mask and leaves all other bits unchanged. This is useful for setting bits without affecting others, and it preserves previous values in bits not mentioned.
It often needs to be paired with “&= ~mask” to clear specific bits. The safe pattern is usually the “read–modify–write”:
R = REG;
R &= ~MASK_CLEAR;
R |= MASK_SET;
REG = R.
This ensures that:
- you only modify the intended bits;
- all reserved bits remain correct;
- the documented behaviour is respected.
Setting a register bit using = instead of |= is useful when:
- You want to set the entire register state at once – = overwrites all bits, while |= only sets specific bits without clearing others.
-
Initialization – When first configuring a peripheral, you often want a known state:
DDRD = 0x10; // Set PD4 as output, all others as input
- Atomic multi-bit operations – When you need to change multiple bits simultaneously:
PORTD = 0x18; // Set PD3 and PD4 high, clear all others
Example: setting and resetting bits in the BSRR register in STM32.
- Clearing unwanted bits – If other bits might be set and you need them cleared:
PORTD = 0x10; // Only PD4 high, ensures all others are low
- Performance – Single assignment is faster than read-modify-write (|= requires reading current value first).
Use = when you need complete control over the register state or don’t care about other bits. Just be careful not to accidentally clear bits that other parts of your code depend on!
Many registers require specific write patterns. In microcontrollers/SoCs, it’s common to have:
- W1C (write 1 to clear)
- W1S (write 1 to set)
- RC (read-clear)
- WO (write-only)
- Bits that ignore 0 or 1, etc.
A simple assignment (=) can:
– clear flags you didn’t intend to,
– trigger unintended operations,
– cause undefined behaviour.
The safe pattern is almost always read–modify–write.
Using bitwise operations is essential because they:
- prevent overwriting unwanted bits
- respect special write semantics
- keep reserved bits intact
- avoid undefined behavior
- implement the correct pattern recommended by manufacturers
A simple assignment should only be used when you want to write the entire register, with all bits set exactly as specified.
References
K.N. King
C Programming – A Modern Approach
Second edition
Stallings, William.
Computer Organization and Architecture – Designing for performance
Tenth edition
Douglass, Bruce Powel
Design Patterns for Embedded Systems in C – An Embedded Software Engineering Toolkit
Barr, Michael & Massa, Antony
Programming Embedded Systems
With C and GNU Development Tools
Second Edition
https://docs.arduino.cc/language-reference/en/functions/bits-and-bytes/bitSet/
https://bit-by-bit.gitbook.io/embedded-systems/perifericos-mapeados-em-memoria/configuracao-de-registradores
https://docs.arduino.cc/retired/hacking/software/PortManipulation/
https://developer.arm.com/documentation/dui0379/e/writing-arm-assembly-language/the-read-modify-write-operation
https://onlinedocs.microchip.com/oxy/GUID-08F34A7C-AE69-443E-863E-D5ED2FFA578F-en-US-4/GUID-30E8B6B3-92CB-46EB-ADE7-541724D6C1FF.html
https://medium.com/@levinet.nicolai/strengths-and-weaknesses-for-bit-banding-in-arm-cortex-m-58217cd12260
