LABORATORY 1B

Embedded Programming - Working with Bits and Registers

Duration: 2 hours Type: preparatory session Required for: Lab. 2-7 Platform: any microcontroller PDF: download the material RO versiunea română

This preparatory session fixes the one technique without which the rest of the semester makes no sense: manipulating individual bits of a register. Library functions such as digitalWrite() exist only on Arduino; bit masks work identically on AVR, STM32, ESP32, PIC, or any other microcontroller in the world.

Extra sessionThis session has no counterpart in the printed handout. It covers the notions assumed known by the following labs, and it is recommended to go through it before Laboratory 2.

1Lab objectives

  • Quick conversion between binary, hexadecimal and decimal
  • Correct use of the six bitwise operators
  • Building masks with the shift operator
  • Applying the four fundamental idioms: set, clear, toggle, test
  • Writing and reading multi-bit fields
  • Understanding the nature of read-modify-write and atomicity issues
  • Writing portable code, independent of the Arduino library

2Why we work directly on registers

A microcontroller is configured exclusively by writing values into registers - special memory locations wired directly to hardware. Each bit of a register controls a concrete function: a pin's direction, enabling a peripheral, choosing a prescaler.

AspectLibrary functionDirect register access
Speed~50 cycles (about 3 µs at 16 MHz)1 cycle (62.5 ns)
Flash memory usedhundreds of bytes of helper codeone instruction
Portabilityonly where the library existsanywhere, with different register names
Controlonly what the library author providedeverything the hardware allows
Readabilityvery good for beginnersrequires the documentation (datasheet)
Debugginghides what is happeningdirect correspondence with the datasheet
Where this course standsWe will keep using the Arduino functions too, because they speed up prototyping. But every notion will also be shown at the register level: that is the only way to read a datasheet, use a peripheral the library does not expose, and move from Arduino to STM32 or ESP32 without relearning everything.
Library functions do not cover everythingThere is no ready-made function to configure a hardware counter to drive a pin on its own, or to start an analog-to-digital conversion triggered by a comparator - things you will do in the following labs, writing directly into registers. Once an application goes past the demo stage, register access becomes mandatory.

3Numbering systems

An 8-bit register is naturally written in binary, but binary is hard to read. Hexadecimal is the standard compromise: each hexadecimal digit corresponds to exactly 4 bits.

BinaryHexDecimalBinaryHexDecimal
000000100088
000111100199
0010221010A10
0011331011B11
0100441100C12
0101551101D13
0110661110E14
0111771111F15
the same value, three notations
0b10110100      // binary  - every bit is visible
0xB4            // hex     - compact: B=1011, 4=0100
180             // decimal - says nothing about the bits

// all three are IDENTICAL to the compiler
Quick binary to hex conversionGroup the bits in fours, from the right: 1011 0100 → B and 4 → 0xB4. The reverse is just as simple. With practice, the conversion becomes instant - and you will need it on every page of a datasheet.
Bit position76543210
Decimal value1286432168421
1 << n0x800x400x200x100x080x040x020x01
An 8-bit register shown as eight memory cells
Fig. 1 - An 8-bit register: eight cells, each holding 0 or 1. The value shown is 0b01100001 = 0x61 = 97. Fig. 2.3 in the handout

4Bitwise operators

OperatorNameRuleUsed for
&AND1 only if both bits are 1testing and clearing bits
|OR1 if at least one bit is 1setting bits
^exclusive OR1 if the bits differtoggling bits
~negationinverts every bitbuilding clearing masks
<<left shiftmoves bits left, fills with 0positioning the mask
>>right shiftmoves bits rightextracting fields

Truth tables

ABA & BA | BA ^ B~A
000001
010111
100110
111100
& is not &&1 & 2 gives 0 - the bits do not overlap (01 and 10). But 1 && 2 gives 1 - both values are nonzero, so true. The same trap exists between | and ||. The confusion produces bugs that compile with no warning at all.

The properties that make everything work

PropertyPractical consequence
x | 0 = xOR with 0 leaves the bit unchanged → bits set to 0 in the mask stay untouched
x | 1 = 1OR with 1 forces it to 1 → bits set to 1 in the mask get set
x & 1 = xAND with 1 leaves the bit unchanged → bits set to 1 in the mask stay untouched
x & 0 = 0AND with 0 forces it to 0 → bits set to 0 in the mask get cleared
x ^ 0 = xXOR with 0 leaves the bit unchanged
x ^ 1 = ~xXOR with 1 inverts the bit
This is the whole secretEvery idiom below follows from this table. If you understand it, there is nothing left to memorize.

