Баузбенд 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
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 withlcd_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 byspi_flash_read. The single most important callable isdraw_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+0x18andbase+0x28(used asstr val,[port]andstr val,[port+0x10]after the code loadsport+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 @0x08021C5Care 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 viatim_set_ccr @0x08021C14andtim_set_arr_psc @0x08021C04.lcd_set_brightness(level) @0x08014C34—level0..4 → duty {0, 5, 0x1E, 0x64, 0xFF}. Called from0x08009204.backlight_on/init @0x08014BECand@0x08014C0C— enable pin + set duty.
- Panel reset / power pins are toggled in
lcd_power_seq @0x0801D938(writes GPIOB+0x18/+0x28BSRR, runs a PWM ramp viapwm_cfg @0x0802129C, delays viadelay_ms @0x08007946). - The panel command init sequence (contrast/segment-remap/display-on) is executed once at boot
through the same
lcd_send_cmdpath; it does not need to be re-issued by a UI rewrite that keeps the stock boot. (Reusinglcd_set_pos/lcd_write_colafter 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.
pagegrows downward,x/colgrows rightward. page(row) unit: one page = 8 vertical pixels. Valid 0..7. Passed asr0to blitters.x(column) unit: pixels, 0..127. Passed asr1.lcd_set_pos(page, col)mapscolto the controller command0xB7 − col(window offset) andpagevia0x10 | (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_stringauto-wraps to next 2-page cell whenxpasses ~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
lenbytes intodst. Bit-banged on GPIOB (0x40020400region, literals0x8021820..0x8021AD8). 34 direct BL callers — the glyph blitters are the heavy users. - Font bases: ASCII narrow
0x19C000(14 B/glyph), GBK/wide0x19E000(28 B/glyph). Cyrillic is present in the GBK bank at GB2312 row A7 (seeRT-4D_Russification.md). A separate Unicode codepoint index lives at SPI0x3F0000. - 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_readas-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 withblit_rect @0x08008D62. - Use
mode=1for the selected item;mode=0otherwise;mode=2for outlined labels. - Cyrillic/CJK: put raw GB2312 double-byte codes (lead ≥0x80) in the string;
draw_stringauto-routes them throughgbk_blit. No firmware change needed (see Russification report). - All of this is display-only and reuses
spi_flash_readsolely 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,modesemantics, 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 +
0xB7column 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_rowfont-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.