Files
viktorиClaude Opus 4.8 ae36c3b729 RT-4D: реверс прошивки, русификация, кастомный UI, флешеры
- Полный RE стока V3.25 (Cortex-M4F) + FM100B: карта памяти, протокол, codeplug, UI-архитектура
- Русификация: свой CP1251-шрифт + патч рендера, перевод меню и надписей, ребренд Ru-4D V3.25
- Блюпринт переделки UI + C-тулчейн (clang thumbv7em), доказан инъекцией
- Готовые флешеры: WebSerial .html и Windows .exe со вшитой прошивкой
- Дамп SPI рации, стоковая прошивка, инструменты сборки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:47:22 +09:00

278 строки
19 KiB
Markdown
Исходник Постоянная ссылка Ответственный История

Этот файл содержит неоднозначные символы Юникода
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 + (ch0x20)*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=lead0x81, col=trail0x40, 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 `(ch0x20)*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?+(ch0x20)*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 + (ch0x20)*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.