Address Encoding Explained#

This document explains how addresses and binary numbers work in hardware systems. Understanding address encoding is fundamental to working with memory, packets, and hardware interfaces.


Binary Address Encoding#

Addresses in hardware are binary numbers. Each bit position represents a power of 2:

Binary:    0b101010
Positions:   543210  (bit positions, numbered right to left)

Value = (1 × 2^5) + (0 × 2^4) + (1 × 2^3) + (0 × 2^2) + (1 × 2^1) + (0 × 2^0)
      = 32 + 0 + 8 + 0 + 2 + 0
      = 42 (decimal)

Key concepts:

  • Rightmost bit (bit 0) is the Least Significant Bit (LSB) - contributes 2^0 = 1

  • Leftmost bit is the Most Significant Bit (MSB) - contributes the largest power of 2

  • Each additional bit doubles the range: n bits can represent 0 to (2^n - 1)


Hexadecimal Notation#

Hexadecimal (base 16) is commonly used in hardware because each hex digit represents exactly 4 binary bits:

Binary    Hex    Decimal
0000      0      0
0001      1      1
0010      2      2
0011      3      3
0100      4      4
0101      5      5
0110      6      6
0111      7      7
1000      8      8
1001      9      9
1010      A      10
1011      B      11
1100      C      12
1101      D      13
1110      E      14
1111      F      15

Example: Converting between representations

Binary:       0b11010110
Group by 4:   1101  0110
Hexadecimal:    D      6   = 0xD6
Decimal:      (13×16) + 6  = 214

Why use hexadecimal?

  • Compact: 32-bit number = 8 hex digits vs 32 binary digits

  • Easy conversion: Each hex digit ↔ 4 bits (no complex math)

  • Standard in hardware: Datasheets, debuggers, memory dumps all use hex


Memory Address Format#

32-bit address example: 0x12345678#

Hex:    1    2    3    4    5    6    7    8
Binary: 0001 0010 0011 0100 0101 0110 0111 1000

This represents byte address 305,419,896 in decimal.

Breakdown:
  0x10000000 = 268,435,456
  0x02000000 =  33,554,432
  0x00300000 =   3,145,728
  0x00040000 =     262,144
  0x00005000 =      20,480
  0x00000600 =       1,536
  0x00000070 =         112
  0x00000008 =           8
  ──────────   ───────────
  0x12345678   305,419,896

64-bit address example: 0x0000000123456000#

Used in: PCIe Memory Read TLPs, host system physical addresses

Upper 32 bits: 0x00000001
Lower 32 bits: 0x23456000

Decimal: 4,883,857,408 (over 4 GB)

Byte Addressing#

Why byte addresses?

  • Most systems are byte-addressable: Each address points to 1 byte (8 bits)

  • Even if you read/write larger chunks (32-bit words, 512-bit packets), addresses still refer to bytes

  • Consecutive bytes have consecutive addresses

Example: Address sequence

Address     Content (1 byte each)
─────────   ────────────────────
0x1000      0xAA
0x1001      0xBB
0x1002      0xCC
0x1003      0xDD
0x1004      0xEE
...

Reading multi-byte values:

32-bit word at address 0x1000:
  - Byte 0 (0x1000): 0xAA
  - Byte 1 (0x1001): 0xBB
  - Byte 2 (0x1002): 0xCC
  - Byte 3 (0x1003): 0xDD

Little-endian system: 0xDDCCBBAA (least significant byte first)
Big-endian system:    0xAABBCCDD (most significant byte first)

Address Alignment#

Many systems require addresses to be aligned to the size of the data being accessed.

Alignment rules:

  • Byte (8-bit): Any address (0x1000, 0x1001, 0x1002…)

  • 16-bit (2-byte): Address must be divisible by 2 (0x1000, 0x1002, 0x1004…)

  • 32-bit (4-byte) word: Address must be divisible by 4 (0x1000, 0x1004, 0x1008…)

  • 64-bit (8-byte): Address must be divisible by 8 (0x1000, 0x1008, 0x1010…)

  • 512-bit (64-byte) packet: Address must be divisible by 64 (0x1000, 0x1040, 0x1080…)

How to check alignment:

addr = 0x12345678

# Check if 4-byte aligned (for 32-bit words)
is_4byte_aligned = (addr % 4) == 0
# Equivalent: Check if lower 2 bits are zero
is_4byte_aligned = (addr & 0x3) == 0

