# RT-4D Display Subsystem — API Reference (`display` key) Reverse-engineered drawing toolkit for the Radtel RT-4D stock firmware (`rt4d_stock_v3.25_abs_0x08002800.bin`, ARM Cortex-M4F Thumb, load base `0x08002800`). This is the low-level graphics API to **reuse** when rewriting the UI. All addresses are absolute vaddrs. Signatures use ARM Thumb AAPCS (r0..r3 = args, extra args on stack). > **TL;DR for the UI rewrite:** the LCD is a **monochrome page-addressed panel, 128×64 > px = 8 pages × 128 columns**, driven by **bit-banged serial (GPIO), with NO framebuffer** — > every draw call clocks pixels *directly into the controller's GDDRAM*. There is nothing to > "flush." You position a cursor with `lcd_set_pos(page,col)` and stream 8-bit vertical > pixel-slices with `lcd_write_col(byte)`. Everything above that (chars, strings, icons, > boxes) is built from those two. Fonts live in **external SPI data-flash**, fetched at draw > time by `spi_flash_read`. The single most important callable is > **`draw_string(page,x,str,len,mode) @0x08008A50`**. --- ## 1. Display hardware ### 1.1 Panel type, resolution, color format | Property | Value | Evidence | |---|---|---| | Type | **Monochrome, page-addressed** LCD (ST7565/UC1701/SSD1306-class controller) | `lcd_write_col` streams **8 vertical pixels per byte, LSB=top**; page addressing in `lcd_set_pos` | | Resolution | **128 (W) × 64 (H) px** | `draw_string` wraps X at 121 and cycles page `& 7` (8 pages × 8 = 64 rows); status icons at cols 0x15/0x7B (21/123) | | Pages (rows of 8px) | **8** (page 0 = top) | page arg masked `& 7` in the wrap logic (`0x08008A82`) | | Color format | **1 bpp** (bit set = pixel on). "mode" adds invert/outline (see §4) | `lcd_write_col` shifts one bit/pixel; blitter mode transforms are bit ops | | Controller column origin | **183** (`0xB7`) — visible window is offset inside a wider GDDRAM | `lcd_set_pos`: `col_cmd = 0xB7 − x` (`rsb r1,r5,#0xB7` @`0x08014A82`) | | Framebuffer | **NONE** — direct-to-GDDRAM bit-bang | `lcd_write_col`/`lcd_set_pos` write GPIO registers directly; no large SRAM buffer touched | Because there is no framebuffer, drawing is **immediate** and **non-atomic** — a partial redraw is visible mid-frame. The stock UI mitigates flicker by only repainting changed regions. A rewrite can adopt the same discipline, or maintain its own SRAM shadow buffer and blit it with the raw-bitmap primitive (`lcd_blit_cols`, §2). ### 1.2 The bit-bang interface (GPIO, not SPI2) The LCD is **not** on SPI2 (SPI2 @0x40003800 is used elsewhere; the external **SPI data-flash** that holds the fonts is itself bit-banged on GPIOB — see §5). The LCD is driven by three GPIO "registers" resolved from `lcd_set_pos`/`lcd_write_col` literal pools: | Symbol in this doc | Literal | Role (inferred) | Access pattern | |---|---|---|---| | `LCD_RS` | `0x40010000` | **Register-Select / DC** line, driven bit0 via helper `gpio_bit0(base,val) @0x08021C4C` (`bfi [r0],r1,#0,#1`). `0`=command phase, `1`=data phase | set 0 before a byte, 1 after | | `LCD_PORT` | `0x40020400` (GPIOB) | **CLK + DATA** bit-bang port. Set-bits reg at `+0x18`, clear-bits reg at `+0x28` (Artery-clone GPIO SCR/CLR layout) | per-bit CLK low/high, DATA set/clear | | `LCD_CS` | `0x40020000` (GPIOA) | **CS/latch** — writes GPIOA `+0x18`/`+0x28` around each byte | strobe per byte | > **Note on the clone GPIO map.** This MCU is an STM32F407-class **Artery AT32** clone. Its GPIO > "set" and "clear" strobe registers sit at `base+0x18` and `base+0x28` (used as `str val,[port]` > and `str val,[port+0x10]` after the code loads `port+0x18`). Bit masks seen: `0x08` = CLK, > `0x20` = DATA. The exact silicon pin numbers are not load-bearing for the UI rewrite (you call > the primitives, you don't re-bang pins); they are documented here only to explain the register > writes. `gpio_bit0 @0x08021C4C`, `gpio_clr @0x08021C56`, `gpio_test @0x08021C5C` are the generic > helpers. ### 1.3 The two hardware primitives (bottom of the stack) ```c // @0x08014A7C — position the GDDRAM write cursor. // Sends 3 command bytes: (0xB7 - x_col), (0x10 | (y>>4)), (y & 0x0F) via lcd_send_cmd. // NOTE arg order: r0 = page/x-select, r1 = the 16-bit-ish column value. See §3 for the // exact meaning as used by the blitters: r0 = page index, r1 = column (x pixel). void lcd_set_pos(uint8_t page, uint16_t col); // @0x08014A7C // @0x08014AA0 — send ONE command byte (RS=0), MSB-first, 8 clocked bits. Internal to set_pos; // 0 external callers. Effectively lcd_send_cmd(uint8_t cmd). static void lcd_send_cmd(uint8_t cmd); // @0x08014AA0 // @0x08014B28 — write ONE data byte = a vertical strip of 8 pixels (LSB = top pixel) at the // current cursor, then auto-advances the column. RS=1. Bits shifted LSB-first (asrs r5,#1). void lcd_write_col(uint8_t column_bits); // @0x08014B28 ``` `lcd_set_pos` internally calls `lcd_send_cmd` three times (col-high, page, col-low). `lcd_write_col` clocks 8 data bits then strobes CS. Both use the busy-wait delay `delay_short(n) @0x08007934` (a `subs/bne` spin — **not** a real timer) between edges. ### 1.4 Panel init / power / backlight - **Backlight** is a **TIM PWM** on TIM1 (`0x40010000`), programmed via `tim_set_ccr @0x08021C14` and `tim_set_arr_psc @0x08021C04`. - `lcd_set_brightness(level) @0x08014C34` — `level` 0..4 → duty {0, 5, 0x1E, 0x64, 0xFF}. Called from `0x08009204`. - `backlight_on/init @0x08014BEC` and `@0x08014C0C` — enable pin + set duty. - **Panel reset / power pins** are toggled in `lcd_power_seq @0x0801D938` (writes GPIOB `+0x18`/`+0x28` BSRR, runs a PWM ramp via `pwm_cfg @0x0802129C`, delays via `delay_ms @0x08007946`). - The panel **command init sequence** (contrast/segment-remap/display-on) is executed once at boot through the same `lcd_send_cmd` path; it does not need to be re-issued by a UI rewrite that keeps the stock boot. (Reusing `lcd_set_pos`/`lcd_write_col` after boot is sufficient.) `delay_ms(ms) @0x08007946`, `delay_short(loops) @0x08007934` — timing helpers usable by UI code. --- ## 2. Drawing primitives (callable API) All coordinates: **`page`** = vertical row of 8 px (0=top..7), **`x`/`col`** = pixel column (0=left..127). Text/GBK cells are **2 pages tall (16 px)**; the compact font is 1 page tall. | vaddr | Suggested name | Signature (AAPCS) | What it does | Evidence | |---|---|---|---|---| | **0x08008A50** | **draw_string** | `void draw_string(u8 page, u8 x, const char *s, u16 len, u8 mode /*[sp]*/)` | **Primary text API.** Iterates bytes: `0x01–0x7F`→ASCII glyph (+7px); `0x80–0xFE`→GBK lead byte, consumes trail, `(lead<<8)\|trail`, wide glyph (+14px); `0x00/0xFF`→space/stop. Handles X-wrap at 121 and page advance. 5th arg `mode` is read at `[sp+0x28]` inside (10-reg push). | disasm §confirmed; callers pass `mode` via `str r?,[sp]` before `bl` (e.g. `0x0800318C`, `0x0801408E`) | | 0x08007FB8 | ascii_blit | `void ascii_blit(u8 page, u8 x, u8 ch, u8 mode)` | Draws one **7px-wide × 16px-tall** ASCII glyph. Reads **14 bytes** from SPI font `0x19C000 + (ch−0x20)*14` via `spi_flash_read`. Applies mode (invert/outline). Emits via `lcd_set_pos`+`lcd_write_col` (2 pages × 7 cols). | `mov r2,#0x19c000`; `index=(ch-0x20)*? ; 14B read`; per-col set_pos/write_col loop | | 0x08008076 | ascii_blit_v2 | `void (u8 page,u8 x,u8 ch)` | Variant of ascii_blit using width-shaper `0x08005368` (proportional spacing table). | `0x19c000`, calls `0x08005368` | | 0x08008454 | gbk_blit | `void gbk_blit(u8 page, u8 x, u16 gbk, u8 mode)` | Draws one **14px-wide × 16px-tall** double-byte (GB2312/GBK) glyph. `row=lead−0x81, col=trail−0x40, index=col+row*190 (−1 if trail>0x7F)`, reads **28 (0x1C) bytes** from `0x19E000 + index*28`. | `mov r2,#0x19e000`; `movs r1,#0x81`; 0x1C read | | 0x08008530 | gbk_char_at | `void gbk_char_at(u8 page, u8 x, u16 gbk)` | Standalone directly-callable single wide-char draw (same math as gbk_blit, base `0x19E000`, 28B). | `0x19e000`, `subs #0x40`, `0x81` | | 0x080089AC | small_char | `void small_char(u8 page, u8 x, char ch, ...)` | **Compact 5px font** renderer (1 page tall). Reads `(ch−0x20)*5` from an in-flash 5×8 table; clamps ch to 0x20..0x7F. Used for tiny status text/numbers. | `subs #0x20`, `*5` (`r7*5`), single-page write | | **0x08008CF0** | draw_hline_seg | `void (u8 col, bool on)` | Draws a 2-px separator at pages 4 & 5 (values 0xFE/0x7F when on, else 0) — a horizontal rule/underline used under menu fields. | set_pos(page4/5), write_col(0xFE/0x7F) | | **0x0800842C** | fill_col_run | `void fill_col_run(u8 page, u8 x, u8 count)` | Writes `count` columns of constant `0x18` at `page` — a thin horizontal bar (used for gauge ticks). | write_col(0x18) loop `r5>bit`) for sub-page pixel plotting; used by line/rect fills. | `0x80 asr`, `orr` mask accumulate | | 0x08008B90 | draw_number_row | `void (u8 page,u8 x,const u8*digits,u16 n,u8 mode)` | Draws `n` glyphs from `digits[]` with **12px** stride (`(i*3)<<2`) — fixed-pitch number/label row (calls big_char `0x08008100`). | stride `r4*3<<2`, calls `0x08008100` | | 0x08008BC6 | draw_str_spaced | `void (u8 page,u8 x,const u8*s,u16 n,u8 mode)` | Draws `n` chars with **6px** proportional stride (calls small_char `0x080089AC`). | `+6` stride, calls `0x080089AC` | | 0x08008BFC | draw_field | menu field value paint | Copies a field record (0x14 bytes) and renders label+value; reads struct offsets `+0x102/+0x104/+0x108/+0x10A`. | `movs r2,#0x14`, `memcpy 0x08002BEE` | ### 2.1 Screen-clear / fill There is **no single "clear whole screen"** ROM export separate from the loop; the stock code clears by writing zero columns. **To clear a region**, call `blit_cols(page, count, NULL, 0)` (`@0x08008D24` with `src==0` → writes `0x00`) per page, or `blit_rect` (`@0x08008D62`) with a zero bitmap. **To clear the full 128×64:** loop `page = 0..7`, `blit_cols(page, 128, NULL, 0)`. `draw_string` with a run of `0x20` (space) glyphs also erases a text row. --- ## 3. Coordinate system & text metrics ``` col/x → 0 ........................................ 127 (128 px wide) page 0 ┌───────────────── status bar (icons) ─────────────┐ y 0..7 page 1 │ │ y 8..15 page 2 │ text/GBK cells are 16 px tall = 2 pages │ ... │ │ page 7 └───────────────────────────────────────────────────┘ y 56..63 ``` - **Origin:** top-left. `page` grows downward, `x`/`col` grows rightward. - **`page` (row) unit:** one page = **8 vertical pixels**. Valid 0..7. Passed as `r0` to blitters. - **`x` (column) unit:** **pixels**, 0..127. Passed as `r1`. - **`lcd_set_pos(page, col)`** maps `col` to the controller command `0xB7 − col` (window offset) and `page` via `0x10 | (page>>4)` / `page & 0x0F`. The blitters call it once per (page, column) pair, so you rarely call it directly. **Text cell metrics:** | Font | Draw fn | Cell W × H | Advance | Glyph src (SPI) | Bytes/glyph | |---|---|---|---|---|---| | ASCII (normal) | `ascii_blit` / via `draw_string` | 7 × 16 px (2 pages) | **+7 px** | `0x19C000 + (ch−0x20)*14` | 14 | | GBK / CJK / Cyrillic | `gbk_blit` / via `draw_string` | 14 × 16 px (2 pages) | **+14 px** | `0x19E000 + index*28` | 28 | | Compact | `small_char` | 5 × 8 px (1 page) | **+6 px** (with `draw_str_spaced`) | in-flash 5×8 table | 5 | | Large digits | `big_char` | ~11 × 16 px | +12 px (`draw_number_row`) | in-flash *0x20 table | 33 (0x21) | - **Row height** = 16 px = 2 pages. A 128×64 screen therefore fits **4 text rows** (pages 0-1, 2-3, 4-5, 6-7). Menus use page-4 for the highlighted line (`draw_string(4, …)` in the menu field painter `@0x08014074`). - **Chars per row:** 128/7 ≈ **18 ASCII** or **9 GBK** (2-byte) chars. - **X-wrap:** `draw_string` auto-wraps to next 2-page cell when `x` passes ~121 (ASCII) / 114 (GBK). --- ## 4. Color / mono handling and the `mode` parameter The panel is 1-bpp. "Color" = pixel on/off. The **`mode`** argument (5th arg of `draw_string`, 4th of `ascii_blit`/`gbk_blit`, read from stack `[sp+0x28]` in `draw_string`) selects a per-glyph bit transform applied to the fetched font bytes **before** they are clocked out: | mode | Name | Transform (in `ascii_blit @0x08007FB8`) | Visual | |---|---|---|---| | **0** | Normal | none | black text on white (pixel = font bit) | | **1** | Inverse | `byte = ~byte`, plus edge-bit clears (`&0xFE`/`&0x7F` on alternating bytes) to keep a 1px gutter | white text in a black bar (selection highlight) | | **2** | Outline | `byte \|= 0x80` on even bytes (adds a boundary line) | boxed/outlined text | Evidence: `0x08007FDE cmp r7,#1 → invert loop (mvns)`; `0x0800801C cmp r7,#2 → orr #0x80 loop`. `draw_string` forwards its 5th arg unchanged as this `mode`. **Selection highlighting in menus = `mode 1`.** For a UI rewrite, pass `mode=1` to render the currently-selected line inverted. --- ## 5. Font/asset fetch (SPI data-flash) Glyph bitmaps are **not in MCU flash** — they are read on demand from the external SPI data-flash (the same 4 MB device dumped in `radio-spi-dump.bin`) by: ```c // @0x08021828 (entry 0x08021826 sets r0=0 then falls through) void spi_flash_read(void *dst, uint32_t src_addr, uint32_t len); // @0x08021828 ``` - Issues SPI opcode **0x03** (read), clocks a **3-byte** address (or 4-byte if the chip capacity byte is 0x18/0x19), streams `len` bytes into `dst`. Bit-banged on **GPIOB** (`0x40020400` region, literals `0x8021820..0x8021AD8`). 34 direct BL callers — the glyph blitters are the heavy users. - **Font bases:** ASCII narrow `0x19C000` (14 B/glyph), GBK/wide `0x19E000` (28 B/glyph). Cyrillic is present in the GBK bank at GB2312 row A7 (see `RT-4D_Russification.md`). A separate Unicode codepoint index lives at SPI `0x3F0000`. - **Constraint respected:** these are SPI **reads only** for the UI; they do not touch the codeplug format or the CPS serial protocol. A UI rewrite reuses `spi_flash_read` as-is for fonts. Related SPI HW primitives (do **not** reuse for writes in a UI rewrite — codeplug boundary): `spi_send_byte @0x08021538`, `spi_read_bytes @0x08021580`, `spi_sector_erase @0x08021924` (opcode 0x20, 4 KB), `spi_page_program @0x08021A70`. --- ## 6. Minimal "draw a screen from scratch" recipe Using only reused stock entry points (no framebuffer needed): ```c // ---- reused stock API (thumb addresses, call with bl / function pointers) ---- void draw_string (u8 page, u8 x, const char *s, u16 len, u8 mode); // 0x08008A50 void gbk_char_at (u8 page, u8 x, u16 gbk); // 0x08008530 void blit_cols (u8 page, u8 count, const u8 *cols, u8 src); // 0x08008D24 (src=0 => clear) void draw_box (u8 page_origin); // 0x08008DAA void lcd_set_pos (u8 page, u16 col); // 0x08014A7C void lcd_write_col(u8 column_bits); // 0x08014B28 void lcd_set_brightness(u8 level); // 0x08014C34 void my_screen(void) { // 1. Clear the whole 128x64 (8 pages) — write zero columns. for (u8 pg = 0; pg < 8; pg++) blit_cols(pg, 128, 0, 0); // src=0 -> 0x00 fill // 2. Title on the top text row (pages 0-1), normal video. draw_string(0, 2, "MAIN", 4, 0); // 3. A selected menu line on pages 4-5, inverse video (highlight). draw_string(4, 1, "Channel 001", 11, 1); // mode 1 = inverse // 4. A framed popup border (rounded box) starting at page 1. draw_box(1); // 5. Draw a raw 8px-tall icon: position cursor, stream columns. static const u8 batt[8] = {0x3C,0x24,0x24,0x24,0x24,0x24,0x3C,0x18}; lcd_set_pos(0, 118); // page 0, x=118 for (u8 i = 0; i < 8; i++) lcd_write_col(batt[i]); // No flush needed — pixels are already in the LCD. } ``` **Notes for the rewrite:** - Keep to the (page, x-pixel) model. A "row" is 2 pages (16 px); place text rows at pages 0,2,4,6. - For flicker-free full-screen redraws, either repaint only changed regions (stock approach) or keep your own `u8 fb[8][128]` SRAM shadow and blit it with `blit_rect @0x08008D62`. - Use `mode=1` for the selected item; `mode=0` otherwise; `mode=2` for outlined labels. - Cyrillic/CJK: put raw GB2312 double-byte codes (lead ≥0x80) in the string; `draw_string` auto-routes them through `gbk_blit`. No firmware change needed (see Russification report). - All of this is **display-only** and reuses `spi_flash_read` solely for font fetches — it never writes the SPI codeplug regions and never touches the CPS serial protocol, satisfying the hard constraint. --- ## 7. Confidence & open items - **High confidence:** `draw_string`, `ascii_blit`, `gbk_blit`, `gbk_char_at`, `lcd_set_pos`, `lcd_write_col`, `spi_flash_read`, `mode` semantics, font bases, char metrics, 128×64/8-page geometry, no-framebuffer/direct-GDDRAM model, brightness PWM. All verified from disassembly + literal pools + call sites. - **Medium confidence:** exact controller part number (ST7565/UC1701/SSD1306-family inferred from page addressing + `0xB7` column origin, not read from an ID); the precise GPIO pin numbers for CLK/DATA/RS/CS on the Artery clone (register offsets are certain; silicon pin mapping is not load-bearing). `big_char`/`draw_number_row` font-base literal not fully resolved (only the 33-byte stride is confirmed). - **Not needed for the rewrite:** the boot-time panel command-init sequence (reused as-is via the stock boot); re-deriving it would only matter for a from-scratch panel bring-up.