5Operator playground

Toggle bits directly on the binary representation and watch how the result forms, position by position. Experimentally verify every property from the table above.

Bitwise operators - bit by bit

6Building masks

A mask is a value in which the bits we care about are marked with 1. It is built with the shift operator, starting from the value 1:

how a bit gets shifted
1                 ->  0000 0001     (bit 0)
1 << 1            ->  0000 0010     (bit 1)
1 << 2            ->  0000 0100     (bit 2)
1 << 5            ->  0010 0000     (bit 5)
1 << 7            ->  1000 0000     (bit 7)

~(1 << 5)         ->  1101 1111     (every bit 1, except bit 5)

(1 << 3) | (1 << 1)  ->  0000 1010  (bits 3 AND 1 at once)
Why we do not write the value directly PORTB |= 0b00100000; works, but you have to count positions every time. PORTB |= (1 << PB5); explicitly says which bit, using the name from the datasheet. The compiler produces exactly the same machine code - the shift is computed at compile time, not at run time.
Watch the type of the resultOn AVR, 1 is a 16-bit int. For 8-bit registers it does not matter, but for bit 31 of a 32-bit register (STM32, ESP32) you must write 1UL << 31, otherwise the result is undefined.

7Mask simulator

Choose an operation and a bit position, then press "apply". Watch the generated mask, the corresponding expression, and the bit that changes - highlighted in orange. Notice that the rest of the bits always stay untouched.

Building and applying a mask on a register
Try thisStart from the initial value 0b00001010. Set bit 5, then clear bit 1, then toggle bit 3 twice. Check every time that the uninvolved bits kept their value - that is the entire point of masks.

8The four fundamental idioms

Practically all embedded code in the world uses these four forms. They are worth memorizing as such:

OperationIdiomWhy it works
Set (make it 1)REG |= (1 << n);OR with 1 forces 1; OR with 0 changes nothing
Clear (make it 0)REG &= ~(1 << n);AND with 0 forces 0; AND with 1 changes nothing
ToggleREG ^= (1 << n);XOR with 1 inverts it; XOR with 0 changes nothing
Testif (REG & (1 << n))AND isolates the bit; nonzero result if the bit is 1
portable macros - use these in every lab
#define BIT(n)              (1UL << (n))

#define SET_BIT(reg, n)     ((reg) |=  BIT(n))
#define CLR_BIT(reg, n)     ((reg) &= ~BIT(n))
#define TGL_BIT(reg, n)     ((reg) ^=  BIT(n))
#define CHK_BIT(reg, n)     (((reg) &  BIT(n)) != 0)

// writes a value of 0 or 1 into a bit, with no branch
#define WRT_BIT(reg, n, v)  ((reg) = ((reg) & ~BIT(n)) | ((v) ? BIT(n) : 0))

// usage examples
SET_BIT(DDRB,  PB5);        // pin 13 becomes an output
SET_BIT(PORTB, PB5);        // pin 13 to HIGH
CLR_BIT(PORTB, PB5);        // pin 13 to LOW
TGL_BIT(PORTB, PB5);        // toggle pin 13

if (CHK_BIT(PIND, PD2)) {   // the button is NOT pressed (pull-up)
    // ...
}
The parentheses are not optionalInside a macro, every parameter must be wrapped in parentheses. Without them, SET_BIT(x, a + 1) would expand incorrectly, because of operator precedence. This is a classic source of bugs that are very hard to spot.

9Multi-bit fields

Many settings do not take up a single bit, but a group. For example, a timer's prescaler is given by three bits CS12:CS10, and the ADC channel by four bits MUX3:MUX0.

writing a multi-bit field
// Say bits 3, 4, 5 of port B form a 3-bit "field" (a value from 0 to 7
// sent to an external circuit), while bits 0, 1, 2 control something
// completely different and must NOT be touched.

// WRONG: wipes out the rest of the register
PORTB = 0b00101000;                    // the field is 5... but bits 0-2 are lost!

// CORRECT: clear only the field, then write the value
PORTB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3));   // 1. clear the field
PORTB |=  (1 << PB5);                                 // 2. write the new value

