# 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<r4` |
| **0x08008DAA** | draw_box | `void draw_box(u8 page_origin)` | Draws a **rounded-rectangle frame** (corners 0xF8/0x1F, edges 0xFF) spanning pages `origin`..`origin+6`. Menu/popup border. | corner bytes 0xF8/0x1F, edges 0xFF |
| 0x08008D24 | blit_cols | `void blit_cols(u8 page, u8 count, const u8 *cols, bool src)` | Streams `count` raw column-bytes from `cols[]` at `page` (or zeros if `src==0`) — **generic 1-page bitmap blit / clear**. | set_pos(page), loop write_col(buf[i]) |
| 0x08008D62 | blit_rect | `void blit_rect(u8 page, u8 x, u8 pages, u8 width, const u8 *bmp /*[sp]*/)` | Streams a `pages`×`width` raw bitmap (row-major `bmp[page*width+col]`) — **multi-page bitmap/icon blit**. Use this to blit a shadow framebuffer. | `mla r0,r4,r5,r6` addressing, nested page/col loops |
| 0x0800836C | draw_logo48 (`0x080086CC`) | `void draw_logo(void)` | Draws the **48px×48px boot logo** (6 pages × 0x30 cols) from an in-flash bitmap table. | `#0x58` col base, `r4<6`, `r5<0x30` |
| 0x08008874 | draw_icon_batt | `void draw_icon(u8 page, bool on)` | Draws the battery/side icon at col `0x7B` (123). Status-bar icon. | col `r4+0x7B`, 8B table |
| 0x08008A04 | draw_icon_signal | `void draw_icon(u8 page, bool on)` | Draws a status icon at col `0x15` (21) — signal/antenna glyph. | col `0x15+r4` |
| 0x08008300 | draw_dots | `void draw_dots(u8 which)` | Draws 1–3 dotted markers (byte 0xBD) — page/step indicator dots. | `0xBD` at sp[4..b], cases 1/2/3 |
| 0x08008224 | draw_frame_corners | `void (u8 x, u8 page)` | Draws corner pixels of a highlight frame (2-px inset box). | `rsb #1`, write_col(0) corner pattern |
| 0x08008100 | big_char | `void big_char(u8 page,u8 x,u8 ch,u8 mode)` | Large-font glyph: reads **0x21 (33) bytes** from `0x?+(ch−0x20)*0x20` — a taller/wider font variant (frequency display digits). | `subs #0x20`, `*0x20`, 0x21B read, invert mode |
| 0x080081A0 | set_pixel_run | pixel/segment helper | Builds a partial column mask (`0x80>>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.
