Skip to content

pibot_pendant_v1_0_factory_app_display

Display subsystem for the PiBot Pendant V1.0 factory application. Provides a two-layer stack — a drawing API (gfx.c) sitting on top of a minimal ILI9341 SPI driver (ili9341.c) — that the factory application uses to render its interactive menu UI, progress indicators, and diagnostic screens. An optional snapshot subsystem (compiled in when ENABLE_SNAPSHOT is defined) tees every rendered frame to a raw file on the SD card so the snap2png.py tool can produce a PNG audit trail of factory test results.


  1. Architecture Overview
  2. Component Descriptions
  3. API Reference
  4. Data Flow
  5. Snapshot File Format
  6. ILI9341 Initialization Sequence
  7. Memory Design
  8. Hardware Configuration
  9. Color Encoding and Byte Order
  10. Module Relationships

The module is a self-contained display stack isolated from the main firmware’s LVGL-based UI system. It operates entirely in the factory application’s single task context — no LVGL, no DMA callbacks, no FreeRTOS queues. All SPI transfers use synchronous polling (spi_device_polling_transmit).

Hardware

pibot_pendant_v1_0_factory_app_display

Factory Application (pibot_pendant_v1_0_factory_app)

gfx_* calls

gfx_flush() wrapper

snap_write()

[if capturing]

fwrite()

spi_device_polling_transmit

MOSI/CLK/CS/DC

main.c

(draw_header, draw_menu,

draw_progress, …)

gfx.c

Graphics Abstraction Layer

───────────────────────

gfx_clear / gfx_draw_char

gfx_draw_string / gfx_hline

gfx_vline / gfx_rect

gfx_fill_rect

Snapshot Subsystem

(ENABLE_SNAPSHOT)

───────────────────────

gfx_snapshot_begin/end

snap_write

ili9341.c

ILI9341 SPI Driver

───────────────────────

ili9341_init / ili9341_flush

ili9341_backlight

lcd_cmd / lcd_data

ESP-IDF SPI Master

(polling mode)

ILI9341 TFT

240×320 RGB565

SD Card

(snapshot files)

LayerFileRole
Drawing APIgfx.cPixel-level primitives; byte-swap management; snapshot tee
Hardware Driverili9341.cSPI bus setup; ILI9341 register sequences; chunked transfer
Font datafont8x16.hEmbedded 8×16 bitmap font accessed via font8x16_get_glyph()
Pin configurationhw_config.hTFT_* GPIO/SPI macros; SCREEN_WIDTH/HEIGHT/ROTATION

gfx.c provides the complete drawing vocabulary used by the factory application’s UI renderer. Its design priorities are:

  • Zero dynamic allocation in the hot drawing path — a static uint16_t line_buf[SCREEN_WIDTH] is reused across all line, fill, and clear operations.
  • Uniform byte-swap — every color value is byte-swapped from host-endian to SPI big-endian (the ILI9341 wire format) exactly once, before being written into line_buf or char_buf.
  • Transparent snapshot tee — the single internal gfx_flush() wrapper calls both ili9341_flush() and (when capturing) snap_write(), so no drawing call site needs to be aware of snapshotting.

gfx_init() is intentionally empty. The ILI9341 driver must be initialized separately via ili9341_init() before any drawing call.

When ENABLE_SNAPSHOT is defined at compile time, gfx.c gains a file-backed framebuffer tee:

  • gfx_snapshot_begin(filepath) opens a file, writes an 8-byte header, and pre-fills the pixel region with zeros (black). The caller is then expected to perform a full screen redraw to populate the file.
  • Every gfx_flush() call subsequently writes its pixel data to the open file via snap_write(), un-swapping bytes back from SPI big-endian to native RGB565.
  • gfx_snapshot_end() closes the file.
  • snap_write() uses fseek to position each row at the correct file offset (SNAP_HEADER_SIZE + (row * SCREEN_WIDTH + x0) * 2), allowing partial-region updates to land in the correct row without requiring a full scratch buffer.

The raw snapshot file is consumed offline by the pibot_pendant_v1_0_factory_app_tools snap2png.py script, which reads the header dimensions and converts the RGB565 pixel stream to a standard PNG.