// General form, with mask and shift
#define FIELD_SET(reg, mask, shift, val) \
    ((reg) = ((reg) & ~((mask) << (shift))) | (((val) & (mask)) << (shift)))

// example: write the value 5 into the 3-bit field starting at position 3
FIELD_SET(PORTB, 0x07, 3, 5);

Reading a field

extracting the value
// Isolate the field with AND, then bring it to position 0 by shifting
uint8_t lowerHalf   = (PORTB & 0x0F);          // bits 0-3, already at position 0

// a field that does NOT start at position 0: shift first, mask after
uint8_t threeBitField = (PORTB >> 3) & 0x07;     // bits 3-5

uint8_t topBit       = (PORTB >> 5) & 0x01;     // a single bit, position 5

#define FIELD_GET(reg, mask, shift)  (((reg) >> (shift)) & (mask))
Order mattersWhen reading, shift first and mask afterward. When writing, mask the value first (so it does not overflow into neighboring fields) and only then shift it. Reversing the order produces wrong values, with no compile error at all.

10Read-modify-write

The expression PORTB |= (1 << PB5); looks like a single operation, but the processor executes it in three steps:

1. READ reg → CPU register 2. MODIFY apply OR with the mask 3. WRITE CPU register → reg PORTB |= (1 << PB5); → three separate instructions, not one an interrupt occurring HERE sees an intermediate value, and its own change will be lost
Fig. 1 - The real structure of a read-modify-write operation. The window between steps 2 and 3 is where concurrency bugs occur.
what the compiler generates (AVR)
PORTB |= (1 << PB5);

; translates into:
    in   r24, 0x05      ; 1. READ PORTB into register r24
    ori  r24, 0x20      ; 2. MODIFY: OR with mask 0b00100000
    out  0x05, r24      ; 3. WRITE r24 back into PORTB
A useful exception on AVRFor registers within the first 32 I/O bytes (including PORTB, PORTD, DDRB) and for a constant single-bit mask, the compiler uses the sbi / cbi instructions, which are atomic and take a single cycle. For registers in extended memory (TCCR1B, ADCSRA, UCSR0B) or for masks computed at run time, the full three-instruction sequence is generated.

11Atomicity

An operation is atomic if it cannot be interrupted halfway through. The problem shows up when the same register or the same variable is modified both from the main program and from an interrupt routine.

SituationAtomic?Solution
PORTB |= (1 << PB5); - constant mask, low I/O registeryes (sbi)nothing to do
TCCR1B |= (1 << CS12); - extended registernocritical section, if the ISR touches the same register
Reading a uint16_t modified in an ISRnocritical section required
counter++ on a volatile uint8_tnocritical section
Reading a volatile uint8_tyesjust volatile
critical section - the correct form
// WRONG: re-enables interrupts even if the caller had them disabled
cli();
value = counter;
sei();                      // <-- assumes they were enabled. Not guaranteed!

// CORRECT: save and restore the exact prior state
uint8_t sreg = SREG;         // SREG holds the I bit (Global Interrupt Enable)
cli();
value = counter;             // critical section: nothing can interrupt it
SREG = sreg;                 // restores EXACTLY the previous state

// BEST: macro from the standard AVR library
#include <util/atomic.h>

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    value = counter;         // saving and restoring happen automatically
}
Critical sections must be very shortWhile interrupts are disabled, the system is deaf: edges, bytes received over serial, or timer ticks can be lost. Copy the value inside the section and process it outside of it.
An alternative without disabling interruptsFor a single boolean flag or an 8-bit counter, volatile is enough. For more complex structures you can use the "double read" technique: read the variable twice and accept the result only if the two reads agree.

12Simulator: corrupting an RMW operation

Watch exactly how an interrupt occurring between the read and the write makes its own change vanish without a trace. This is one of the hardest bugs to spot in embedded programming, because it appears rarely and seemingly at random.

What happens when the ISR steps in halfway through
How to avoid itIf a register is touched both from loop() and from an ISR, every RMW operation in the main program must be protected with ATOMIC_BLOCK. Alternatively - and preferably - set the rule that each register is modified from a single place.

13Memory-mapped registers

The name PORTB is not an ordinary variable: it is a macro that points to a physical address in the memory space, wired directly to hardware.

