In the previous post, I showed the inside of the Cyfrowy Polsat LT-6408n router. I established that it is based on the RTL8196C chip, whose bootloader offers a simple command line along with the ability to load any machine code into RAM. Here, I’ll try to make use of this – I’ll start by loading simple instructions written by hand in hexadecimal notation, and then I’ll run a compiler and develop my own ‘flasher’, turning the old router into a sort of Arduino.
Previous post in the series: Analysis of the router’s internals, memory dump, bootloader, custom UART programme
A few words on the RTL8196C architecture
In the previous post, it was easy to see that the router in question is based on the RTL8196C. The RTL8196C is a very popular, highly integrated SoC (System on a Chip) from Realtek, which forms the heart of many budget networking devices on the market. Under the bonnet lies a tried-and-tested 32-bit MIPS architecture processor core (typically clocked at around 400 MHz). As befits a dedicated networking chip, this processor is surrounded by a range of hardware features – these include, amongst others, a built-in 5-port Fast Ethernet switch (10/100 Mbps), a PCIe interface (often used for Wi-Fi modules), SDRAM and SPI Flash memory controllers, as well as a suite of basic peripherals, such as GPIO pins and a UART controller. It is thanks to this latter interface and the specific nature of the bootloader here that we’ll be able to get started with our programming without having to desolder the memory or use an external programmer.
What makes this particularly interesting is that we also have register documentation, although I must admit that… when I first started writing this post, I wasn’t aware of this and was experimenting without full knowledge of the subject.
Commands used
Fortunately, we don’t need to program the Flash memory directly; this time, my tried-and-tested CH341 can stay in the drawer. The commands available from the bootloader allow us to load a programme into RAM and execute it from there. These include:
| Command | Syntax | Functional description |
| EW | EW <address> <word1> [word2] ... | Writing 32-bit words directly to memory at the specified address. |
| DW | DW <address> <quantity> | Dump of 32-bit words from memory – useful for verifying the flash update. |
| J | J <address> | Jump to the specified address – triggers the execution of the uploaded code. |
The loading procedure is simple – we send words to the selected memory location via EW, confirm the write via DW, and execute the J jump.
The first programme written without a compiler
We’re starting from scratch. The programme must be written in machine code. We know that the RTL is based on the MIPS RLX4181 core. So we can use the basic instructions of this assembler to write, for example, a loop that sends an exclamation mark ‘!’ to the UART transmit port. I managed to determine its address by searching through the source code of similar Realtek chips:
It is located at address 0xB8002000. The transmission of each character will be interspersed with an empty loop (delay) to prevent the transmit buffer from overflowing. You can make use of the documentation and online tools, for example the MIPS converter .
The code itself boils down to a few simple assembly instructions: loading the target UART address, writing a single byte (an exclamation mark in ASCII) to it, counting down the time in an empty loop (to allow the hardware time to ‘push’ the character out) and finally – performing an unconditional jump back to the start.
As a result, we obtain the following programme:
| Address | Hex | Instruction | Description |
| 80500000 | 3C08B800 | lui $t0, 0xB800 | Load the upper 16 bits of the UART base address |
| 80500004 | 35082000 | ori $t0, $t0, 0x2000 | $t0 = 0xB8002000 (UART THR transmit register) |
| 80500008 | 34090021 | ori $t1, $zero, 0x21 | $t1 = '!' (ASCII 0x21) |
| 8050000C | A1090000 | sb $t1, 0($t0) | Write the character '!' to the UART transmission register |
| 80500010 | 340A8000 | ori $t2, $zero, 0x8000 | $t2 = 32768 (delay counter) |
| 80500014 | 1540FFFF | bne $t2, $zero, delay | If the counter ≠ 0, jump back to the loop |
| 80500018 | 214AFFFF | addi $t2, $t2, -1 | Decrement the counter by 1 (delay slot) |
| 8050001C | 08140002 | j 0x80500008 | Jump back to the start of the loop |
| 80500020 | 00000000 | nop | No operation (delay slot) |
After interrupting the bootloader, we load this code (hexadecimal, word by word) using the command EW :
EW 80500000 3C08B800 35082000 34090021 A1090000 340A8000 1540FFFF 214AFFFF 08140002
EW 80500020 00000000
The results can be verified using the command DW 80500000 9.
We execute the code with a jump:
J 80500000
The result is that the terminal window is immediately flooded with an infinite string of exclamation marks, “!!!!”.
The proof of concept works, but who would want to write machine code by hand?
Compiler and the C language
Writing MIPS instructions by hand and converting them to hexadecimal in a hex converter is a tedious process. To To create more complex programmes, I decided to use a suitably selected compiler, compatible with the processor in use – mips-linux-gnu-gcc . This allows me to write programmes in C. I couldn’t get it to run on Windows, so I used a Linux environment on ‘Windows’ – WSL. I didn’t have any header files for this Realtek chip, but I don’t need them. I started with the simplest piece of code I wanted to compile:
Code: C / C++
This code now needs to be compiled. I run mips-linux-gnu-gcc with the following parameters:
- -march=mips32 – generate code for the MIPS32 architecture (compatible with the RTL8196C core)
- -mno-abicalls -fno-pic – disable position-independent code (PIC) – the programme runs at a fixed address
- -nostdlib -nostartfiles – do not link the standard C library or the default start-up code
- -ffreestanding – do not assume the presence of a standard runtime environment
- -O2 – optimisation – smaller and more efficient machine code
- -T rtl8196c.ld – use a custom linker script defining the memory layout (described below)
The compiler generates an ELF file, from which a raw binary file – the machine code itself, without headers, ready to be loaded directly into RAM – is extracted using ‘mips-linux-gnu-objcopy -O binary’.
The key element is the rtl8196c.ld linker script, which tells the compiler at which address in RAM our programme will ultimately be located:
Code: C / C++
The compilation script (apologies for the image format, but the forum engine won’t let me paste these commands...):
The second step is to export the commands from the binary file into a readable format that can be pasted into Realtek. This isn’t normally done, so I used my own Python script:
Code: Python
Generated commands:
EW A0500000 3C05A050 24A50050 3C04B800 3C06001E 24020048 00A01825 304200FF 24630001
EW A0500020 A0822000 80620000 1440FFFC 304200FF 34C28480 00000000 2442FFFF 1440FFFD
EW A0500040 00000000 1000FFF3 24020048 00000000 4865792E 2E0D0A00 00000000 00000000
DW A0500000 24
J A0500000
Result:
The programme works, although this time the result was slightly worse – characters are getting lost; I suspect my loop waiting for the UART to become free didn’t work.
My own batch upload tool
The instructions are already generated automatically; they just need to be uploaded automatically as well. Command-line mode is enabled by pressing ESCAPE during boot-up; after all, a programme can do this. The programme can also send commands. We just need to determine when the router starts up – its start-up message can be intercepted. Then we can send individual ‘EW’ commands one by one, verifying them with the ‘DW’ command. This is precisely the process carried out by the Python script I’ve refined (‘uart_auto_flash.py’). It operates in a simple loop: it listens for the “Booting” message, automatically presses the ESC key to enter command-line mode, and then sends byte packets to the console, taken directly from the compiled binary file ('.bin'), verifying the correctness of the operation after each block with the ‘DW’ command. This allows the process to be fully automated without the tedious copying and pasting of instructions into the terminal window.
The only thing still missing was automatic rebooting. Ideally, the reboot could be triggered by the DTR/RTS pin from a suitable USB-to-UART converter, but as I didn’t have one to hand at the time, I used... a power socket with Tasmota firmware. I control it via the classic REST API for commands; OpenBeken also supports this standard.
In this way, uploading the test programme to the router simply comes down to executing a single command:
python ..\uart_auto_flash.py hello_world.bin --tasmota 192.168.0.176
GPIO support and necessary fixes
The next thing I tried to get working was the classic LED flashing. The LAN port status LEDs are connected directly to the Realtek pins, so there is potential to control them. They are described, for example, as LED_PORT1/GPIOB3, which suggests they have a dual role – either managed by the Ethernet controller or operating in generic, programmable I/O mode.
I also encountered a few issues along the way – the programme would freeze after a while, so I tried manually initialising the stack pointer. I also disabled interrupts. Initially, the GPIO didn’t respond to control either, but that was because the hardware switch controller was enabled by default. I had to disable it manually in the code.
Here is the final, working code for controlling the LEDs:
Code: C / C++
Result:
Countdown via UART and finalisation
The next thing I wanted to try was displaying numbers via UART. To start with, something simple – a countdown of sorts. I decided on a straightforward method of converting an integer to a string:
Code: C / C++
Unexpectedly, however, the programme then stopped working. I realised that I’d forgotten to specify the entry point for the start function in the linker, with the result that the Realtek bootloader wasn’t jumping to the right place. I had to add the entry ‘z entry’ at the very top of the linker file and then include this identifier in the C code:
Code: C / C++
The rest of the code works as expected – the main loop now has a counter and displays it with every message. There’s no
sprintffunction; I display the static string separately and the number converted to a string separately.
Code: C / C++
Result:
Summary
In this way, I managed to set up a basic working environment for the RTL8196C. I began my experiments with simple instructions, which can be written by hand or with the help of a suitable online tool; I then found a suitable compiler and, starting from scratch, prepared my own linker script and C boot code for it. I resolved issues with unwanted interrupts, the stack and pin multiplexing, and automated the entire flashing process in Python. In doing so, I transformed an old router into a convenient development platform on which I can run my own machine code. Feel free to join the discussion – perhaps some of you have tried similar experiments with other SoCs?
PS: This isn’t the last post in this series, and there are still issues to resolve in my overall workflow – I tried to create a simple console for this router, but for some unknown reason the programme would crash after processing just a few characters. Once I’ve sorted that out, I’ll show you a more complex project based on this Realtek chip.
Cool? Ranking DIY Helpful post? Buy me a coffee.