MD-380: hosting the AMBE+2 codec
The merge moves the code to sector 11 (Hosting); the placed layout is checked on a host.
Rule: the radio runs exactly the codec its own TYT firmware shipped. The user's machine extracts it from a firmware file the user supplies. No codec bytes go in this repository or in released binaries.
Firmware files
| version | file | size | sha256 | code window |
|---|---|---|---|---|
| D002.032 (the only supported one) | MD-380-D2.32(AD).bin (TYT "Firmware 2.32", model string "JST51") |
994816 (0xF2E00) | e1a1a11a84d9afd2d2ceed61be8395e32a8034785abd0be0236478f7d8609f43 |
0x08050C44 |
Other versions (for example the D13.20 / S13.20 family) sit at other addresses and need their own profile.
Container and encryption
The file is a TYT "OutSecurityBin" container in its single-segment form (hardware-md380.md 14.1): segment count word 0xFFFFFFFF at 0x7C, one segment (0x0800C000, 0xF2C00) at 0x80, the payload at 0x100, then 0x100 trailing bytes. The payload is XORed with the MD-380's 1024-byte bootloader key:
plain[i] = file[0x100 + i] ^ key[i % 1024]
So for an application address A, file offset = A - 0x0800C000 + 0x100 and the key index = (A - 0x0800C000) % 1024. The decrypted image has SP 0x2001DC10 and reset vector 0x080FA969.
Key recovery
The key (SHA-256 5254139c6b596d9413d106e6b7c0685ec245d69555cb28df98f061d6b879c4cd, first bytes 2E DF 40 B5) was
derived from the firmware file alone, checked by that hash. The plain column vote (most common cipher byte per key
column) is wrong in 352 of the 1024 columns: the image is far less zero-dominated than the DM-1701's (0x00 is only
12 % of the plain bytes). In every column the true key byte is among the 8 most common cipher bytes, though.
What converges is a byte-pair refinement trained on the file itself: plain firmware byte pairs (Thumb-2 halfwords,
little-endian addresses) are strongly skewed, with even and odd offsets distinct. Learn a pair model from the 40 % of
columns with the clearest vote, re-pick every other column among its 8 vote candidates, then repeat with the model
learned from the whole image until nothing changes. Two rounds give the exact key. rfoundry/stockfw.derive_key does
this when the plain vote fails (about 5 s in Python), so neither rfoundry nor tools/codec_extract.py needs a key file
for the MD-380. The key file is never stored in the repository.
Codec in the D002.032 file
| window | flash | length | file offset | key phase | sha256 (decrypted) | CRC-32 |
|---|---|---|---|---|---|---|
| code | 0x08050C44..0x080614C8 | 0x10884 | 0x44D44 | 0x044 | 00d9565dc459c47119690e47c35daf9ae1a89c13bb3513d966390e0835b0c0ad |
0x32F0A7C1 |
| tables | 0x0808FC64..0x0809AC5C | 0xAFF8 | 0x83D64 | 0x064 | 9c7b66b08a6d397565c70551df9097870757f8de719b8d31525eba72e1db4840 |
0x91E708C1 |
| small table 1 | 0x080D0FC8..0x080D0FEC | 0x24 | 0xC50C8 | 0x3C8 | d69f60c456d316fd4506d66f706a093a172792e97f21a7d4eb69b94cbf91a1d2 |
0x7AAFE1D3 |
| small table 2 | 0x080F9698..0x080F96B0 | 0x18 | 0xED798 | 0x298 | 6679872ccda55797e8d51692e9a59bcc0ac2c87eaff31736302976be446a6375 |
0x33E82E70 |
| small table 3 | 0x080F9E74..0x080F9E88 | 0x14 | 0xEDF74 | 0x274 | 0af022f016edcb28a3b29d60abdf42846456f22b8b4579a14b2069c7f6f31017 |
0xFFF0E930 |
| small table 4 | 0x080FD490..0x080FD4B0 | 0x20 | 0xF1590 | 0x090 | b6bc2f43d9a7779d7a8ac938db131b049f72620bc6d288db19bd9ecc15a8c812 |
0xDA18C5EF |
First word of the code window: 0x4FF0E92D (push.w {r4-r11, lr}), as on the DM-1701.
What the windows hold:
- Code window. The codec library from 0x08050C44, and at its end the compiler's 64-bit shift helpers (0x08061480..0x080614C6) that the codec calls. 0x08051C28..0x08053538 inside it is non-codec code (calls out, a CRC routine); the codec never executes it, and it is carried only to keep the window contiguous.
- Table window. The codec's constant tables, plus two short codec code islands placed among them by the linker, as on the DM-1701 (whose table window holds a codec function at 0x080D5A80): 0x08092954..0x080932EE (reached through a code pointer at 0x08055034 that holds 0x08092A4D, and calling back into the code window with BL) and 0x080963C4..0x0809647C (a Golay (23,12) encoder and decoder). The rest between the tables is other data and code (for example a flash sector address table at 0x08095B70); the codec doesn't read it. The window ends with the last table (0x0809AC34, 40 bytes).
- Small tables. Constants the compiler merged into other read-only data: 0x080D0FCA (6 halfwords), 0x080D0FD8 and 0x080D0FE4 (FEC decode); 0x080F9698 (12 halfwords, encode ECC); 0x080F9E74 (10 halfwords); 0x080FD490..0x080FD4AB (8 + 8 + 8 + 4 bytes). Each window is cut to whole tables (the DM-1701 image holds the same bytes inside its single table window). The codec faults without each of them.
Entry points and calling convention
All AAPCS, Thumb, arguments beyond four on the stack in order, results in r0. Same library as the DM-1701's: the prologues, argument lists and stack argument offsets match; only the addresses differ.
| call | address | offset from 0x08050C44 | DM-1701 offset | arguments |
|---|---|---|---|---|
| encode | 0x08050D90 | +0x14C | +0x130 | (bits out, 0, PCM in, 80, stack: flags, half, 0x2000, E) |
| encoder set | 0x08050E24 | +0x1E0 | +0x1C0 | (E, 128, -20) |
| encoder init | 0x08050ED8 | +0x294 | +0x284 | (E, 1, 1) |
| FEC init | 0x08051140 | +0x4FC | +0x4CC | (F, 1) |
| encode ECC | 0x08051158 | +0x514 | +0x4E8 | (bit buffer, same buffer, 0, F_enc): 49 bits in, 72 air bits out |
| decode | 0x08051248 | +0x604 | +0x5D8 | (PCM out, 80, bits in, 0, stack: flags, half, D) |
| decoder set | 0x080512F0 | +0x6AC | +0x688 | (D, 128, -20) |
| decoder init | 0x08051334 | +0x6F0 | +0x6D0 | (D, 1) |
| encoder parameters | 0x08051498 | +0x854 | +0x848 | (E, -22, 20, -10, stack: -40, 0x4000, 3276, 3276, 1310, 10, 20) |
| FEC decode | 0x0805170C | +0xAC8 | +0xB54 | (49 bits out, 0, 72 bits in, 1, stack: flags, F_dec): returns 0 good, 4 C0 uncorrectable |
Bits are one per halfword in all buffers, the frame is two calls of 80 samples (half 0, then half 1). Flags: encode see Encode flags; decode 0 for a good frame, 4 (repeat the previous frame) or 8. These are the DM-1701's semantics (codec-dm1701.md, "Per-frame calls").
Encode flags
The encode call's flags word (stack argument 0) is a set of feature bits, not a "first frame" marker. Measured on the DM-1801 Pro build and checked bit for bit against this MD-380 build; the DM-32UV runs the same build as the DM-1801 Pro.
| Bit | Effect |
|---|---|
| 0x0040 | Noise suppression. Noise alone: 2130 -> 486 RMS. Between words in noisy speech: 1611 -> 522. It also lowers steady tones |
| 0x0100 | Turns the input DC-removal filter off. Never set |
| 0x0800 | Noise gate. Acts only while E[0x138A] is not 0 |
| 0x1000 | Tone detection: a steady tone or tone pair is sent as an AMBE tone frame and is decoded at the right frequency. No effect on speech |
| 0x2000 | Input AGC: quiet speech about 4x louder, loud speech reduced. Its parameters are E+0x16A2..0x16B6, left at their values after init. The defaults have not been tested on air |
| 0x4000 | Test mode: one fixed frame whatever the input |
| others | No measurable effect |
Radios use one of two patterns, each a constant value on every call of a transmission: 0x800, or 0x1840 (0x1000 | 0x800 | 0x40). A word that changes from call to call is not one of them. A host that passes 0x1840 on the first call and 0x800 afterwards gets the 0x800 behavior: over 234 frames the output equals 0x800 on every call.
What RadioFoundry passes (drivers/vocoder/ambe2_flags.h, used by the MD-380 and DM-1701 hosts and by
platform/hrc7000/vocoder.c for the DM-1801 Pro and DM-32UV): one word on every call, 0x800 plus 0x40 for
VOCODER_OPT_NOISE_REDUCTION, 0x1000 for VOCODER_OPT_TONE_FRAMES and 0x2000 for VOCODER_OPT_AGC. Noise
reduction and tone frames are on by default and AGC is off, which gives 0x1840 (0x3840 with Mic AGC on). 0x100 and
0x4000 are never set (dmr-voice.md 6).
Cross-check with the 0x2000 bit: with the same values in the AGC parameter block, all 43 tested flag words give the same output on the two builds. The difference seen earlier with 0x2000 set came from different values in that block (state), not from different code.
Init
The encoder block E has its FEC state at E+0x16E4 and the decoder block D has its FEC state at D+0x7F8: the same offsets as the DM-1701's. Store the constants (0x1840, 0x2000, -22, 20, -10, -40, 0x4000, 3276, 3276, 1310, 10, 20, 128, -20) and then calls encoder init (E, 1, 1), FEC init (E+0x16E4, 1), decoder init (D, 1), FEC init (D+0x7F8, 1), encoder parameters, encoder set (E, 128, -20), decoder set (D, 128, -20), and one decode of an all-zero frame with flags 8. RadioFoundry runs the same sequence on its own blocks.
State sizes (measured)
| block | size | notes |
|---|---|---|
| decoder state D | 0x7EC bytes | 0x7E8 faults (decoder init) |
| encoder state E | 0x16E0 bytes | 0x16DC faults (encoder init) |
| FEC state | 0x44 bytes each | 0x40 faults; only for the codec's own FEC routines |
| stack | 4046 bytes peak from the entry | 4234 through RadioFoundry's call veneer and vocoder; allow 4.5 KB plus the caller |
The blocks are passed by pointer and can live anywhere. No fixed RAM address is touched: with only the windows, the state blocks, the buffers and a stack mapped, init and 300 frames of encode, ECC, FEC decode (with 0 to 4 injected bit errors per frame, random frames, flags 8) and decode ran without a fault. The executed code contains no FPU instruction.
Position dependence
- Calls inside the codec are PC-relative, but they cross between the code window and the code islands in the table window (BL from 0x08092E18 and others to 0x08056xxx), and the encoder reaches 0x08092A4C through an absolute code pointer. The code also reaches its tables through 49 distinct absolute literals at 70 sites, into the table window and all four small windows.
- The literals and the code pointer point into the table windows, so they stay valid while the tables stay at their
original addresses. Moving the code alone needs two kinds of rewrite: the two movw/movt pairs in the code that point
into the code, and the 12 calls (BL) from the islands back into the code. The codec map
(
tools/rfoundry/rfoundry/codec_maps/md380/md380-d2032.json) lists both, and the merge applies them (codec-placement.md, "STM32 radios").
Hosting
Run in place from flash, no copy. The merge moves the code to the start of sector 11 and leaves every table window at its original address (codec-placement.md, "STM32 radios").
| window | placed at | length | CRC-32 as placed |
|---|---|---|---|
| code | 0x080E0000 (sector 11) | 0x10884 | 0x067E6A77 (self references rewritten) |
| tables | 0x0808FC64 (sector 8, original address) | 0xAFF8 | 0x2A5C9EE1 (the 12 island calls re-encoded) |
| small tables 1-4 | original addresses (sectors 10 and 11) | as above | unchanged (0x7AAFE1D3, 0x33E82E70, 0xFFF0E930, 0xDA18C5EF) |
| descriptor | 0x080FFF80 (end of sector 11) | 128 | its own (platform/stm32f4/codec_desc.h), tag 0x2032 |
- Flash map. The application gets sectors 3-7, 0x0800C000..0x0807FFFF, 464 KB in one piece, and
boards/md380/memory.ldasserts that the image ends before 0x08080000. The codec uses sectors 8, 10 and 11. Sector 9 (0x080A0000..0x080BFFFF) holds nothing. The firmware file keeps the code at 0x08050C44, inside sectors 6 and 7, so that layout leaves the application only 275 KB (it stops inside sector 6, where the code starts). - Updates. No codec byte shares a sector with the application, so an update writes sectors 3-7 only and keeps the
codec (
boards/md380/board.toml:keep_codec = true; the STM32 stubstub/f4stub.pyowns sectors 3-7). The codec is merged on a first install from the manufacturer's firmware, or when it can't be kept or checked. It comes from the firmware file, or from the copy the flashing tools keep: rfoundry and the web flasher read the codec from a radio running RadioFoundry over the debug link (READ_MEM), in the original or the placed layout, check it against the profile hashes and save it on the computer (rfoundry's config directory, the browser's IndexedDB;rfoundry/codecsave.py,site/static/js/flasher/codecsave.js). - Boot check (
platform/stm32f4/hrc/vocoder.c, this build's profile inboards/md380/vocoder_md380.c). Once, before the first call: the descriptor (its own CRC, tag 0x2032, the code and every table window at the addresses and with the CRCs above), then the signature word 0x4FF0E92D at 0x080E0000, then the CRC-32 of every window (about 112 KB). Otherwise the vocoder reports no codec and the radio is FM only. Entry points are offsets from the code start (encode +0x14C, decode +0x604, the rest invocoder_md380.c), the same offsets as in the original layout. - Profile id.
md380-d2032-s11(debug link VERSIONcodec=). A build from before the move reportsmd380-d2032, so the tools know the codec isn't where the new layout keeps it and ask for the firmware file. - Calls. Through the generic veneer
platform/stm32f4/codec_call.c, as on the DM-1701, with the init sequence and per-frame calls above. The state blocks live in CCM.
Extraction recipe (D002.032)
- Check the file's SHA-256 (
e1a1a11a...), its size 0xF2E00, and the single-segment table (0x0800C000, 0xF2C00). - Get the key: a key file checked by SHA-256 (
5254139c...), or derive it from the file (above). - Decrypt each of the six windows: file offset = A - 0x0800C000 + 0x100, key index (A - 0x0800C000) % 1024. Check each window's SHA-256 and the code's first word 0x4FF0E92D.
- Write the table windows at their original addresses, with the 12 island calls in the main window re-encoded for the
moved code. Write the code at 0x080E0000 with its two movw/movt self references rewritten, and the descriptor at
0x080FFF80. Check the placed CRC-32s (Hosting). 0xFF fills the rest after the application image. The codec map
drives the rewrites. The merge happens on the user's machine (
makewith the firmware file incodecs/,tools/codec_extract.py, rfoundry or the web flasher), never in a release.
Verification on a host
The windows were run on the host with only the six windows, the state blocks, the I/O buffers and a stack mapped, so any other access faults.
- For a 1 kHz tone (30 frames) and a 300 frame stress run (noise, silence, full scale sweeps, injected bit errors, random frames, flags 8), the MD-380 codec's air bits and decoded PCM are bit-identical to the DM-1701 codec's. Their FEC decode results agree (0 and 4).
- RadioFoundry's own
boards/md380/vocoder_md380.candplatform/stm32f4/codec_call.c, compiled with the firmware flags, were run against the windows of the merged image thatmake BOARD=md380writes, with a deliberately misaligned caller stack.present()passes, and the encoded bits and decoded PCM over 30 frames are bit-identical to calling the codec directly. - The placed codec (code at 0x080E0000, rewrites applied), run with only its placed windows mapped, gives the same air bits and PCM as the original layout over 60 frames, plain and stressed, with the same FEC results and a stack peak of 4046 bytes.
Only a radio can check
- Audio: TX and RX voice quality and levels through the MD-380's own audio path (the I2S format, gain staging).
- Timing: encode and decode within 20 ms at the MD-380's clock, and the audio task's stack (5 KB) under the codec's 4.2 KB plus the audio callback.
- That D002.032's bootloader accepts an image that writes sectors 3-8, 10 and 11 in one session, and that nothing else in flash (for example the bootloader's own checks) cares about the codec sectors.
- Voice TX and RX with the moved code, and an update in keep mode that leaves the codec working.
- Calls against the HR_C5000 path with real frames, including the FEC decode results on air.
Open items
-
On-air check of the encode flags (0x800 against 0x1840): speech in a quiet room and with background noise, a 1 kHz tone and a DTMF key into the mic, received on a second radio or the SDR bench. AGC (0x3840): a quiet and a loud talker, levels and pumping between words; whether the parameter block's values after init are usable.
-
Other firmware versions (D13.20 and similar) need their own profile; only D002.032 is supported.
-
Moving the table windows too (rewriting the
table_refs) would free sector 8 and pack the codec into sector 11 (codec-placement.md). The application has 464 KB and doesn't need it yet.