Color Mathematics & Formats

August 20, 2026 · View on GitHub

Understanding bit layout, endianness swapping, and color space conversions in palettes.


1. RGB565 Bit Layout (16-bit Color)

Microcontroller displays (such as ST7789, ILI9341, GC9A01, SSD1351) commonly use 16-bit RGB565, packing 5 bits of red, 6 bits of green, and 5 bits of blue into a single 16-bit integer:

Bit:  15 14 13 12 11   10  9  8  7  6  5    4  3  2  1  0
      [    RED     ]   [     GREEN     ]    [   BLUE    ]
         5 bits               6 bits            5 bits

Conversion Formula from 24-bit RGB888:

Given 8-bit values R,G,B[0,255]R, G, B \in [0, 255]:

RGB565=((R3)11)((G2)5)(B3)\text{RGB565} = ((R \gg 3) \ll 11) \mid ((G \gg 2) \ll 5) \mid (B \gg 3)

def rgb888_to_rgb565(r: int, g: int, b: int) -> int:
    """Convert 8-bit R, G, B components to a 16-bit RGB565 integer."""
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

2. SPI Byte Swapping (swapped=True)

Most SPI TFT display controllers expect pixels in Big Endian format (High byte first over SPI), whereas ARM Cortex-M / RISC-V / x86 processors store 16-bit integers in Little Endian format.

If colors appear inverted or garbled on your panel:

from palettes import get_palette

# Without byte swap (native little-endian integers)
p_native = get_palette("wheel", color_depth=16, swapped=False)

# With byte swap (ready for direct SPI transmission)
p_swapped = get_palette("wheel", color_depth=16, swapped=True)

The Byte Swap Operation:

swapped=((val8)&0x00FF)((val8)&0xFF00)\text{swapped} = ((\text{val} \gg 8) \& \text{0x00FF}) \mid ((\text{val} \ll 8) \& \text{0xFF00})


3. HSV to RGB Interpolation

The WheelPalette computes color sweeps by converting Hue (H[0,360]H \in [0, 360^\circ]), Saturation (S[0,1]S \in [0, 1]), and Value (V[0,1]V \in [0, 1]) to RGB:

  1. C=V×SC = V \times S (Chroma)
  2. H=H60H' = \frac{H}{60^\circ}
  3. X=C×(1(H(mod2))1)X = C \times (1 - |(H' \pmod 2) - 1|)
  4. m=VCm = V - C

Depending on the sextant H\lfloor H' \rfloor, (R1,G1,B1)(R_1, G_1, B_1) is assigned:

HH' Interval(R1,G1,B1)(R_1, G_1, B_1)
$0 \le H' < 1$(C,X,0)(C, X, 0)
$1 \le H' < 2$(X,C,0)(X, C, 0)
$2 \le H' < 3$(0,C,X)(0, C, X)
$3 \le H' < 4$(0,X,C)(0, X, C)
$4 \le H' < 5$(X,0,C)(X, 0, C)
$5 \le H' < 6$(C,0,X)(C, 0, X)

Final 8-bit components: R=(R1+m)×255,G=(G1+m)×255,B=(B1+m)×255R = \lfloor(R_1 + m) \times 255\rfloor, \quad G = \lfloor(G_1 + m) \times 255\rfloor, \quad B = \lfloor(B_1 + m) \times 255\rfloor


4. Depth Constants

ConstantBit DepthBytes / PixelMemory (320×240)
11-bit monochrome1/8 byte9.6 KB
22-bit grayscale1/4 byte19.2 KB
44-bit grayscale1/2 byte38.4 KB
88-bit grayscale1 byte76.8 KB
1616-bit RGB5652 bytes153.6 KB
2424-bit RGB8883 bytes230.4 KB
3232-bit ARGB88884 bytes307.2 KB