ADD I, Vx — Advancing the Pointer
LD I points the index register at an address. But sometimes you need to move that pointer forward — to step through a sequence of sprites, or to store registers at consecutive locations. That is what FX1E does: it adds a register value to I, advancing the pointer by however many bytes you specify.
Encoding
The 0xF group contains several unrelated opcodes that share only their first nibble. They are distinguished by the lower byte:
FX1E
│││└─ E ┐
││└── 1 ┘ 0x1E identifies this specific opcode within the F group
│└─── X: source register
└──── opcode category F
Decode and execute
Because the 0xF group has multiple opcodes, the implementation uses a nested match on the lower byte:
#![allow(unused)] fn main() { 0xF => { let x = ((opcode & 0x0F00) >> 8) as usize; match opcode & 0x00FF { 0x1E => { self.i += self.v[x] as u16; } 0x29 => { /* LD F, Vx — set I to font sprite address */ } // other F opcodes... _ => println!("unknown: {:#06X}", opcode), } } }
self.v[x] is a u8; self.i is a u16. Rust requires an explicit cast before the addition — the types must match.
Why this is useful
Think of I as a cursor in memory. LD I places the cursor. ADD I, Vx moves it forward. Together they let you traverse memory one step at a time without hardcoding addresses.
A typical sequence might look like this:
LD I, 0x300 ; place cursor at the start of a sprite table
ADD I, V2 ; advance by the sprite index (V2 bytes)
DRW V0, V1, 5 ; draw the sprite now at the cursor position
The same pair of opcodes appears when storing or loading a block of registers with FX55 and FX65 — first position I, then optionally advance it to the next block.
Rust concepts used here: The nested match on the lower byte is the same pattern used in the
0x8group — see Pattern Matching. Theas u16cast is Type Casting. See LD I, addr for how I is initially set, and DRW — Drawing Sprites for the most common reason to adjust it.