what is hiding behind the name
// In the AVR header files:
#define PORTB   (*(volatile uint8_t *)0x25)
//               │  │                  └── the register's physical address
//               │  └── cast to a pointer to a volatile byte
//               └── dereference: we work with the VALUE at that address

// So PORTB |= 0x20;  means:
*(volatile uint8_t *)0x25 |= 0x20;

// The same thing can be written explicitly:
volatile uint8_t *portb = (volatile uint8_t *)0x25;
*portb |= (1 << 5);
Why volatile is mandatory hereA register's value can change without the program modifying it - for example PIND reflects the physical state of the pins. Without volatile, the compiler would read the register once and reuse the stored value, and the loop while (!(UCSR0A & (1 << RXC0))); would never end.
Why this mattersOnce you understand this mechanism, you can access any peripheral on any microcontroller, even if no library exists for it: look up the address in the datasheet and write there. This is the definition of bare-metal programming.
The PORTD, DDRD and PIND registers with their addresses and bits
Fig. 2 - The three registers of port D, as they appear in the datasheet: the name of each bit, its address in the I/O space, its access rights (R/W or read-only), and its value after reset. Notice that PIND is marked R - read only. Fig. 2.4 in the handout
How to read such a tableThe Bit row gives the position (7 … 0), the next row the name of each bit - exactly the name used in (1 << DDD4). Read/Write says whether you can write to it, and Initial Value the value after reset. You will find this exact format for absolutely every register, in every lab that follows.

14Porting to other controllers

The bitwise idioms stay identical. Only the register names change, and sometimes the peripheral's architecture. Here is the same operation - "turn on an LED" - on four platforms:

the same concept, four microcontrollers
// ---------- AVR / ATmega328P (Arduino Uno) ----------
DDRB  |= (1 << PB5);            // pin as output
PORTB |= (1 << PB5);            // pin to HIGH
PORTB &= ~(1 << PB5);           // pin to LOW

// ---------- STM32 (F1/F4 series, direct registers) ----------
RCC->APB2ENR |= (1 << 3);       // enable port C's clock
GPIOC->CRH   |= (1 << 20);      // configure pin 13 as output
GPIOC->BSRR   = (1 << 13);      // pin to HIGH  (dedicated register, ATOMIC)
GPIOC->BSRR   = (1 << 29);      // pin to LOW   (bit 13 + 16)

// ---------- ESP32 ----------
GPIO.enable_w1ts   = (1 << 2);  // pin as output
GPIO.out_w1ts      = (1 << 2);  // pin to HIGH  (write-1-to-set, ATOMIC)
GPIO.out_w1tc      = (1 << 2);  // pin to LOW   (write-1-to-clear)

// ---------- PIC (16F series) ----------
TRISB &= ~(1 << 5);             // pin as output (0 = output on PIC!)
LATB  |=  (1 << 5);             // pin to HIGH
LATB  &= ~(1 << 5);             // pin to LOW
AspectAVRSTM32ESP32PIC
Register width8 bits32 bits32 bits8 bits
Pin directionDDRx, 1 = outputMODER / CRLenableTRISx, 0 = output
Writing a levelPORTxODR or BSRRoutLATx
Reading a pinPINxIDRinPORTx
Atomic setsbi (automatic)BSRRw1ts/w1tc-
Peripheral clockalways onmust be enabled (RCC)managed by the SDKalways on
The most important observationModern controllers offer dedicated registers for setting and clearing (BSRR on STM32, w1ts/w1tc on ESP32). Writing to them is a single operation, so it is atomic by construction - the problem from section 11 disappears completely. The existence of these registers is proof that the RMW problem is real and important enough for manufacturers to add special hardware for it.
Porting trapOn PIC, TRISx uses the opposite convention from AVR: 0 means output, 1 means input. Code ported mechanically works exactly backward. Always check the convention in the datasheet, never assume it.

15Source code

Binary display of a value

binary_display.ino
// Connect 6 LEDs to pins 8...13, that is, to bits 0...5 of port B.
// The LED bar becomes the "binary display" of any 6-bit value.

void show(uint8_t v) {
  PORTB = v & 0x3F;     // bits 0-5 of the value reach the 6 LEDs
  _delay_ms(1500);      // pause, so you have time to read the bar
}

