Quansheng UV-K5 · Volume 8

Quansheng UV-K5 Volume 8 — Building from Source

The ARM GCC toolchain and GNU Make, cloning egzumer / F4HWN, the compile-time feature flags, producing an MCU-specific binary, the Docker path, and where to start rolling your own mod

8.1 About this Volume

Downloading a release .bin (Volume 6) gets a radio running someone else’s build. Building from source is the step that turns the UV-K5 from an appliance into a platform: it lets you choose exactly which features compile in, trim the image to fit the flash budget of your generation, and — the real payoff — modify the firmware and flash your own changes. The toolchain is small and standard: ARM GCC plus GNU Make, the same bare-metal Cortex-M toolchain used across embedded work, with a short Python packing step at the end.

The two forks a builder will actually clone are egzumer (github.com/egzumer/uv-k5-firmware-custom) and F4HWN (github.com/armel/uv-k5-firmware-custom), with fagci’s original spectrum-mod repo (github.com/fagci/uv-k5-firmware-fagci-mod) as the historical root of the spectrum lineage. See Volume 4 for the fork comparison that informs which to start from, and Volume 2 for why the resulting binary is MCU-generation-specific — the single most important build-time fact, revisited here as a target-selection step (§4, §6).

This volume stops short of reproducing proprietary bootloader internals; it covers the open application build, which is all a modder needs.

8.2 The toolchain — ARM GCC and GNU Make

The UV-K5 firmware is C targeting an ARM Cortex-M0+ class core (Volume 2). The build needs four things:

Table 1 — The UV-K5 firmware is C targeting an ARM Cortex-M0+ class core (Volume 2). The build needs four things

ComponentPackage / sourceRole
arm-none-eabi-gccgcc-arm-none-eabi + binutils-arm-none-eabi (distro) or ARM’s own toolchain downloadCross-compiler + linker for the Cortex-M target
GNU MakemakeDrives the Makefile that compiles + links + packs
Python 3python3Runs the fork’s packing script that emits firmware.packed.bin
gitgitClone the fork; track your own changes
# Debian/Ubuntu
sudo apt update
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi make python3 git

# Verify the cross-compiler is on PATH and targets the right core
arm-none-eabi-gcc --version

Note — toolchain version can matter for flash fit. These builds live inside a tight flash budget (Volume 2), and different arm-none-eabi-gcc versions emit slightly different code sizes. A feature set that just fits with one toolchain version can overflow with another. If a link step fails with a “region overflowed” error (§10), the toolchain version is one variable; the feature flags (§5) are the other. The Docker path (§7) pins a known-good toolchain so this never surprises you.

8.3 Cloning a fork

Start from the fork whose feature set and generation support match your radio (Volume 4 compares them; Volume 3 has the supported-radios table).

# egzumer — the frozen v0.22 lineage, the most-installed V1/DP32G030 base.
git clone https://github.com/egzumer/uv-k5-firmware-custom.git
cd uv-k5-firmware-custom
git checkout v0.22        # pin the frozen release tag rather than building a moving HEAD

# F4HWN "Fusion" — actively maintained; the V3/PY32F071 + UV-K1 flagship.
git clone https://github.com/armel/uv-k5-firmware-custom.git f4hwn
cd f4hwn
git checkout <tag>        # e.g. the v5.9.0 release tag for the current Fusion build

# fagci — the original spectrum-analyzer mod; the historical root of the lineage.
git clone https://github.com/fagci/uv-k5-firmware-fagci-mod.git

Callout — pin a tag, don’t build HEAD blindly. egzumer is frozen (v0.22, 2024-01-28), so its HEAD is stable, but F4HWN moves. Checking out a release tag means you are building the exact source behind a known, tested release — which also means the version string the radio reports after flashing (Volume 6 §2) matches a real release you can compare against. Building an untagged HEAD is fine for experimenting, but note it in your records so a later “which build is this?” has an answer.

8.4 The build system and the MCU target

The root Makefile compiles the C sources, links against the fork’s linker script for the Cortex-M target, and invokes the Python packing step. The build fact that governs everything else: the image is specific to the MCU generation, because flash size and some peripheral details differ across DP32G030 (V1), PY32F030 (V2), and PY32F071 (V3). A binary built for one generation does not run on another (Volume 2; the flashing consequence is Volume 6 §4).

How a fork exposes target selection differs, so read the fork’s README:

Table 2 — How a fork exposes target selection differs, so read the fork's README

ForkGeneration coverageHow the target is chosen
egzumer v0.22Primarily V1 / DP32G030Single primary target; build as-is for DP32G030
F4HWN “Fusion”V3 / PY32F071 (+ UV-K1), with the fork’s build script producing the generation variants it supportsPer the fork’s build script/README — a build variable or a dedicated build command selects the generation
fagci modV1 / DP32G030 historicalSingle primary target

Hard rule — build for the generation you will flash. Determine the radio’s generation first (Volume 6 §3), then build the matching target. Producing a V1/DP32G030 image and flashing it to a V3/PY32F071 radio soft-bricks it exactly as a mismatched download would. The build does not know which radio you own; you do, so set the target deliberately.

