Loading header...

Hand-Assembly — Turning Mnemonics Into Machine Code With Just a Pencil

Last lesson handed you the dictionary. This one hands you a pencil and a real program, and asks you to do what an assembler does in microseconds — except you'll do it slowly enough to feel every bit fall into place. There's a particular, almost puzzle-like satisfaction in watching a line of assembly collapse into four hex digits you derived yourself, with nothing but a bit-field table and arithmetic. Let's do it.


Learning Objectives

By the end of this lesson, you should be able to:

  • translate a small assembly listing into 16-bit instruction words using opcode fields;
  • distinguish a word value such as 0xE005 from the little-endian bytes stored in flash;
  • compute an AVR relative branch offset from target = PC + 1 + k;
  • spot common hand-assembly mistakes before they become silent machine-code bugs.

The Program We're Hand-Assembling

A tiny AVR routine: load a constant into R16, load another constant into R17, add them together into R16, then jump back to the start.

        LDI   R16, 0x05      ; R16 = 5
        LDI   R17, 0x03      ; R17 = 3
        ADD   R16, R17       ; R16 = R16 + R17
        RJMP  start          ; loop forever

Four instructions. Four opcode-table lookups. Let's go one at a time.


Step 1 — LDI R16, 0x05

Recall the LDI field layout from lesson 10:

1110 KKKK dddd KKKK
Field Bits Value needed
Opcode 15–12 1110 (fixed)
K (upper nibble) 11–8 upper 4 bits of 0x05
d 7–4 register encoding for R16
K (lower nibble) 3–0 lower 4 bits of 0x05

Register field for LDI is 4 bits wide and covers only R16–R31, using the mapping register − 16. R16 → 0000.

Immediate split: 0x05 = 0000 0101 in binary. Upper nibble = 0000, lower nibble = 0101.

Assemble the word:

1110   0000   0000   0101
opcode  K(hi)  d=R16  K(lo)

= 1110000000000101 = 0xE005

Mnemonic Binary Hex
LDI R16, 0x05 1110 0000 0000 0101 0xE0 0x05

(AVR stores instructions little-endian in memory as two bytes — low byte first — so in program memory you'd see 0x05 then 0xE0. We'll keep showing the word as a 16-bit value for clarity, and note byte order at the end.)


Step 2 — LDI R17, 0x03

Same field layout, new register and new immediate.

Register field: R17 → 17 − 16 = 10001.

Immediate split: 0x03 = 0000 0011. Upper nibble 0000, lower nibble 0011.

1110   0000   0001   0011
opcode  K(hi)  d=R17  K(lo)

= 1110000000010011 = 0xE013

Mnemonic Binary Hex
LDI R17, 0x03 1110 0000 0001 0011 0xE0 0x13

Step 3 — ADD R16, R17

Recall:

0000 11rd dddd rrrr
Field Bits Value needed
Opcode 15–10 000011 (fixed)
r (MSB of Rr) 9 MSB of register 17's 5-bit number
d (Rd, full 5 bits) 8–4 register number 16, in 5 bits
r (low 4 bits of Rr) 3–0 low 4 bits of register number 17