int main(void) {
  DDRB = 0x3F;                    // all 6 pins, outputs

  uint8_t reg = 0b00001010;
  show(reg);                      // initial: 001010

  reg |=  (1 << 5);   show(reg);  // set b5: 101010
  reg &= ~(1 << 1);   show(reg);  // clr b1: 101000
  reg ^=  (1 << 3);   show(reg);  // tgl b3: 100000

  // TESTING a bit: the LED on pin 7 shows the answer
  DDRD |= (1 << PD7);
  if (reg & (1 << 5)) PORTD |=  (1 << PD7);   // bit 5 is SET
  else                PORTD &= ~(1 << PD7);   // bit 5 is clear

  while (1) { }
}

Manipulation exercises

bit_exercises.ino
// 1. Counting the set bits (Kernighan's algorithm)
uint8_t countBits(uint8_t v) {
  uint8_t n = 0;
  while (v) {
    v &= (v - 1);            // clear the rightmost set bit
    n++;
  }
  return n;
}

// 2. Testing whether a value is a power of 2
bool isPowerOf2(uint8_t v) {
  return v && !(v & (v - 1));
}

// 3. Reversing the order of the bits
uint8_t reverseBits(uint8_t v) {
  uint8_t r = 0;
  for (uint8_t i = 0; i < 8; i++) {
    r = (r << 1) | (v & 1);
    v >>= 1;
  }
  return r;
}

// 4. Swapping two values with no temporary variable
void swap(uint8_t *a, uint8_t *b) {
  *a ^= *b;  *b ^= *a;  *a ^= *b;
}

// 5. Extracting the upper and lower nibble
uint8_t nibbleHigh(uint8_t v)  { return (v >> 4) & 0x0F; }
uint8_t nibbleLow(uint8_t v)   { return  v       & 0x0F; }

The same application, three levels of abstraction

