Home / Knowledge Hub / Embedded C Cheat Sheet

Embedded C Cheat Sheet

Large data sheets make it easy to forget the fundamentals of C programming. Enjoy KasperAero's single-page quick reference for embedded C development on the ATtiny1616 and related AVR tinyAVR 0/1/2-series microcontrollers.

It covers essential data types, preprocessor rules, GPIO port and pin control, bitwise operations, TWI/I²C master communication sequences, TMAG3001 Hall-effect sensor register map and mT conversion, common rookie mistakes, and MPLAB X live debug tips.

Embedded C Cheat Sheet

AVR tinyAVR 0/1/2-Series  ·  ATtiny1616  ·  MPLAB X  ·  XC8 Compiler

Types & Essential Includes
TypeMeaning
uint8_tUnsigned 8-bit  (0 – 255)
int8_tSigned 8-bit  (–128 to 127)
uint16_tUnsigned 16-bit  (0 – 65535)
int16_tSigned 16-bit  (–32768 to 32767)
uint32_tUnsigned 32-bit  (0 – 4 billion)
booltrue / false — needs <stdbool.h>
float32-bit floating point
voidNo return type / no arguments
staticVariable persists between function calls
volatileTells compiler: value may change externally
constValue cannot be changed after initialization
// Required headers — order matters for F_CPU
#define F_CPU 20000000UL // MUST come before delay.h

#include <avr/io.h> // Port/register names
#include <util/delay.h> // _delay_ms(), _delay_us()
#include <stdint.h> // uint8_t, int16_t, etc.
#include <stdbool.h> // bool, true, false
Preprocessor — Text Substitution, Not Code

Runs before compilation. No memory is allocated, no instructions generated. Every occurrence is replaced with the literal value.

#define F_CPU 20000000UL // 20 MHz clock
#define LED_PIN PIN3_bm // bit mask for pin 3
#define THRESH 1000 // magic number → named constant

// Macros with arguments
#define ABS(x) ((x) < 0 ? -(x) : (x))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
GPIO — Port A, B, C Pin Control
Register / MacroAction
PORTx.DIRSET = maskSet pin(s) as OUTPUT
PORTx.DIRCLR = maskSet pin(s) as INPUT
PORTx.OUTSET = maskDrive pin(s) HIGH
PORTx.OUTCLR = maskDrive pin(s) LOW
PORTx.OUTTGL = maskToggle pin(s)
PORTx.INRead actual pin state (use for inputs)
PORTx.PINnCTRLPer-pin config: pull-up, invert, etc.
PORT_PULLUPEN_bmEnable internal pull-up resistor
PIN naming — critical concept

PIN0_bm is just the number 0x01. It carries no port information. The PORT register you write to is what selects the physical pin.

// Same mask, three completely different pins:
PORTB.OUTSET = PIN0_bm; // → PB0
PORTA.OUTSET = PIN0_bm; // → PA0 (different!)
PORTC.OUTSET = PIN0_bm; // → PC0 (different again)

// Best practice: name both port AND pin together
#define LED_PORT PORTB
#define LED_PIN PIN0_bm
LED_PORT.OUTSET = LED_PIN; // unambiguous — always PB0

// Enable pull-up on an input pin
PORTA.PIN1CTRL |= PORT_PULLUPEN_bm;
Bitwise Operations
OpPurpose
&AND — mask / read specific bits
|OR — set bits without touching others
^XOR — toggle specific bits
~NOT — invert all bits
<<Shift left (multiply by 2 per shift)
>>Shift right (divide by 2 per shift)
&=AND-assign — clear bits safely
|=OR-assign — set bits safely
^=XOR-assign — toggle bits safely
// Set bit 4
REG |= (1 << 4);

// Clear bit 4
REG &= ~(1 << 4);

// Toggle bit 4
REG ^= (1 << 4);

// Test if bit 4 is set
if (REG & (1 << 4)) { ... }

