MD380 firmware overview
('informal' and most likely outdated, by DL4YHF, 2017-04-04)
The first part of this file contains a 'heavily linked' overview of the patches for the MD380 / RT3 firmware.
Anything in this file is an 'interpretation' of DL4YHF, which may be utterly wrong.
The second part contains the description of firmware modifications tried by DL4YHF.
For a complete and up-to-date documentation, build your own (using Doxygen or similar) from the source,
and see what's in the most recent distribution of Travis Goodspeed's MD380 Tools !
The third part contains fragments of information about the original firmware,
with the intention of 'trying to understand' which hardware resources (timers, audio interface, etc)
are still available for own experiments.
The fourth part describes a few of the author's experiments with own additions to the experimental firmware.
See also: RT3/MD-380 hardware, RT3/MD-380 index (by DL4YHF)
The links to the C sources below only work after copying this file into ?/md380tools/docs !
- C sourcecode overview
- Diagnostic functions
- Accessing C5000 registers via USB
- Disassembling parts of the original firmware
- Intro, disassembling with Radare2
- The interrupt vector table
- Processing of analog inputs (volume pot, battery voltage, etc)
- Hooked functions (in the original firmware)
- Own experiments with / additions to the experimental firmware
- Dimmed 'ambient' backlight for the display
- Morse code output (PWM, via speaker)
- An own menu (independent of the original firmware)
- Implementing a simple LCD driver
- Using bitmap fonts from the original firmware
| > Configures our patches, either to temporarily enable
| > unstable features or to disable standard features when
| > porting to a new target application version.
|__applet/inc : 'Library configuration file' by ST. Not very interesting.
| No MD380 specific functions in these stm32_.. header files.
|__applet/lib : STM32F4xx peripheral library from STM. Not very interesting,
| | some say quite bloated. No MD380 specific functions in here.
| |___ startup_stm32f4xx.s interrupt vector table and 'early startup' code
| First calls SystemInit(), then main(), as CMSIS wants us to :o)
|__applet/src : Contains most interesting parts of the patched firmware .
|___ ambe.c AMBE2+ Hook Functions
|___ app_menu.c alternative menu, opened via red BACK button
|___ beep.c beep_process Hook Functions
|___ console.c simple text console for netmon
|___ debug.c debug_printf(), etc
|___ dispflip.c display Hook functions
|___ dispinit.c display_reset(), display_init()
|___ display.c high-level drawing functions
|___ dmesg.c Kernel logging functions and buffers
|___ dmr.c hooks some of the DMR packet handler functions
|___ gfx.c Graphics wrapper functions
|___ i2c.c i2c Hooks and diagnostic functions
|___ irq_handlers.c interrupt handlers, first used for 'dimmed backlight'
|___ keyb.c implements a few hotkey functions
|___ lcd_driver.c Much faster LCD driver for text output
|___ main.c MD380Tools Main Application
|___ mbox.c RTOS mailbox test
|___ menu.c Menu hooks and extensions
|___ netmon.c 'highly volatile' network monitor, controlled via hotkeys
|___ os.c Hook functions for the real time OS
|___ printf.c subroutines for lightweight 'printf'
|___ rtc_timer.c wrapper functions for the "RTC Timer"-Task
|___ spiflash.c SPI-Flash Hook functions
|___ stm32f4xx_it.c default interrupt handlers for STM32F4xx
|___ system_stm32f4xx.c Low-level CPU initialisation
|___ usb.c USB Hook functions, optional C5000 register access via USB, etc
|___ usersdb.c implements the DMR user database, find_dmr(), etc
|___ util.c mkascii(), uli2w(), strhex(), wstrhex()
To use some of these diagnostic functions, some of the macros in config.h must be defined,
for example CONFIG_SPIC5000 to read and (for the brave-hearted) write registers in the C5000 as explained below.
For this feature, CONFIG_SPIC5000 must be defined in config.h .
In the md380tools pulled from Github in 2016-12, this was already the case.
At the time of this writing (2016-12), c5000_spi0_readreg() was still the original code,
for which no C sourcecode existed.
How to access registers inside the HR_C5000 chip is described in the datasheet,
see english translation, chapter 8, 'data read and write'.
In the 'hooked' usb.c : usb_dnld_hook(), c5000_spi0_readreg() and c5000_spi0_writereg() can be called.
After switching to libusb,
this can be achieved via Python script, by calling c5000() in md380_tool.py :
For example, the author wanted to know the 'microphone gain' setting.
The datasheet (partially translated from Chinese to English) says 'MicGain' is a bitgroup
in register 0x0F.
$ cd c:/tools/md380tools
[email protected] /c/tools/md380tools
$ c:/tools/libusb-win32/bin/amd64/testlibusb-win.exe # DL4YHF: for windows, tickle USB
# before accessing USB from Python
[email protected] /c/tools/md380tools
$ python md380_tool.py c5000 # Prints some DMR registers...
[0x00]=0x00 [0x01]=0xb0 [0x02]=0x00 [0x03]=0x00
[0x04]=0x00 [0x05]=0x00 [0x06]=0x21 [0x07]=0x0b
[0x08]=0xb8 [0x09]=0x00 [0x0a]=0x00 [0x0b]=0x28
[0x0c]=0x33 [0x0d]=0x8c [0x0e]=0x44 [0x0f]=0xc8
[0x10]=0x4f [0x11]=0x80 [0x12]=0x0a [0x13]=0x22
[0x14]=0x8d [0x15]=0x0d [0x16]=0x28 [0x17]=0x33
[0x18]=0xef [0x19]=0x00 [0x1a]=0xff [0x1b]=0xff
[0x1c]=0xff [0x1d]=0xff [0x1e]=0xf1 [0x1f]=0x10
[0x20]=0x00 [0x21]=0x00 [0x22]=0x07 [0x23]=0x3b
[0x24]=0xf8 [0x25]=0x3e [0x26]=0xff [0x27]=0x40
[0x28]=0xff [0x29]=0x00 [0x2a]=0x0b [0x2b]=0x00
[0x2c]=0x00 [0x2d]=0x00 [0x2e]=0x04 [0x2f]=0x0b
[0x30]=0x00 [0x31]=0x17 [0x32]=0x02 [0x33]=0xff
[0x34]=0xe0 [0x35]=0x28 [0x36]=0xf0 [0x37]=0xc2
[0x38]=0x00 [0x39]=0x02 [0x3a]=0x00 [0x3b]=0x00
[0x3c]=0x00 [0x3d]=0x0a [0x3e]=0x28 [0x3f]=0x10
[0x40]=0x43 [0x41]=0x40 [0x42]=0x60 [0x43]=0x00
[0x44]=0x22 [0x45]=0x00 [0x46]=0x00 [0x47]=0x22
[0x48]=0x00 [0x49]=0x00 [0x4a]=0x00 [0x4b]=0x00
[0x4c]=0x00 [0x4d]=0x00 [0x4e]=0xf3 [0x4f]=0x09
[0x50]=0x00 [0x51]=0x08 [0x52]=0x01 [0x53]=0x00
[0x54]=0x00 [0x55]=0x00 [0x56]=0x00 [0x57]=0x00
[0x58]=0x00 [0x59]=0x00 [0x5a]=0x00 [0x5b]=0x00
[0x5c]=0x00 [0x5d]=0x00 [0x5e]=0x00 [0x5f]=0x00
[0x60]=0x00 [0x61]=0x33 [0x62]=0xef [0x63]=0x00
[0x64]=0x00 [0x65]=0x0a [0x66]=0x00 [0x67]=0x00
[0x68]=0x00 [0x69]=0x00 [0x6a]=0x00 [0x6b]=0x00
[0x6c]=0x00 [0x6d]=0x20 [0x6e]=0xc0 [0x6f]=0x29
[0x70]=0x00 [0x71]=0x49 [0x72]=0x00 [0x73]=0x00
[0x74]=0x00 [0x75]=0x00 [0x76]=0x00 [0x77]=0x00
[0x78]=0x00 [0x79]=0x00 [0x7a]=0x00 [0x7b]=0x00
[0x7c]=0x00 [0x7d]=0x00 [0x7e]=0x00 [0x7f]=0x00
[0x80]=0x00 [0x81]=0x08 [0x82]=0x00 [0x83]=0x00
[0x84]=0x00 [0x85]=0x00 [0x86]=0x00
The md380-tool (Python script invoked above) showed register 0x0f contained 0xC8 .
0xC8 = binary 1100 1000 (in C5000 register 0x0F)
|||| ||||__ bit 0 : not specified in the HR_C5000 datasheet
|||| |\\___ bits 2..1 = MicVol . 00 = 0 dB (which is the DEFAULT value)
\\\\_\_____ bits 7..4 = ADLinVol. 11001 = (12 - 25*1.5) dB = -25.5 dB
At the time of this writing, it wasn't clear where and why the MD380 firmware modifies
the 5 bits in 'ADLinVol'. The read-out value (ADLinVol=0b11001) is different from the default.
The original firmware possibly initialises this register based on the settings
in the original CPS'es 'Test Mode' screen.
With a modified Python script (tool2.py),
the value in register 0x0F was inspected or overwritten while transmitting.
Surprisingly, the 'MicVol' bits did not affect the voice modulation (neither in FM nor in DMR),
but the 'ADLinVol' bits did. The (original) firmware seems to switch between DTMF ("Beep")
and voice (input from the microphone) by switching bits in register 0x0E.
The patched firmware (2016-12-17) sets...
- register 0x0E to 0x44 when beginning to transmit in FM
- register 0x0E to 0x42 when pressing a numeric digit (to send a DTMF tone, with the microphone MUTED)
- register 0x0E back to 0x44 when releasing the numeric digit (to continue FM voice transmission)
- register 0x0E to 0x4C when beginning to receive DMR voice ("squelch" opens)
- register 0x0E back to 0x44 at the end of the DMR voice reception ("squelch" closes)
- register 0x0E to 0x40 for about 220 milliseconds shortly after beginning to transmit DMR voice (i.e.
after pressing PTT ).
During this short gap, the QSO partner hears 'nothing'. Only noticeable in DMR-simplex, not via repeater.
- register 0x0E back from 0x40 to 0x44 during the rest of the DMR transmission (when the QSO partner can hear the voice).
0x44 = binary 0100 0100 (in C5000 register 0x0E, while idle, or receiving FM, or sending FM or DMR voice except the 'gap')
|||| ||||__ bit 0 : "Switch1" - purpose unknown by the time of this writing (2016-12)
|||| |||___ bit 1 : "Mic2 enable". Here: 0 when transmitting voice, 1 when sending DTMF in FM
.|.. ||____ bit 2 : "Mic1 enable". Here: 1 when sending voice
| |_____ bit 3 : "LineOutEnable. Here: 0, since we don't want to hear anything in the speaker.
|_________ bit 6 : "HPMute". Seems to be always set to mute the (unused?) headphone output.
0x42 = binary 0100 0010 (in C5000 register 0x0E, while sending DTMF in FM )
|||| ||___ bit 1 : "Mic2 enable". Here: 1 when sending DTMF in FM
.... |____ bit 2 : "Mic1 enable". Here: 0 to disable the microphone
0x4C = binary 0100 1100 (in C5000 register 0x0E, while receiving DMR voice, with the "squelch open")
|||| |||___ bit 1 : "Mic2 enable". Here: 0 when transmitting voice, 1 when sending DTMF in FM
.... ||____ bit 2 : "Mic1 enable". Here: 0 when sending DTMF in FM, 1 when sending voice
|_____ bit 3 : "LineOutEnable. Here: 1, to have an audio signal in the speaker.
After 'understanding' C5000 reg 0x0E, what about the 'mic gain' settings via reg 0x0F ?
While transmitting in FM and DMR-simplex, different values were poked into register 0x0F,
and the md380tool's "mic bargraph" was used to adjust the amplitude of a 1 kHz test signal
to let the bargraph just touch the point between 'gray' and 'green area' in the colour legend.
The following voltages were necessary to reach that point:
Value in reg 0x0F | 0xC8 | 0xD8 | 0xE0 | 0xE8 | 0xF0 | 0xF8 |
Voltage at Mic-Input | 12 mVpp | 9 mVpp | 7.2 mVpp | 6 mVpp | no modulation ! |
The reason for not being able to use the highest gain settings (reg_0E: 0xF0 or 0xF8) was unknown.
It may be an improperly biased analog input (similar to an 'overdriven' op-amp), etc.
Anyway, the distance between 0xC8 (=default gain in the stock firmware) and 0xE8 (=highest possible gain
in the author's RT3) is 6 dB. That's worth for being 'tried out' with a modified firmware...
which was later implemented by KG5RKI by overwriting register 0x0F in added in rtc_timer.c : f_4225_hook().
See also: audio signal paths during transmission.
Before adding adding features that use certain hardware resources (for example, PWM-capable timers),
it's necessary to find out if those resources aren't used by the already existing firmware.
In traditional software development for embedded systems, there should be a nicely documented
or 'self-documenting'(?) pile of C sources, which can be extended easily.
In the partially-reverse-engineered MD380 firmware, things are different. Thanks to the effort Travis
and a Merry Band of Reverse Engineers (see PoC||GTFO issue 10, chapter 8),
the hood of The Engine That Runs The World has already been lifted (firmware been read out and unscrambled).
But to understand how a certain part of the engine works, it must be disassembled
(or even decompiled into functions with 'speaking names', which would be a daunting task).
This chapter describes a half-hearted attempt to disassemble parts of the original firmware (the one used for a non-GPS radio, RT3).
Much work has already been done by Travis and contributors, see ?/md380tools/annotations/d13.020 for example.
- find out which hardware resources were used (timers, PWMs, etc)
- get a glance at the original interrupt service handlers.
The disassembler used for this purpose is not the old-familiar 'objdump' (for Cortex-M, arm-none-eabi-objdump)
but a utility named 'Radare2'. Even though being familar with ARM/Cortex-M development (but only "forward engineering"),
it took some time to get used to it. Radare2 can be installed from https://github.com/radare/radare2.
> Radare is an open source reversing framework. It comes with a ton of options,
> functionality, and a somewhat daunting learning curve. (..)
There at least three very different ebooks on Radare/Radare2.
For a beginner (in Radare) like myself, 'radare2-explorations.pdf' was the easiest to read.
'radare2book.pdf' fills the gap (read at least 'Basic Radare Usage' in it).
Radare commands are composed of single characters (!), like seek, print, alterate/analyse;
often followed by more characters, like afl ("list all functions", with addresses, flags, sizes, and names).
Radare2 was installed on the windows 'workhorse' under c:/tools/Radare2.
Next, loosely based on github.com/travisgoodspeed/md380tools/blob/master/docs/windows.md:
( "flash.img" had been copied and renamed from firmware/unwrapped/D013.020.img)
c:\tools\md380tools\annotations\d13.020>radare2.exe -a arm -m 0x0800C000 -b 16 -i flash.r flash.img
To produce a half-way readable "listing" of the original firmware, with a few annotations of function names and registers,
a Radare2 script was stitched together based on the already existing files (flash.r, cpu.r).
The result was saved as md380tools/annotations/d13.020/disasm_yhf.r .
A copy of the modified Radare2 script may still be available at DL4YHF's website ( disasm_yhf.r ).
The output (disassembly listing) contains most exception- and interrupt handlers.
For very curious readers (familiar with Cortex-M assembly), the result is
this disassembly listing
(The file 'listing.htm' contains some of the disassembled original functions,
garnished with HTML anchors so it can easily be linked from other documents.
The listing does not include anything about the software AMBE vocoder.)
The number of annotations in the listing will hopefully increase. In January 2017, it was "subject to frequent changes".
If you are interested in the subject, get in touch via the md380tools group.
Structure of the above disassembly listing (at the time of this writing... also "subject to change"):
- Overview of all functions currently in the listing (with 'clickable' links injected by another script)
- Hex dump of the interrupt vector table (see next chapter for details)
- Disassembly of the startup code (Reset_Handler(), SystemInit(), etc)
- Disassembly of exception- and interrupt service handlers
- Disassembly of other functions, one by one, whose purpose is (almost) known
- List of annotated symbols, along with their hex addresses and (for some) size in bytes
Principal workflow to generate the above disassembly listing:
New annotations in disasm_yhf.r may eventually find their way into the 'master' at Github (md380tools/applet/src/symols_d13.020 ?).
- Let disam_yhf.bat call Radare2.exe, telling it to process disasm_yhf.r
- Examine the result (listing.txt or listing.htm)
- Modify disasm_yhf.r if necessary (this is a rather cryptic Radare2 script),
then go back to step 1
- When 'satisfied' with the result, convert to html (via disasm2htm.py),
garnished with 'anchors' and links, and upload to the website
The STM32F4xx Reference Manual ("RM0090", "DocID108909 Rev 7", page 374) shows the (interrupt-) vector table beginning at 0x0000 0000.
But since the firmware image starts at 0x0800C000 (i.e. internal flash), the vector table must have been remapped to some
address inside that range (start=0x0800C000, size=0xF3000 bytes). Remapping takes place in the custom bootloader shortly after
power-up, because the first part of the internal flash is occupied by Tytera's "custom" firmware bootloader.
On first glance, the STM32F40x didn't seem to have a VTOR register (Vector Table Offset Register),
at least the Reference Manual (ST's RM0090, DocID108909 Rev 7) doesn't mention it.
All it says on page 70 is that certain devices can boot from Flash "bank 1" or "bank 2".
The "Reference Manual" only mentions a 'SYSCFG memory remap register' on page 286, with only TWO BITS:
Two bits are used to configure the type of memory accessible at address 0x0000 0000.
These bits are used to select the physical remap by software and so, bypass the BOOT
A full-text search for 'SYSCFG' in RM0090 showed it's base address is 0x40013800 (RM0090 page 66).
'SYSCFG_MEMRM' is at offset 0x00 (RM0090 page 291), so let's ask md380_tool.py for the contents of SYSCFG_MEMRM:
MEMRM=0x00000000 means 'Main Flash memory is mapped at 0x00000000'.
[email protected] /c/tools/md380tools
$ python md380_tool.py hexdump 0x40013800
Dumping memory from 0x40013800.
00 00 00 00 00 00 00 00 22 02 00 00 00 00 00 00
|_________| |_________| |_________| |_________|
| | | |
SYSCFG.MEMRM .PMC .EXTICR1 .EXTICR2
With that setting, the first couple of bytes @ 0x00000000 should contain the same
as the begin of the 'Main Flash' (0x08000000, not 0x0800C000) !
Let's see if our poor man's debugger (patched USB handler) confirms if Flash is being 'mirrored' to the begin of the address space:
All these vectors (except the first, which is the "initial stack pointer") point into the bootloader !
[email protected] /c/tools/md380tools
$ python tool2.py hexdump32 0x00000000
00000000: 20001A30 08005615 08005429 0800542B
00000010: 0800542D 0800542F 08005431 00000000
00000020: 00000000 00000000 00000000 08005433
00000030: 08005435 00000000 08003083 08005437
$ python tool2.py hexdump32 0x08000000
08000000: 20001A30 08005615 08005429 0800542B
08000010: 0800542D 0800542F 08005431 00000000
08000020: 00000000 00000000 00000000 08005433
08000030: 08005435 00000000 08003083 08005437
08000040: 08005661 08005665 08005669 0800545B
Thus they can't belong to the 'application firmware'. There must be some magic that the STM32F4xx "Reference Manual" doesn't tell
that allows the application firmware to have its own vector table.
After wondering why even a FULL TEXT SEARCH for "VTOR" in the "Reference Manual" (RM0090) didn't reveal anything,
the specification of SCB (System Control Block) and VTOR (Vector Table Offset Register) was finally found
in an extra document, titled "PM0214 / Programming manual" / "DocID022708 Rev 5". Aaargh !
'VTOR' is at address 0xE000ED08, and only accessable in privileged mode. Let's try if our 'debugger' can access it:
We're lucky, the USB command handler runs in privileged mode,
and the 32-bit values in VTOR, 0x0800C000, is just as expected:
[email protected] /c/tools/md380tools
$ python tool2.py hexdump32 0xE000ED00
E000ED00: 410FC241 00000853 0800C000 FA050300 ← CPUID, ICSR, VTOR, AIRCR
E000ED10: 00000000 00000200 00000000 00000000 ← SCR, CCR, SHPR1, SHPR2
E000ED20: F0F00000 00000000 00000000 00000000 ← SHPR3, SHCRS, CFSR, HFSR
E000ED30: 00000000 E000ED34 E000ED38 00000000 ← ? ? ?, MMAR, BFAR, AFSR
The exception- and vector table begins at the start of the firmware image.
Can we trust the values read from the 'SCB' registers via md380_tool.py ?
The first 32-bit register in SCB, CPUID, should contain 411FC231 (according to PM0214 Rev 5 page 243).
'Not quite right' but this wouldn't be the first bug in a datasheet.
Ok, so VTOR contains 0x0800C000, which is the application firmware's vector table in Flash,
and we already know the VT has not been remapped to internal RAM (which would also be possible on the STM32F4).
Let tool2.py list the vector table (in 32-bit DWORDs) to find the addresses of dozens
of interrupt- and exception handlers in the patched firmware. Underlined entries were added manually
to indicate differences between original firmware (here: unwrapped/d13.020.img):
The vector table layout is in the STM32F4 Reference Manual ("RM0090" Rev 7). Beware, RM0090 describes various ('slightly incompatible') chips;
the one to use here is 'Table 61. Vector table for STM32F405xx/07xx ..' on pages 368..372.
[email protected] /c/tools/md380tools
$ python tool2.py hexdump32 0x0800C000
0800C000: 2001F170 0809AF01 08093EF1 08093EF9 ← initial SP, RESET, NMI, HardFault
0800C010: 08093F01 08093F09 08093F11 00000000 ← MemManage, BusFault, UsageFault, rsvd
0800C020: 080F9245 00000000 00000000 08093F19 ← rsvd?!, rsvd, rsvd, SVCall
0800C030: 08093F1B 00000000 08043E1F 08093F1D ← DebugMonitor, rsvd, PendSV, SysTick
0800C040: 080FC5AD 080FC5B1 080FC5B5 08094239 ← Watchdog, PVD_EXTI, TAMP_STAMP, RTC_WKUP
0800C050: 080FC5BD 080FC5C1 0809403F 08094023 ← Flash, RCC, EXTI0, EXTI1
0800C060: 08093F9D 08093F89 080FC5D5 080FC5D9 ← EXTI2, EXTI3, EXTI4, DMA1_S0
0800C070: 080FC5DD 0804E9A1 080FC5E5 080FC5E9 ← DMA1_S1,DMA1_S2,DMA1_S3,DMA1_S4
0800C080: 0804E9A9 080FC5F1 080FC5F5 080FC5F9 ← DMA1_S5,DMA1_S6, ADC, CAN1_TX
0800C090: 080FC5FD 080FC601 080FC605 080FC609 ← CAN1_RX0, CAN1_RX1, CAN1_SCE, EXTI9_5
0800C0A0: 080FC60D 080FC611 080FC615 080FC619 ← TIM1_BRK_TIM9, TIM1_UP_TIM10, TIM1_TRG_COM_TIM11, TIM1_CC
0800C0B0: 08049D5F 08094109 0809408D 080FC629 ← TIM2, TIM3, TIM3, I2C1_EV
0800C0C0: 080FC62D 080FC631 080FC635 080FC639 ← I2C1_ER,I2C2_EV, I2C2_ER, SPI1
0800C0D0: 080FC63D 080FC641 080FC645 080FC649 ← SPI2, USART1, USART2, USART3
0800C0E0: 080FC64D 080FC651 08093F3F 080FC659 ← EXTI15_10, RTC_Alarm, OTG_FS_WKUP, TIM8_BRK_TIM12
0800C0F0: 08094229 080FC661 080FC665 080FC669 ← TIM8_UP_TIM3, TIM8_TRG_COM_TIM1, TIM8_CC, DMA1_S7
0800C100: 080FC66D 080FC671 08094079 0804E999 ← FSMC, SDIO, TIM5, SPI3
0800C110: 080FC67D 080FC681 08094139 080941E9 ← UART4, UART5, TIM6_DAC, TIM7,
0800C120: 080FC68D 080FC691 080FC695 08094271 ← DMA2_S0, DMA2_S1, DMA2_S2, DMA2_S3,
0800C130: 080FC69D 080FC6A1 080FC6A5 080FC6A9 ← DMA2_S4, ETH, ETH_WKUP, CAN2_TX
0800C140: 080FC6AD 080FC6B1 080FC6B5 08093F71 ← CAN2_RX0, CAN2_RX1, CAN2_SCE, USB_OTG_FS
0800C150: 080FC6BD 080FC6C1 080FC6C5 080FC6C9 ← DMA2_S5, DMA2_S6, DMA2_S7, USART6
0800C160: 080FC6CD 080FC6D1 080FC6D5 080FC6D9 ← I2C3_EV,I2C3_ER, OTG_HS_EP1_OUT, OTG_HS_EP1_IN
0800C170: 080FC6DD 080FC6E1 080FC6E5 080FC6E9 ← OTG_HS_WKUP, OTG_HS, DCMI, CRYPTO
0800C180: 080FC6ED 080FC6F1 ← HASH_RNG, FPU
Bit 0 in a table entry (data=address) indicates a handler written in Thumb code. Since this Cortex-M only 'understands' Thumb, all entries in the VT must be odd. The handler functions themselves must start at even addresses. DL4YHF's Radare2 script uses a kluge to find out the even handler addresses to disassemble them.
The 'reserved' interrupt vector at 0x0800C020 is (ab-)used as storage for the original reset vector
- see patches in patches/d13.020/patch.py.
From the above list it's obvious that the original firmware uses different default handlers for each interrupt.
For example, the MD380 will never need a CAN-receive-interrupt. This gave some confidence
that also some of those timer interrupts are also just dummies.
- The interrupt vector table itself appeared to be implemented in
applet/lib/startup_stm32f4xx.s, which was referenced
from the makefile. We could populate it with our own interrupt handlers, but at the time of this writing (2016-12),
too many of the original firmware's interrupt handlers would need to be patched in.
For that reason, the patch (in patch.py) only modifes 'a few words' but not the entire interrupt vector table.
The map file (applet/applet.map from 2016-12-30) started at Flash-address 0809B000,
i.e. our own compiled and linked part of the firmware doesn't even get close to the interrupt vector table.
To install own interrupt handlers (later), the VT could be copied into RAM (at runtime), and any new vector be installed "dynamically"
to avoid even more script-controlled patches.
For own extensions (described in later chapters of this file), it was necessary to 'tap' some
of the analog inputs. As explained in the hardware description,
the following analog inputs (on ADC1) are copied via DMA into a 6-element array in the internal
In DL4YHF's disassembly listing, the array with the 6 'raw' ADC readings
was named 'adc1_6channel_dma_buffer'
(RAM address 0x2001e51c).
- "MOD2_BIAS" at array index 0
- "BATT" at array index 1
- "RSSI" at array index 2
- "VOX" at array index 3
- "VOL_POT" at array index 4
- "BATT" at index 5 (digitized twice in a 'regular channel sequence')
Follow the links in the listing to a few functions that reference this array to find some of the
places where these values are processed (finding all details requires more patience or doggedness).
The following guesswork applies to firmware D13.020:
- BATT from adc1_6channel_dma_buffer is processed at 0x8046078
- VOL_POT, adc1_6channel_dma_buffer, is processed at 0x804518e
- BATT from adc1_6channel_dma_buffer is processed at 0x80453d0
- MOD2_BIAS, adc1_6channel_dma_buffer is processed at 0x8045806
- the places where 'VOX' and 'RSSI' are processed where not pursued yet
The origin of this function name was unknown (perhaps just a number, or part of an address
in an older firmware). In D13.020, two calls to f_4315_hook() were patched
at addresses 0x080202D0 and 0x080202FC, replacing the original calls to F_4314().
These functions are related with the display output. By intercepting these calls,
the netmon, text console, and possibly a few other 'own' displays can prevent
updates of the framebuffer (inside the LCD) by the original firmware,
and draw something else instead (e.g. console or an optional 'own menu').
Because the original firmware also 'draws' something on the display,
by calling subroutines like gfx_set_fg_color(), gfx_blockfill(), and various
flavours of gfx_drawtext(), it's safe to access the display from here.
Replaces the original keyboard handler (kb_handler), which in turn is called from this function.
In D13.020, called from the main loop at 0x0804ebd2 (via 'biglist_pollsubsys_maybe').
The orignal function (kb_handler at 0x0804f94c) scans the multiplexed keyboard matrix though various
subroutines (quite obfuscated, and no reason to find out how it works yet). The result of this
'lowest level' of polling the keyboard is stored in variable 'kb_row_col_pressed'.
Keys are debounced via software, and distinction is made between short and long key pressing times.
Depending on which keys were pressed (and how long they were pressed), different subroutines
are called from the original kb_handler(), including a table-driven conversion of the key into
a keycode (variable kb_keycode). Further actions don't seem to take place in the original kb_handler(),
so the hooked function (kb_handler_hook, only exists in the patched firmware),
can intercept certain keystrokes. That happens in applet/src/keyb.c .
Functions called from kb_handler_hook() besides the 'original' kb_handler():
- handle_hotkey() : These seemed to be the 'Netmon' controlling keys (?)
- handle_sidekey() : Seems to be called for ANY programmable sidekey, but what about the original sidekey functions
like 'zone toggle' or 'zone increment' ?
- evaluate_sidekey() : Only implements the additional sidekey functions (?)
(The author finally gave up on trying to understand the convoluted original menu functions.
This chapter only contains a few notes which may be helpful when replacing the
original Menu with an easier-to-manage menu, e.g. "app_menu.c")
The 'Zone' menu
In the Radare2 srcipt for the old D02.032 firmware, the implementation of the 'Zone' menu had
been given the name 'F_Menu_Zone' (at 0x080123C4).
In D13.020, equivalent (with a funny name that was replaced later) is at 0x080131ac.
It reads an awful lot of stuff from different addresses in the SPI flash (-> md380_spiflash_read)
and often accesses struct 'current_channel_info'.
The 'forensic analysis' of the original firmware is only part of the fun (for microcontroller enthusiasts).
This chapter describes a few of the author's own experiments. Some of them may eventually make it into
the 'official' repository at Github. At the time of this writing (early 2017) it wasn't sure which of these
features would be useful and stable enough to be merged with the rest of the patches.
When beginning with the first own experiments (with modications in the sourcecode that definitely
shouldn't have affected the code, strange things happened,
and the radio refused to boot with the new compiled firmware.
For example, here is an example from the map file with 'g_pfnVectors' imported from a dumb (temporary)
subdirectory, after GIT CLONE (into an EMPTY subdirectory) and MAKE DIST CLEAN:
After a ridiculously small modification somewhere in a C source in the 'applet' folder,
and a second MAKE DIST CLEAN, the firmware wouldn't start anymore, and the map file now contained this:
LOAD src/symbols_d02.032 (we're obviously linking for the 'old' firmware, but that doesn't matter here)
.isr_vector 0x0809d000 0x188 C:\Users\Wolf\AppData\Local\Temp\ccv5AJyH.o ?!?
0x0809d188 . = ALIGN (0x4)
.text 0x0809d1a0 0x6be8
0x0809d1a0 . = ALIGN (0x4)
Summary: The 'fresh' firmware contained an array with 0x188 bytes called 'g_pfnVectors',
which vanished into air after touching a sourcecode.
.isr_vector 0x0809d000 0x0 wtf happened to Mister 'g_pfnVectors' ?!?
0x0809d000 . = ALIGN (0x4)
0x0809d000 . = ALIGN (0x4)
.text 0x0809d000 0x6ba8
0x0809d000 . = ALIGN (0x4)
So where did 'g_pfnVectors' come from, and what caused it to magically disappear ?
A full text search in the entire md380tools directory revealed it's in md380tools/applet/lib/startup_stm32f4xx.s .
In the 'fresh installed' md380tools, that file contained a nice readable assembler source
(borrowed from the 'Atollic TrueSTUDIO toolchain').
In the second ('dirty') MAKE DIST CLEAN, the file at the same location had magically lost the original contents,
and contained the following crap instead (..excuses for using strong words here..):
'New' contents of md380tools/applet/lib/startup_stm32f4xx.s
Doesn't look like something an ARM assembler would understand !
# 1 "lib/startup_stm32f4xx.S" crap !
# 1 "<built-in>" crap !
# 1 "<command-line>" crap !
# 1 "lib/startup_stm32f4xx.S" crap !
Let's find out how this crap crept in. Another full-text search in the entire md380tool
(for 'startup_stm32f4xx', without an extension) showed there was not only
a hand-written startup_stm32f4xx.s, but also a file named startup_stm32f4xx.c !
Except for the file extension, two source files with the same name - that's asking for trouble.
Under Linux it may work (because for Linux, startup_stm32f4xx.s is not the same as startup_stm32f4xx.S),
but under Windows it doesn't. Having two files generating an object file
named startup_stm32f4xx.o will sooner or later cause chaos (regardless if using Linux or Windows),
so get rid of either startup_stm32f4xx.s or system_stm32f4xx.c .
To make a long story short:
(*) Decades ago GNU suggested ".S" for hand-written, and ".s" for compiler-generated assembly.
- We're going to use OUR OWN vector table a bit later anyway,
so instead of the (trashed) applet/lib/startup_stm32f4xx.s,
we'll use applet/src/startup_D13_0200.S (*) from now on.
- Carefully edit applet/Makefile (don't replace tabs by spaces!) to use the above startup (for FW D13.020):
SRCS += lib/startup_stm32f4xx_asm.S # add startup file to build. Mod DL4YHF 2017-01-06
But under Windows, "Bla.S" would be overwritten if the C compiler emitted "bla.s".
After these modifications, 'our' vector table (g_pfnVectors) appeared in the map file again,
and the own additions to the firmware patches worked like a charm.
As descibed here, a simple hardware
modification (shunting the backlight switching transistor with a resistor) can provide a permanent weak backlight,
which makes the otherwise 'completely black' display readable as long as the radio was turned on. That modifcation came
at the cost of a 220 Ohm SMD resistor, and additional 2 mA (!) drawm from the battery, which is neglectable.
To try a similar 'dimming' of the LCD backlight via software (instead of turning the backlight completely off),
the typical solution would be a pulse-width modulated drive (PWM) for the LEDs behind the TFT and the illuminated keyboard.
Both are driven through the same switching transistor, controlled by the CPU's "PC6" pin.
According to the STM32F405's datasheet ("DocID22152 Rev 4", page 52), this pin can be configured as
I2S2_MCK, TIM8_CH1, SDIO_D6, USART6_TX, DCMI_D0, TIM3_CH1, and EVENTOUT.
The original firmware configures PC6 as GPIO. The original plan to use PC6 as hardware PWM output failed,
because Timer 3 and Timer 8 were both 'occupied' by the original firmware.
Generating a PWM signal with at least 60 Hz (to avoid flicker) with ten intensity steps via software
would require a periodic interrupt fired 600 times per second. To find out if one of the existing 'periodic'
interrupts already runs at a suitable repetition rate, a new module (applet/irq_handlers.c) was added to the makefile
(in md380tools/applet/Makefile), and an option to include or exclude that feature when compiling other modules
was added in md380tools/applet/config.h .
Instead of generating the PWM via bit-banging on PC6 ("Lamp"), that pin is reconfigured
as UART6_TX, and 9-bit words are periodically sent to the UART which 'look like' PWM with 104.25 Hz .
Details are in the heavily documented sourcecode, available on Github (started as temporary fork 'DL4YHF-Backlight') or
in the zipped archive on DL4YHF's webpage at QSL.NET (before being merged back into the master repository).
How to use (configure) the dimmable backlight is described here.
Unfortunately, in a few radios, reducing the PWM intensity via pulse width modulation caused audible hum.
Two newer RT3's (bought in December 2016) did not suffer from this. Older radios possibly had a bypassing
capacitor at the white LEDs, causing a current spike at each PWM pulse.
After finding out how the CPU generates audible 'beeps' (details here),
the implementation of a simple Morse code generator was trivial.
The file applet/src/irq_handlers.c
contains the Morse code generator, which can be easily fed with an 8-bit ASCII string.
The Morse code is generated 'in the background', in SysTick_Handler().
SysTick_Handler is called 666 times per second, so even VHSC (very high speed Morse code)
would be possible. But, for mere mortals like yours sincerely, 18 to 22 WPM sounded more appropriate.
The API (application interface) for the Morse generator is simple - a function to send an ASCII string,
another function to send a wide string (with 16-bit characters),
and -mainly for debugging- a function to send an integer as a decimal number.
Because the available RAM is small and precious, the Morse generator itself only has a small transmit buffer.
Thus if the narrator (or whoever wants to 'say' something) needs send more
than MORSE_TX_FIFO_LENGTH permits (e.g. 40 characters or more),
the should be generated and sent to the Morse generator line-by-line,
for example using a state machine or an extra task running in the background.
Retrieving the channel names, and the text of the currently focused menu item
turned out much trickier (without digging deep into the firmware) than writing the Morse generator.
The output text is generated as a sequence of ASCII- and wide character strings (because the
menu items in the original firmware almost exclusively uses 16-bit characters). This happens
in module applet/src/narrator.c.
At the time of this writing (2017-03-05), the Morse code output was in an early beta stage (see topic
for visually impaired hams at github/travisgoodspeed/md380tools).
A compiled firmware, and a short description for the operator, including how to set
the CW output speed, frequency ("pitch") and volume is here.
'Rattling' noise at the end of a Morse message / where is the DMR Power-Save Countdown Timer ?
Sometimes during the end of a Morse message, a rattling noise was heard
in the speaker. That noise occurred under the following conditions:
As a fix, the disassembly was searched for the suspected 'power-save-timer'. If such a timer exists,
it must be independent from the backlight timeout: The 'rattling' always started along
with a supply current reduction, 5 seconds after the last keypress, channel switch, or squelch-closing event.
- Only when a DMR channel was selected, and that channel was not busy
- Only when the analog volume control pot was not at minimum position
- Only as long as the radio (the baseband chip?) was in power saving mode (*)
Since it completely disappeared when the volume pot was turned down (but the Morse signal
was still audible because it's not routed via the pot), it could only originate from
the C5000's 'LINEOUT' pin - see details in the hardware description.
So where (and what) is this 5-second 'power saving' software timer, presumably
decremented in one of the many 'periodic' tasks or interrupt handlers ?
When the C5000 'baseband' chip enters power-saving mode,
the supply current measured at the input to the 12 V 'battery eliminator'
drops from ca. 100 to 60 mA (with backlight at dimming level 1).
The input current pulsates at the same rate (5 Hz) as the rattling noise.
This may be the interval to check for a received (DMR-) signal during standby.
The circuit diagram shows a control signal 'DMR_SLEEP', which is on the CPU's PE6.
The USB handler in the modified firmware allows reading any register, so 'GPIOE.ODR'
(output data register) was examined while the radio entered power-saving mode.
Not PE6, but PE5 was toggling in power-saving mode,
and permanently low when active. Most likely the C5000 enters
sleep mode via SPI command (see datasheet).
When implementing the Morse output, the original menu turned out to be overly complex
(especially to retrieve certain items, edit fields, cursor position, etc),
so an attempt was made to implement an easier to use, "stripped down" menu.
The result is in sourcecode module 'app_menu.c' (alternative menu, formerly 'red-button menu').
For that purpose, a simple LCD driver is implemented (in lcd_driver.c) that doesn't depend
on the many 'black boxes' of the original firmware.
(functional but description not finished yet.. "use the source, Luke" .. see applet/src/lcd_driver.c )
From the MD380Tool's GFX.H, all we know about gfx_font_small and gfx_font_norm
are their ADDRESSES but not the structure.
Instead of trying to understand how Tytera's original firmware draws characters,
and instead of calling any of the 'black boxes' in that firmware,
the simple LCD driver uses the bitmap font addresses listed in
Basically, the 'small' font consists of 96 tiny monochrome bitmaps, each of them 12 pixels high,
and 6 pixels wide, thus 12 bytes per bitmap. The bitmap for the first character (space, ASCII #32)
was at address 0x80614d8 in D13.020 and surprisingly, also in the patch for S13.020.