# Check if 64-byte aligned (for 512-bit packets)
is_64byte_aligned = (addr % 64) == 0
# Equivalent: Check if lower 6 bits are zero
is_64byte_aligned = (addr & 0x3F) == 0

Why alignment matters:

  • Performance: Aligned accesses are faster (single memory transaction)

  • Requirements: Some hardware can ONLY access aligned addresses

  • Atomicity: Aligned accesses are often atomic (all-or-nothing)

Example: Aligned vs unaligned

32-bit word read at address 0x1000 (aligned):
  Single memory access: Read bytes [0x1000-0x1003]

32-bit word read at address 0x1001 (unaligned):
  Two memory accesses needed:
    Read bytes [0x1000-0x1003]
    Read bytes [0x1004-0x1007]
  Extract and combine the middle bytes → slower!

Bit Field Notation#

When documenting packet formats and registers, we use [MSB:LSB] notation to specify bit ranges.

Notation Format#

[31:0]    means bits 31 down to 0 (32 bits total, all bits of a 32-bit value)
[511:504] means bits 511 down to 504 (8 bits = 1 byte)
[15]      means bit 15 only (single bit)
[7:0]     means bits 7 down to 0 (1 byte, lower 8 bits)

Why MSB:LSB order?

  • MSB (Most Significant Bit) comes first: Bit 511 has the highest value (2^511)

  • LSB (Least Significant Bit) comes last: Bit 0 has the lowest value (2^0)

  • Reading left-to-right gives you the range in decreasing significance

Example: 512-bit Command Packet#

┌─────────────────────────────────────────────────────────────┐
│                   512-bit Command Packet                    │
├───────────────┬─────────────────────────────────────────────┤
│ [511:504]     │ Opcode (8 bits)                             │
│ [503:496]     │ Core ID (8 bits)                            │
│ [495:0]       │ Payload (496 bits)                          │
└───────────────┴─────────────────────────────────────────────┘

Total bits: 512
Bit 511 is the highest/leftmost bit
Bit 0 is the lowest/rightmost bit

Extracting Bit Fields in Python#

# Example: 512-bit packet (represented as Python integer)
packet = 0x0102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ABCD

# Extract opcode from bits [511:504]
# Step 1: Shift right by 504 bits to move [511:504] to [7:0]
# Step 2: Mask with 0xFF (8 bits = 0b11111111) to keep only lower 8 bits
opcode = (packet >> 504) & 0xFF
# Result: 0x01

# Extract core_id from bits [503:496]
core_id = (packet >> 496) & 0xFF
# Result: 0x02

# Extract payload from bits [495:0]
# Mask with all 1s for lower 496 bits: (2^496 - 1)
payload = packet & ((1 << 496) - 1)
# Result: 0x0000...ABCD (496 bits)

# Extract a middle field, e.g., bits [479:464] (16 bits)
field_16bit = (packet >> 464) & 0xFFFF

Constructing Packets from Bit Fields#

# Build a 512-bit packet from components
opcode = 0x01      # 8 bits
core_id = 0x00     # 8 bits
payload = 0x1234   # 496 bits (upper bits assumed zero)

# Shift each field to its position and combine with OR
packet = (opcode << 504) | (core_id << 496) | payload

# Result:
# Bits [511:504] = 0x01 (opcode)
# Bits [503:496] = 0x00 (core_id)
# Bits [495:0]   = 0x00...001234 (payload)

Bit Masks#

Masks are used to isolate specific bits:

# Create a mask for N bits (all 1s)
mask_8bit  = 0xFF          # 0b11111111 (8 bits)
mask_16bit = 0xFFFF        # 0b1111111111111111 (16 bits)
mask_32bit = 0xFFFFFFFF    # (32 bits)

# Create a mask for specific bit range [N:M]
def create_mask(high_bit, low_bit):
    num_bits = high_bit - low_bit + 1
    mask = (1 << num_bits) - 1  # 2^num_bits - 1
    return mask << low_bit

# Example: Mask for bits [15:8] (8 bits in the middle)
mask = create_mask(15, 8)  # Returns 0xFF00

# Use mask to extract field
value = 0x12345678
field = (value & mask) >> 8  # Extract bits [15:8] → 0x56

Address Arithmetic#

Adding Offsets#

base_address = 0x12340000
offset = 0x100  # 256 bytes