8.5 Compile-time options — the feature flags

The forks expose features as ENABLE_* Make variables. Setting them on the make command line (or editing their defaults) chooses what compiles in. This is where the flash budget bites: on the smaller V1/DP32G030 image you cannot have everything at once, and some features are effectively mutually exclusive once the image fills. The exact set and names vary by fork and version — the fork README/Makefile is authoritative — but the common, load-bearing ones:

Table 3 — The forks expose features as ENABLE Make variables. Setting them on the make command line (or editing their defaults) chooses what compiles in. This is where the flash budget bites: on the smaller V1/DP32G030 image you cannot have everything at once, and some features are effectively mutually exclusive once the image fills. The exact set and names vary by fork and version — the fork README/Makefile is authoritative — but the common, load-bearing ones

Flag (representative)EffectNotes
ENABLE_SPECTRUMThe spectrum-analyzer / scope view (Volume 5)The signature mod; costs flash
ENABLE_AM_FIXAGC workaround for AM-mode overload distortion (Volume 5)Near-essential for usable airband AM
ENABLE_SSBSSB demodulation (where the fork implements it)Not on every fork/generation; flash-hungry
ENABLE_WIDE_RXExtend the tunable RX range (Volume 5)RX unlock; front-end still limits real performance (Volume 9)
ENABLE_FMRADIOBroadcast-FM receive via the board’s FM chipSeparate receiver path from the main SoC
ENABLE_NOAANOAA weather-channel supportRegional relevance
ENABLE_VOXVoice-operated TX
ENABLE_AIRCOPYRadio-to-radio over-the-air config copy
ENABLE_TX17501750 Hz repeater tone burstEU repeater access
ENABLE_FLASHLIGHTDrive the white LED as a torch
ENABLE_RSSI_BAR / ENABLE_AUDIO_BAROn-screen signal / audio metersUI polish, small cost
ENABLE_FASTER_CHANNEL_SCANQuicker scan step timing
# Conceptual: a feature set is a list of ENABLE_* assignments passed to make.
# The combination must fit the generation's flash budget, or the link overflows (§10).
make ENABLE_SPECTRUM=1 ENABLE_AM_FIX=1 ENABLE_FMRADIO=1 ENABLE_WIDE_RX=1

Note — the flash budget is the real constraint. On the small V1/DP32G030 image, enabling spectrum + SSB + wide-RX + FM broadcast + every meter and tone can overflow flash at link time. Building your own image is largely the art of choosing which features matter for your use and leaving the rest off. The larger V3/PY32F071 image (Volume 2) has more headroom, which is part of why the current flagship fork lives there.

8.6 Producing an MCU-specific binary

A successful build emits the same two files Volume 6 §2 described, via the fork’s Python packing step:

make / build script
      │
      ├─ arm-none-eabi-gcc  → compile .c → .o
      ├─ arm-none-eabi-ld   → link against the target's linker script
      ├─ objcopy            → firmware.bin   (raw application image)
      └─ python fw-pack step → firmware.packed.bin
                               (obfuscation + version-string header the bootloader expects)

Table 4 — 6. Producing an MCU-specific binary

OutputUse
firmware.binRaw image — for emulators, disassembly, size inspection (§9)
firmware.packed.binThe flashable image — feed to k5prog -F / verbatim web-flashers (Volume 6)

The packing step stamps a version string into firmware.packed.bin. Setting that string to something you recognise (the fork’s build scripts usually let you) pays off later: the radio echoes it during flashing (Volume 6 §2) and shows it on the boot screen, so “which of my builds is on this radio?” always has an answer.

Callout — size the image before you flash. After a build, check the reported flash usage against the generation’s budget. If it fits comfortably you are done; if it is near the ceiling, trimming a flag or two (§5) leaves room and reduces the chance a future toolchain bump overflows the link. arm-none-eabi-size firmware.elf (or the build’s own size report) is the quick check.

8.7 Building in Docker

Both active forks ship a container path so you do not have to install the toolchain locally — which also pins the exact toolchain version and sidesteps the “fits with one gcc, overflows with another” trap (§2). This is the recommended path for a first build and for reproducibility.

# Conceptual Docker build (exact script name per the fork README — e.g. a
# compile-with-docker helper or a provided Dockerfile).
cd uv-k5-firmware-custom

# Build the image once:
docker build -t uvk5-build .

# Compile inside the container, mounting the repo so outputs land on the host:
docker run --rm -v "$PWD:/app" uvk5-build \
    make ENABLE_SPECTRUM=1 ENABLE_AM_FIX=1 ENABLE_FMRADIO=1

# firmware.bin and firmware.packed.bin appear in the repo directory on the host.

Table 5 — firmware.bin and firmware.packed.bin appear in the repo directory on the host.

PathWhen to use
DockerFirst build; reproducibility; CI; avoiding a local toolchain install; pinning gcc version
Native toolchainFast iterative edit-compile loops when rolling your own mod (§9); no container overhead