three_levels.ino
// ============ LEVEL 1: the Arduino library ============
// advantage: readable right away   drawback: slow, not portable
void blinkArduino() {
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

// ============ LEVEL 2: AVR registers, with names ============
// advantage: fast, explicit   drawback: tied to AVR
void blinkRegisters() {
  DDRB  |=  (1 << PB5);
  PORTB |=  (1 << PB5);
  _delay_ms(500);
  PORTB &= ~(1 << PB5);
  _delay_ms(500);
}

// ============ LEVEL 3: your own abstraction layer ============
// advantage: fast AND portable - only this block changes when porting
#if defined(__AVR__)
  #define LED_DIR_REG   DDRB
  #define LED_OUT_REG   PORTB
  #define LED_PIN_NR    PB5
#elif defined(ESP32)
  #define LED_DIR_REG   GPIO.enable
  #define LED_OUT_REG   GPIO.out
  #define LED_PIN_NR    2
#endif

#define LED_INIT()    (LED_DIR_REG |=  (1UL << LED_PIN_NR))
#define LED_ON()      (LED_OUT_REG |=  (1UL << LED_PIN_NR))
#define LED_OFF()     (LED_OUT_REG &= ~(1UL << LED_PIN_NR))
#define LED_TOGGLE()  (LED_OUT_REG ^=  (1UL << LED_PIN_NR))

void blinkPortable() {
  LED_INIT();
  LED_ON();     _delay_ms(500);
  LED_OFF();    _delay_ms(500);
}
Level 3 is the goalThis is the pattern used in industry: the application logic calls LED_ON(), and porting to another microcontroller means rewriting a single block of definitions. You will use this structure in the final project.

Measuring the performance difference

bit_benchmark.ino
// We do not have the serial monitor yet (lab 6), so we do not print the
// duration: we make it VISIBLE. Each method keeps a marker LED lit for
// as long as its loop runs. The faster the method, the shorter the
// marker blinks. On an oscilloscope, the pulse width IS the measured duration.

#define N 20000

int main(void) {
  DDRB |= (1 << PB0);                             // the pin being toggled
  DDRD |= (1 << PD5) | (1 << PD6) | (1 << PD7);   // the three markers

  uint16_t i;
  while (1) {
    PORTD |= (1 << PD7);                          // function-call variant
    for (i = 0; i < N; i++) { digitalWrite(8, HIGH); digitalWrite(8, LOW); }
    PORTD &= ~(1 << PD7);
    _delay_ms(300);

    PORTD |= (1 << PD6);                          // register variant
    for (i = 0; i < N; i++) { PORTB |= (1 << PB0); PORTB &= ~(1 << PB0); }
    PORTD &= ~(1 << PD6);
    _delay_ms(300);

    PORTD |= (1 << PD5);                          // toggling by writing to PINB
    for (i = 0; i < N; i++) { PINB = (1 << PB0); PINB = (1 << PB0); }
    PORTD &= ~(1 << PD5);
    _delay_ms(300);
  }
}
The PINB trickOn AVR, writing a 1 into the PINx register toggles the corresponding bit of PORTx. This is the fastest way to toggle a pin - a single, atomic instruction. A detail found only in the datasheet, never in an Arduino tutorial.

16Interactive circuit

Here you see, at the same time, the three faces of the same thing: the C instruction you write, the bit that changes in the register, and the LED that lights up. The register panel is also interactive - click directly on a bit to toggle it manually and watch what happens to the circuit.

The four fundamental idioms, seen on a bar of LEDs
#include <avr/io.h>
#include <util/delay.h>

/* Pins 8...13 are bits 0...5 of port B.
   Pin 7 (PD7) is a marker LED for the TEST idiom. */

int main(void)
{
    DDRB = 0x3F;               // all 6 pins become outputs
    DDRD |= (1 << PD7);        // the marker pin, output

    while (1) {
        PORTB = 0x00;                  // start from 0b000000
        _delay_ms(600);

        PORTB |= (1 << PB0);           // SET bit 0     -> turns on LED 0
        _delay_ms(600);

        PORTB |= (1 << PB2) | (1 << PB4);   // two SETs at once
        _delay_ms(600);

        PORTB &= ~(1 << PB2);          // CLEAR bit 2   -> turns off LED 2
        _delay_ms(600);

        PORTB ^= (1 << PB4);           // TOGGLE bit 4  -> it flips
        _delay_ms(600);

        if (PORTB & (1 << PB0))        // TEST bit 0
            PORTD |=  (1 << PD7);      // true -> the marker lights up
        else
            PORTD &= ~(1 << PD7);
        _delay_ms(900);
    }
}
Try thisBelow the bar you are shown, at all times, the binary and decimal value of the port: watch how each idiom changes exactly one bit. Pause execution and click on the bits of PORTB: the LEDs turn on and off immediately. Then click on the bits of DDRB - if a bit of DDRB is 0, the pin becomes an input and the LED no longer lights up, no matter how much you try with PORTB. That is the entire difference between the two registers.
Exercise - the sweeping light ("Knight Rider")
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB = 0x3F;
    unsigned char position = 0;

    while (1) {
        /* Light up A SINGLE LED, the one at the current position.
           Hint: PORTB = (1 << position);
           Then move the position forward and, when it reaches 5,
           start over. Step of 120 ms.

           Homework: make it bounce back, instead of jumping back. */

        _delay_ms(120);
    }
}
Try thisA single lit LED means a single bit at 1 in PORTB. Shifting left with << moves that bit - which is why the effect comes from a single line.

17Work tasks

  • Convert by hand, with no calculator: 0b11010110 to hex and decimal; 0x3F to binary.
  • Implement the binary display function and check it for 0, 1, 128 and 255.
  • Write the four macros (SET, CLR, TGL, CHK) and use them to drive an LED.
  • Set bits 1, 3 and 5 of a variable at the same time, with a single expression.
  • Clear a 3-bit field and write the value 5 into it, without affecting the rest of the register.
  • Extract the upper and lower nibble of a byte and display them separately.
  • Implement counting the set bits and check the result for 0b10110110.
  • Run the benchmark and note the ratio between the three methods of toggling a pin.
  • Write the same blink program at the three levels of abstraction and compare the size of the compiled code.
  • Look up the address of the PORTD register in the ATmega328P datasheet and access it through a pointer, without using its name.

18Extended application

Your own pin-access libraryWrite a small abstraction layer that offers the functions pinInit(port, bit, dir), pinWrite(port, bit, val), pinRead(port, bit) and pinToggle(port, bit), using exclusively bitwise operations and pointers to registers.

Requirements: it must work on all three ports (B, C, D), be at least 10 times faster than digitalWrite(), and use no function from the Arduino library. Compare the size of the compiled code with the variant that uses the standard library.

Extension: add a pinToggleAtomic() variant that uses the PINx trick, and demonstrate, with a logic analyzer or a second pin, that it is atomic.

19Review questions

20Resources