# Compute new address
new_address = base_address + offset  # 0x12340100

# For array indexing (4-byte elements)
element_size = 4  # bytes
index = 10
element_address = base_address + (index * element_size)  # 0x12340028

Splitting Addresses into Pages and Offsets#

Many memory systems organize memory into pages:

address = 0x12345678

# 4KB pages (12-bit offset, 20-bit page number for 32-bit address)
page_size = 4096  # 0x1000
page_offset = address & 0xFFF         # Lower 12 bits: 0x678
page_number = address >> 12           # Upper 20 bits: 0x12345

# Reconstruct address
reconstructed = (page_number << 12) | page_offset  # 0x12345678

Range Checking#

# Check if address is within a region
region_start = 0xD0000000
region_size = 0x1000  # 4KB
region_end = region_start + region_size

address = 0xD0000500

if region_start <= address < region_end:
    print(f"Address 0x{address:08X} is in region")
    offset_in_region = address - region_start  # 0x500

Common Address Ranges in Our System#

┌──────────────────────────────────────────────────────────────┐
│ System Address Map                                           │
├────────────────────────┬─────────────────────────────────────┤
│ 0x00000000-0x7FFFFFFF  │ Host System RAM (DDR4, 2 GB)        │
│ 0xD0000000-0xD0000FFF  │ FPGA Control Registers (4 KB)       │
│ 0x00000000-0x00003FFF  │ HBM Region 1: Axon Pointers (16 KB) │
│ 0x00004000-0x0007FFFF  │ HBM Region 2: Neuron Ptrs (512 KB)  │
│ 0x00080000-0x7FFFFFFF  │ HBM Region 3: Synapses (~2 GB)      │
└────────────────────────┴─────────────────────────────────────┘

Note: HBM addresses are local to the FPGA (not in host address space)

Python Helper Functions#

def hex_dump(data, bytes_per_line=16):
    """
    Print data in hexadecimal with addresses

    Example output:
    0x0000: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
    0x0010: 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20
    """
    for i in range(0, len(data), bytes_per_line):
        # Format address
        addr = f"0x{i:04X}:"

        # Format hex bytes
        hex_bytes = " ".join(f"{b:02X}" for b in data[i:i+bytes_per_line])

        print(f"{addr} {hex_bytes}")

def extract_bits(value, high_bit, low_bit):
    """Extract bits [high_bit:low_bit] from value"""
    num_bits = high_bit - low_bit + 1
    mask = (1 << num_bits) - 1
    return (value >> low_bit) & mask

def insert_bits(dest, value, high_bit, low_bit):
    """Insert value into bits [high_bit:low_bit] of dest"""
    num_bits = high_bit - low_bit + 1
    mask = (1 << num_bits) - 1
    # Clear the target bits
    dest &= ~(mask << low_bit)
    # Insert the new value
    dest |= (value & mask) << low_bit
    return dest

def is_aligned(address, alignment):
    """Check if address is aligned to 'alignment' bytes"""
    return (address & (alignment - 1)) == 0

# Usage examples
value = 0x12345678
opcode = extract_bits(value, 31, 24)  # Get upper byte → 0x12

packet = 0
packet = insert_bits(packet, 0x01, 511, 504)  # Set opcode
packet = insert_bits(packet, 0x00, 503, 496)  # Set core_id

print(f"Is 0x1004 aligned to 4 bytes? {is_aligned(0x1004, 4)}")  # True
print(f"Is 0x1005 aligned to 4 bytes? {is_aligned(0x1005, 4)}")  # False

Summary#

Key Takeaways:

  1. Binary is fundamental: Everything in hardware is binary (0s and 1s)

  2. Hexadecimal is convenient: Each hex digit = 4 bits, compact representation

  3. Byte addressing: Addresses point to bytes (8 bits), not bits or words

  4. Alignment matters: Many operations require aligned addresses for correctness/performance

  5. Bit field notation [MSB:LSB]: Standard way to specify bit ranges in documentation

  6. Bit manipulation: Shift (>>, <<) and mask (&, |) operations extract/insert bit fields

When working with hardware:

  • Always check address alignment requirements

  • Use bit field notation to understand packet/register formats

  • Convert between hex, binary, and decimal as needed

  • Verify address ranges before accessing memory

This foundation is essential for understanding the packet formats and memory structures described in the rest of the documentation.