Files
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

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

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)

// @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) @0x08014C34level 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:

// @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):

// ---- 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.