ili9341.c is a minimal, factory-only SPI driver for the ILI9341 TFT controller. It does not use the shared hardware driver from display_spi_drivers; instead it provides a lean, self-contained implementation suited to the factory environment (no LVGL, no ESP LCD panel API abstraction).

Key design choices:

  • Uses spi_device_polling_transmit — synchronous, no DMA callback dependencies, predictable latency in a non-RTOS-sensitive single-task context.
  • Issues spi_bus_free() before spi_bus_initialize() to handle the case where the SPI2 peripheral retains state after the previous firmware’s software reset.
  • Pixel data is sent in 4096-byte chunks to stay within SPI DMA transfer size limits.
  • Backlight is kept off (gpio_set_level(TFT_LED, 0)) during ili9341_init() to prevent the user from seeing partial initialization frames; the caller enables it afterwards with ili9341_backlight(true).
  • Display inversion (0x21) is enabled by default because most ILI9341 panels require it for correct color rendering. A comment in the source guides hardware porters who encounter inverted colors.
  • The DC (Data/Command) GPIO is driven directly by lcd_cmd() (DC=0) and lcd_data() (DC=1) before each SPI transaction.

All coordinates are in pixels relative to the top-left corner (0, 0). All functions silently clip to screen bounds — no call results in an out-of-bounds SPI transfer.

FunctionSignatureDescription
gfx_initvoid gfx_init(void)Placeholder; currently a no-op. Call ili9341_init() separately before drawing.
gfx_clearvoid gfx_clear(uint16_t color)Fill the entire screen with a solid RGB565 color. Renders row by row using line_buf.
gfx_draw_charvoid gfx_draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)Render a single 8×16 character at (x, y) with foreground and background colors. Non-printable characters are substituted with ?.
gfx_draw_stringvoid gfx_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg)Render a null-terminated string left-to-right. Stops when the next character would exceed SCREEN_WIDTH.
gfx_hlinevoid gfx_hline(int x, int y, int w, uint16_t color)Draw a horizontal line of width w starting at (x, y).
gfx_vlinevoid gfx_vline(int x, int y, int h, uint16_t color)Draw a vertical line of height h starting at (x, y). Sends one pixel per flush call.
gfx_rectvoid gfx_rect(int x, int y, int w, int h, uint16_t color)Draw a hollow rectangle outline using four line calls.
gfx_fill_rectvoid gfx_fill_rect(int x, int y, int w, int h, uint16_t color)Draw a solid filled rectangle, row by row using line_buf.
gfx_snapshot_beginbool gfx_snapshot_begin(const char *filepath)(Requires ENABLE_SNAPSHOT) Open a raw capture file. Returns false if already capturing or the file cannot be created.
gfx_snapshot_endbool gfx_snapshot_end(void)(Requires ENABLE_SNAPSHOT) Close the capture file. Returns false if not currently capturing.
gfx_snapshot_is_capturingbool gfx_snapshot_is_capturing(void)(Requires ENABLE_SNAPSHOT) Returns true if a snapshot file is currently open.
FunctionSignatureDescription
ili9341_initesp_err_t ili9341_init(void)Configure GPIO pins, initialize SPI bus and device, run ILI9341 startup sequence. Returns ESP_OK on success.
ili9341_backlightvoid ili9341_backlight(bool on)Enable (true) or disable (false) the TFT backlight GPIO.
ili9341_flushvoid ili9341_flush(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, const uint16_t *data, size_t len)Set column/row address window and transmit len RGB565 pixels in 4096-byte chunks. Data must already be in SPI big-endian byte order.

The static functions lcd_cmd(), lcd_data(), and lcd_data_byte() are internal to ili9341.c and are not part of the public interface.