This time the full 5-bit register numbers are used (not the −16 offset trick — that's only for LDI). R16 = 10000 in 5 bits. R17 = 10001 in 5 bits.

Split Rr = R17 = 10001: MSB = 1, low 4 bits = 0001.
Rd = R16 = 10000 goes in whole, as the 5-bit ddddd field.

000011   1   10000   0001
opcode   r   d=R16   r(low4)=R17

= 0000111100000001 = 0x0F01

Mnemonic Binary Hex
ADD R16, R17 0000 1111 0000 0001 0x0F 0x01

This is the trickiest one precisely because the register field is split across non-adjacent bit positions — exactly the kind of thing that's invisible when you just call ADD in an assembler, and exactly the kind of thing this exercise is designed to make visible.


Step 4 — RJMP start

start is the address of the first instruction (LDI R16, 0x05), which lives at word address 0x0000. This RJMP is the 4th instruction, at word address 0x0003 (each instruction here is one word). The offset field k in RJMP is relative to PC + 1 (the address after the RJMP itself):

target = current_RJMP_address + 1 + k
0x0000 = 0x0003 + 1 + k
k = 0x0000 − 0x0004 = −4

In 12-bit two's complement, −4 = 1111 1111 1100.

1100   k k k k k k k k k k k k
opcode  = 111111111100

= 1100111111111100 = 0xCFFC

Mnemonic Binary Hex
RJMP start (k = −4) 1100 1111 1111 1100 0xCF 0xFC

The Finished Machine-Code Listing

Address Mnemonic Binary (16-bit word) Hex word Bytes in memory (LE)
0x0000 LDI R16, 0x05 1110 0000 0000 0101 0xE005 05 E0
0x0001 LDI R17, 0x03 1110 0000 0001 0011 0xE013 13 E0
0x0002 ADD R16, R17 0000 1111 0000 0001 0x0F01 01 0F
0x0003 RJMP start 1100 1111 1111 1100 0xCFFC FC CF

That right-most column — eight raw bytes, 05 E0 13 E0 01 0F FC CF — is exactly what would sit in the AVR's flash memory. There is nothing else to it. No symbol table, no parser, no magic: just bit-fields, filled in by hand, from a table you could photocopy and tape to a wall.

flowchart LR classDef cpu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef mem fill:#dcfce7,stroke:#16a34a,color:#14532d classDef bus fill:#f3e8ff,stroke:#9333ea,color:#581c87 A["Mnemonic + operands"]:::bus --> B["Opcode table\n(lesson 10)"]:::cpu --> C["Fill in bit-fields\nby hand"]:::cpu --> D["16-bit word →\ntwo bytes, little-endian"]:::mem

Worked Check: Recompute the Jump Offset

The RJMP field is where most hand-assembly errors hide, so always check it twice:

Quantity Value
Target word address 0x0000
RJMP word address 0x0003
Address after RJMP, PC + 1 0x0004
Signed displacement, k 0x0000 - 0x0004 = -4
12-bit two's complement 0xFFC
Final word 0xC000 OR 0x0FFC = 0xCFFC

If you accidentally use the current PC instead of PC + 1, you get k = -3 and the loop lands on the second instruction. If you count byte addresses instead of word addresses, the error is even larger. AVR relative branches are measured in instruction words, not bytes.


Common Mistakes

Mistake Symptom How to catch it
Using R17 directly in the 4-bit LDI register field Wrong destination register Remember LDI encodes d = register - 16 for R16-R31
Forgetting split fields Good opcode, wrong operands Mark every K, d, and r bit before converting to hex
Swapping word value and memory bytes Hex file appears reversed Show both 0xE005 and stored bytes 05 E0
Branching from current PC instead of PC + 1 Loop lands one instruction late Recalculate with target = current + 1 + k
Treating bytes as word addresses Jump distance doubled AVR program counter counts instruction words for these 16-bit opcodes

Why This Exercise Matters

Every assembler ever written — from the one bundled with avr-gcc to the cross-assemblers used on 1970s minicomputers — does exactly the arithmetic you just did, just faster and without typos. Once you've manually placed a register number into a split bit-field and manually computed a signed branch offset, "the assembler computed the relative jump offset" stops being a magic phrase and becomes a concrete, checkable fact. And that hard-earned feeling — of being the one doing the work a machine usually does for you — is about to get a name: this is precisely what programmers did, instruction by instruction, using nothing but a hex keypad.

Key Takeaway

Hand-assembly is opcode tables applied with discipline: pick the fixed bits, slot in operand fields, and watch a line of assembly collapse into bytes you can verify by hand. There is no hidden step between mnemonic and machine code — only fields, positions, and arithmetic you now own. Next, we go back in time to where this hex-byte listing was the only interface between a programmer and a running computer.

Further Reading

  • Microchip, AVR Instruction Set Manual, especially LDI, ADD, and RJMP opcode formats.
  • Microchip, ATmega328P Datasheet, program memory and instruction execution sections.
  • GNU Binutils documentation for avr-as and avr-objdump, useful for comparing hand-assembled bytes against assembler output.

Mind Map

mindmap root((Hand Assembly)) Core idea Mnemonic to fields Fields to word Word to bytes Opcode fields Fixed opcode bits Register bits Immediate bits Split operands Key formulas LDI d equals R minus 16 RJMP target equals PC plus 1 plus k Negative k uses twos complement AVR flash stores low byte first Practical checks Mark every bit Recompute branch target Verify endian column Compare with objdump Applications Debug disassembly Understand assemblers Trainer kit entry Bootloader bring-up Common mistakes Wrong PC base Byte address confusion Reversed byte order Missed split field

The 8085 Trainer Kit