Note — the container builds, the host flashes. The Docker container compiles the image, but flashing (Volume 6) still happens from the host against the physical cable/radio. The container has no business touching /dev/ttyUSB0; mount the repo, build, then flash the produced .packed.bin from the host with k5prog or a web-flasher.

8.8 A realistic build-command block

End-to-end, native toolchain, V1/DP32G030 example with a sensible feature set:

# 1. Toolchain (once)
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi make python3 git

# 2. Clone + pin a known release
git clone https://github.com/egzumer/uv-k5-firmware-custom.git
cd uv-k5-firmware-custom
git checkout v0.22

# 3. Clean, then build a chosen feature set (must fit the V1 flash budget)
make clean
make \
    ENABLE_SPECTRUM=1 \
    ENABLE_AM_FIX=1 \
    ENABLE_FMRADIO=1 \
    ENABLE_WIDE_RX=1 \
    ENABLE_RSSI_BAR=1

# 4. Confirm the outputs and check size
ls -l firmware.bin firmware.packed.bin
arm-none-eabi-size firmware.elf   # or read the build's own size report

# 5. Flash the PACKED image (radio in bootloader mode — Volume 6 §5/§6)
#    (from the host; cd to your k5prog checkout or use its path)
k5prog -p /dev/ttyUSB0 -F firmware.packed.bin

# 6. Power-cycle; confirm the boot-screen version string matches this build

Callout — iterate in a loop. The edit → make → flash → observe loop is fast once set up: a code change, a make (seconds), a 10-second flash, a power-cycle. Keep a one-line shell function that builds-and-flashes so a mod experiment (§9) is a single command. Just hold the generation constant — you are flashing the same physical radio each loop, so its generation never changes.

8.9 Rolling your own mod — where to start

The firmware is readable C organised around the radio’s subsystems. A productive first mod is a small, observable change — not a new DSP feature — so you confirm the whole toolchain and flash loop works before investing in anything hard.

Typical source-tree shape (names vary by fork):
  app/        high-level app logic: menus, modes, scanning, the spectrum view
  driver/     peripheral drivers: BK4819 RF SoC, EEPROM, display, keypad, backlight
  ui/         screen rendering
  radio.c     channel/VFO/radio-state model
  settings.c  EEPROM-backed settings (the region Volume 7 §4 maps)
  Makefile    the ENABLE_* flags (§5) and build rules

A sensible on-ramp:

  1. Change something visible. The boot message, a menu label, a default value, or an on-screen meter’s position — build, flash, see it change. This proves the loop end to end.
  2. Add a toggle behind a flag. Wrap your change in a new ENABLE_MYMOD guard in the Makefile so it is opt-in and doesn’t bloat other builds — and so it respects the flash budget.
  3. Touch the BK4819 driver last. The RF SoC driver (Volume 2 covers the chip; Volume 5 covers the RX unlocks it enables) is where the powerful mods live — spectrum, AM-fix, wide RX — and also where mistakes are hardest to debug. Read how the existing spectrum/AM-fix code drives the chip before writing your own register pokes.
  4. Keep diffs small and in git. Commit each working step. A soft-brick from your own code is recovered exactly like any bad flash (Volume 6 §8); a small, committed diff tells you which change caused it.

Note — the open surface is the application, not the bootloader. Everything above the bootloader is open and modifiable; the bootloader itself is Quansheng’s and is the recovery path you rely on (Volume 6 §8). Mod the application freely — the worst case is a soft-brick and a re-flash — and leave the bootloader alone.

8.10 Pitfalls and gotchas

Table 6 — 10. Pitfalls and gotchas

PitfallSymptomFix
Toolchain not on PATHarm-none-eabi-gcc: command not foundInstall gcc-arm-none-eabi; or use the Docker path (§7)
Flash region overflowLink error: region FLASH/ROM overflowed by N bytesDisable feature flags (§5); the V1 image can’t hold everything; or move to a V3 build
Wrong-generation targetBuilds fine, soft-bricks on flashBuild the target matching the radio’s generation (§4, §6); verify generation first (Vol 6 §3)
Built HEAD, can’t reproduce”Which build is this?” unanswerablePin a release tag (§3); set a recognisable version string (§6)
Missing Python for packingfirmware.bin produced but no .packed.binInstall python3; the packing step emits the flashable image
Toolchain version driftFits on one machine, overflows on anotherUse Docker to pin gcc (§7); note the toolchain version with the build
Flashed the raw .binBootloader rejects the imageFlash firmware.packed.bin, not firmware.bin (§6; Vol 6 §2)
Edited BK4819 driver firstHard-to-debug RF misbehaviourStart with a visible UI change (§9); read existing RF code before writing register pokes

See Volume 6 to flash what you built, Volume 5 for the RX-unlock features behind the flags, Volume 4 for the fork comparison behind the clone choice, and Volume 2 for why the binary is generation-specific.

Comments (0)

  1. Loading…

Comments are held for moderation — nothing appears until approved.