ILI9341 TFTESP-IDF SPIili9341.cgfx.cmain.cILI9341 TFTESP-IDF SPIili9341.cgfx.cmain.cloop[chunks of 4096 bytes]loop[for each row]gfx_fill_rect(x, y, w, h, color)byte-swap color into line_buf[w]gfx_flush(x, y+row, x+w-1, y+row, line_buf, w)ili9341_flush(x0, y0, x1, y1, data, len)lcd_cmd(0x2A) - column addresslcd_data(col_data, 4)lcd_cmd(0x2B) - row addresslcd_data(row_data, 4)lcd_cmd(0x2C) - memory writespi_device_polling_transmit(chunk)MOSI/CLK/CS/DC SD Card Fileili9341.cgfx.cmain.cSD Card Fileili9341.cgfx.cmain.cFull screen redraw beginsloop[every draw call]Raw file ready for snap2png.pygfx_snapshot_begin("/sdcard/snap_001.raw")fopen() → write 8-byte header → pre-fill zeros (black)truegfx_*(…)gfx_flush(x0, y0, x1, y1, data, n)ili9341_flush(…) - display updatedsnap_write(x0, y0, x1, y1, data_swapped, n)fseek to row offset → un-swap bytes → fwrite pixelsgfx_snapshot_end()fclose()true

The raw snapshot file produced by gfx_snapshot_begin / gfx_snapshot_end has the following binary layout:

Offset Size Type Description
────── ──── ──── ───────────
0 4 uint32 LE Screen width (e.g. 240)
4 4 uint32 LE Screen height (e.g. 320)
8 w×h×2 uint16[] RGB565 pixels, native endian, row-major
Row 0 (top) → Row h-1 (bottom)
Each row: pixel[0] (left) → pixel[w-1] (right)

Total file size (240×320 display): 8 + 240 × 320 × 2 = 153,608 bytes

The snap_write() function reverses the SPI big-endian byte swap applied by gfx.c drawing functions before writing to the file, so stored values are native-endian RGB565 as consumed by standard image tools.

The pibot_pendant_v1_0_factory_app_tools snap2png.py script reads this format and produces a standard PNG file.


ili9341_init() configures GPIO and SPI before running the ILI9341 startup register sequence:

GPIO config

DC pin + LED pin as OUTPUT

Backlight OFF during init

spi_bus_free(TFT_HOST)

Clear stale SPI state from prior firmware

spi_bus_initialize(TFT_HOST)

max_transfer_sz = SCREEN_WIDTH × SCREEN_HEIGHT × 2

spi_bus_add_device()

speed = TFT_SPI_FREQ_HZ, CS = TFT_CS

CMD 0x01 - Software Reset

⏱ delay 150 ms

CMD 0x11 - Sleep Out

⏱ delay 120 ms

CMD 0x3A + 0x55

Pixel Format = 16-bit RGB565

CMD 0x21 - Display Inversion ON

Required for correct colors on most ILI9341 panels

Change to 0x20 if colors appear inverted

CMD 0x36 + rotation_map[SCREEN_ROTATION]

Memory Access Control - sets orientation

CMD 0x29 - Display ON

⏱ delay 50 ms

Init complete

Caller invokes ili9341_backlight(true)

Rotation map (MADCTL byte via SCREEN_ROTATION):

SCREEN_ROTATIONMADCTLOrientation
00x28Landscape
10x48Portrait
20xE8Landscape inverted
30x88Portrait inverted

This module is designed to operate within the constrained DRAM budget of the factory application (no WiFi stack, no Bluetooth, but still a resource-limited ESP32).

Stack - allocated per call, freed on return

char_buf[FONT_WIDTH × FONT_HEIGHT]

8 × 16 × 2 = 256 bytes

gfx_draw_char only

spi_transaction_t ~32 bytes

inner loop of ili9341_flush

Static / BSS - zero heap cost

line_buf[SCREEN_WIDTH]

240 × 2 = 480 bytes

Reused across ALL drawing calls

s_snap_file FILE*

ENABLE_SNAPSHOT only

spi_dev spi_device_handle_t

Key constraints respected:

  • No malloc() / new in any drawing path.
  • char_buf (256 bytes) is the largest per-call stack allocation; it is within the 512-byte safe limit defined in the project memory guidelines.
  • line_buf is a module-level static — the same buffer is reused for every row of every fill or clear operation, eliminating both allocation overhead and fragmentation risk.
  • snap_write() uses fseek/fwrite in a tight per-pixel loop with no intermediate heap buffer; the SD card’s internal buffering handles I/O efficiency.