// Chained error check — ok stays true only if ALL calls succeed
bool ok = true;
ok &= write_reg(addr, REG1, 0x40);
ok &= write_reg(addr, REG2, 0x02);
if (!ok) { error_blink(); }
TWI0 (I²C Master) — ATtiny1616
RegisterPurpose
TWI0.MBAUDSet clock speed. 95 → ~100 kHz @ 20 MHz
TWI0.MCTRLAEnable master: TWI_ENABLE_bm
TWI0.MSTATUSStatus flags — check after every byte
TWI0.MADDRWrite device_addr << 1 | 0(W) or 1(R)
TWI0.MDATAWrite data byte OR read received byte
TWI0.MCTRLBSend ACK / NACK / STOP commands
Status flags — check after every byte
TWI_WIF_bmWrite complete — byte was sent
TWI_RIF_bmRead complete — byte received
TWI_RXACK_bmDevice NACKed (1 = NACK = bad)
TWI_ARBLOST_bmArbitration lost
TWI_BUSERR_bmBus error — invalid START/STOP
TWI_BUSSTATE_IDLE_gcForce bus idle after init (errata fix)
BAUD formula
// MBAUD = (F_CPU / F_SCL − 10) / 2
// For 100 kHz @ 20 MHz: (200 − 10) / 2 = 95

TWI0.MBAUD = 95;
TWI0.MCTRLA = TWI_ENABLE_bm;
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc; // errata workaround
Write one register
// START → addr+W → reg → data → STOP

TWI0.MADDR = (dev_addr << 1) | 0x00;
while (!(TWI0.MSTATUS & TWI_WIF_bm));
if (TWI0.MSTATUS & TWI_RXACK_bm) return false; // NACK

TWI0.MDATA = reg_addr;
while (!(TWI0.MSTATUS & TWI_WIF_bm));

TWI0.MDATA = data;
while (!(TWI0.MSTATUS & TWI_WIF_bm));

TWI0.MCTRLB = TWI_MCMD_STOP_gc;
Read registers (repeated START)
// Phase 1: set register pointer (write phase)
TWI0.MADDR = (dev_addr << 1) | 0x00;
TWI0.MDATA = reg_addr;
while (!(TWI0.MSTATUS & TWI_WIF_bm));

// Phase 2: repeated START → switch to read
TWI0.MADDR = (dev_addr << 1) | 0x01;
while (!(TWI0.MSTATUS & TWI_RIF_bm));
data[i] = TWI0.MDATA;

// ACK middle bytes, NACK+STOP on last byte
TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc; // ACK
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc; // NACK+STOP
TMAG3001 Hall Sensor Quick Reference
I²C addresses — set by ADDR pin on PCB
ADDR pin7-bit AddrWrite byteRead byte
GND0x340x680x69
VCC0x350x6A0x6B
SDA0x360x6C0x6D
SCL0x370x6E0x6F
Key result registers (Table 7-1)
0x12 / 0x13X axis MSB / LSB
0x14 / 0x15Y axis MSB / LSB
0x16 / 0x17Z axis MSB / LSB
0x10 / 0x11Temperature MSB / LSB
0x18Conv_Status — conversion complete flag
SENSOR_CONFIG_1 (0x02) — MAG_CH_EN bits [7:4]
0x00All channels OFF (default)
0x10X only
0x20Y only
0x40Z only
0x70X, Y, Z all enabled
Convert raw Z reading to milliTesla
// 16-bit signed 2's complement, full scale = 32767
// Change 40.0f to match sensor variant:
// TMAG3001A1: 40 or 80 mT
// TMAG3001A2: 120 or 240 mT

float z_mT = ((float)z_raw / 32767.0f) * 40.0f;
MPLAB X — Monitoring Live Data
Where to lookWhat you get
Window → Debugging → VariablesAuto-shows all locals. Right-click → Decimal
Window → Debugging → WatchesAdd any variable or custom expression
Watch expression(float)z1 / 32767.0 * 40.0 → live mT
Tools → Data VisualizerScrolling live graph via UART output
Debug → Pause / F7 / F8Pause · Step Into · Step Over
PICkit 5 power: Project Properties → PICkit 5 → Power target from PICkit 5. Max 150 mA. Set voltage to 3.3V for TMAG3001 (max VCC = 3.6V — 5V will damage it).
Common Gotchas ⚠
MistakeWhy It Hurts
F_CPU mismatchdelay() wrong timing if F_CPU ≠ actual clock
Wrong PORT for PIN maskPIN0_bm on PORTA ≠ PIN0_bm on PORTB
Read OUT instead of INOUT shows what you drove, not actual pin level
No pull-up on inputFloating pin → random reads, unpredictable behavior
Ignoring TWI NACKSilently continues; reads garbage data
VCC > 3.6V on TMAG3001Will permanently damage the sensor
UPDI not disabled after flashOscillator stays on; power consumption wrong
Non-atomic register writesUse |= and &= not = on shared registers
delay() inside ISRNever call _delay_ms inside an interrupt handler
Signed vs unsigned compareint8_t vs uint8_t comparison gives wrong result