On extracting opcode and nibbles

Let's say you've successfully read a Chip8 ROM and ended up with the following data:

let data: Vec<u16> = vec![0x12, 0x02];

Extracting the opcode§

To better understand what we're going to do, we need to see the binary data:

data[0]data[1]
hex0x120x02
binary0b0000_0000_0001_00100b0000_0000_0000_0010

As I understand it, the aim of opcode extraction is to merge two 8-bit instructions into one 16-bit instruction (at least for Chip8 instructions). Here's how:

let i: usize = 0;
let opcode: u16 = (data[i] << 8) | data[i + 1];

Here's a detailed and commented code of each step above to visualize what's going on, but essentially, we shift data[i] 8 bits to the left with the << operator and then merge the resulting value with data[i + 1] using the | operator.

let i: usize = 0;

// the << operator moves each bit 8 positions to the left
// data[i] equals 0b0000_0000_0001_0010
let shift: u16 = data[i] << 8; // shift equals 0b0001_0010_0000_0000
// before shift: 0b0000_0000_0001_0010
//               ---------------------
// after shift : 0b0001_0010_0000_0000

// the | operator returns 1 if at least one of the values in the expression is 1
// data[i + 1] equals 0b0000_0000_0000_0010
let opcode: u16 = shift | data[i + 1]; // opcode equals 0b0001_0010_0000_0010
// shift      : 0b0001_0010_0000_0000
//              ---------------------
// data[i + 1]: 0b0000_0000_0000_0010
//              ---------------------
// opcode     : 0b0001_0010_0000_0010

Extracting nibbles§

Extracting the nibbles means separating the opcode into 4 half-byte (4 bits).

let nibbles: (u16, u16, u16, u16) = (
    (opcode & 0xF000) >> 12,
    (opcode & 0x0F00) >> 8,
    (opcode & 0x00F0) >> 4,
    (opcode & 0x000F),
);

Here's a detailed and commented code of the first step (the rest can be extrapolated from this step). Essentially, first we extract the 4 bits we're interested in with the & operator and then shift to the right the number of bits (for this step, 12 bits) that separate us from the 4 rightmost bits.

// opcode binaire actuel: 0b0001_0010_0000_0010
let opcode: u16 = 0x1202;

// the & operator returns 1 if and only if both values of the expression are 1
let and_mask: u16 = opcode & 0xF000; // and_mask equals 0b0001_0000_0000_0000

// the >> operator moves each bit of P position to the left (here 12)
let nibble: u16 = and_mask >> 12; // nibble equals 0b0000_0000_0000_0001
//                           |  |
//                           ---- usually a multiple of 4
// opcode     : 0b0001_0010_0000_0010
//              ---------------------
// and_mask   : 0b0001_0000_0000_0000
//              ---------------------
// nibble     : 0b0000_0000_0000_0001

This is an important part of writing an emulator, at least for simple emulators like the Chip8.