All pin assignments and display geometry are resolved at compile time from hw_config.h. The factory application does not support runtime reconfiguration.

MacroPurpose
TFT_HOSTESP32 SPI host (SPI2_HOST / SPI3_HOST)
TFT_MOSIGPIO: SPI Master-Out (data to display)
TFT_MISOGPIO: SPI Master-In (typically -1; display is write-only)
TFT_CLKGPIO: SPI Clock
TFT_CSGPIO: Chip Select (active low)
TFT_DCGPIO: Data/Command select (low = command, high = data)
TFT_LEDGPIO: Backlight enable
TFT_SPI_FREQ_HZSPI clock frequency in Hz
SCREEN_WIDTHDisplay width in pixels (e.g. 240)
SCREEN_HEIGHTDisplay height in pixels (e.g. 320)
SCREEN_ROTATIONRotation index 0–3 (see rotation map above)
FONT_WIDTHCharacter cell width in pixels (8)
FONT_HEIGHTCharacter cell height in pixels (16)
FONT_BYTES_PER_ROWBytes per glyph row in the font8x16 bitmap

For the complete hardware schematic and pin mapping, see the PiBot CNC Pendant Hardware Documentation.


Understanding the byte-swap pipeline is important when reading or modifying rendering code:

(c >> 8) | (c << 8)

byte-swap in gfx.c

ili9341_flush()

MOSI wire

snap_write()

(sw >> 8) | (sw << 8)

un-swap

Application color

RGB565 host endian

e.g. 0xF800 = red

SPI buffer / line_buf

RGB565 big-endian

e.g. 0x00F8 = red on wire

ILI9341 panel

decodes RGB565 correctly

displays red pixel

Raw snapshot file

RGB565 native endian

ready for snap2png.py

  • Every color is byte-swapped exactly once inside gfx.c before entering line_buf or char_buf.
  • ili9341_flush() receives already-swapped data and transmits it verbatim over SPI.
  • snap_write() receives the swapped data from gfx_flush() and un-swaps it before writing to the file, restoring native-endian RGB565 suitable for offline processing tools.

Hardware_Peripheral_Drivers

pibot_pendant_v1_0_bootloader

pibot_pendant_v1_0_factory_app

gfx_* drawing calls

snapshot .raw files

(ENABLE_SNAPSHOT)

snap2png.py reads

raw snapshot files

button/touch events

trigger UI redraws

parallel implementation

factory-only, no LVGL

pibot_pendant_v1_0_factory_app_display

(this module)

pibot_pendant_v1_0_factory_app_main

pibot_pendant_v1_0_factory_app_input

pibot_pendant_v1_0_factory_app_storage

pibot_pendant_v1_0_factory_app_tools

custom_bootloader/hooks.c

Launches factory app

display_spi_drivers

Shared ILI9341 driver

for main firmware

Relationship summary:

Related ModuleRelationship
pibot_pendant_v1_0_factory_app_mainPrimary consumer — all draw_* functions in main.c call this module’s gfx_* API
pibot_pendant_v1_0_factory_app_inputSibling — button/touch/encoder events trigger display redraws via main.c
pibot_pendant_v1_0_factory_app_storageSnapshot output destination — SD card must be mounted via sdcard_mount() before calling gfx_snapshot_begin()
pibot_pendant_v1_0_factory_app_toolsOffline consumer — snap2png.py converts the raw .raw files produced by this module to PNG images
pibot_pendant_v1_0_bootloaderLaunches the factory app that initializes this display subsystem
display_spi_driversParallel implementation — the main firmware uses the shared ESP LCD panel-API ILI9341 driver; the factory app uses its own lean version without LVGL dependency
pibot_pendant_v1_0_bspBSP layer that drives the same physical display with LVGL in the main firmware — different code path, same hardware

Important: This module is part of the factory firmware only. It is not linked into the main pendant firmware. The main firmware display stack uses LVGL with the display_spi_drivers panel driver via the pibot_pendant_v1_0_bsp board support package. Refer to docs/architecture/display_drivers.md for the full display driver architecture comparison between factory and main firmware.