Meraki MR18 installation using JTAG and OpenOCD
A modern firmware prevents installation of the Meraki MR18 using its own firmware, and therefore upgrading the device to OpenWrt requires use of the JTAG, which we're going to do using a Raspberry Pi and open source software. An overview of the process is:
- Disassemble the unit to access the UART
- Solder some pins to allow access to the JTAG
- Connect both of these interfaces to a Raspberry Pi
- Upload an installation kernel into the device's memory using OpenOCD
- Use the running kernel to carry out regular OpenWrt instructions to run the regular flashing process
Disassembly
Remove the rubber feet to access the four torx screws. The back of the unit is easy to remove but you might feel some restriction caused by the lump of rubber glued to the heatsink; it's okay to pull and overcome this force. The device is easy to disassemble as there are no plastic clips to break, only screws.
Then remove the single screw on the mainboard to remove it from the chassis. As you do, it's easiest to unplug the small antenna (black) from the board and leave it in the chassis.
Soldering
Locate the JTAG port. The layout appears to be in line with a standard 14-pin EJTAG 2.6. Familiarise yourself with this port and which pins are which on the device you have in front of you.
At this point is recommended to remove the metal cover on the mainboard which has the antennae attached. This is to allow access to the underside of the mainboard for soldering. To do this, unplug each of the antennae carefully, unscrew the two small screws (top-left and bottom-right) and slide the cover.
For the purposes of this exercise, we only need a subset of the pins on one row: nTRST, TDI, TDO, TMS, TCK.
We can also skip the 'ground' if we're willing to use the ground pin on the UART. That means it's important to keep the UART connected while doing the JTAG operations, and that both are connected to the same device (in this case, the Raspberry Pi).
Using only one row of pins is to our advantage. If we were to solder a connector vertically on both rows they will protrude with the case, preventing re-assembly or, worse, shorting on the case. Instead, we can solder right-angled pins to only one row, which doesn't affect reassembly should we wish to return to the JTAG at a later date.
With access to the underside of the board (from removing the metal cover) it's easy and clean to solder to the 5 pins needed; you may even want to solder all 7 on that row if you want to experiment later with the reset pins. It's recommended to solder and position just one pin to hold everything in place, before doing the rest.
Serial connection
Boot up the Rasperry Pi and disable it's own console on the serial port. To do this:
$ sudo raspi-config # "Interfacing options" # "Serial" # "Would you like a login shell to be accessible over serial?" -> "No" # "Would you like the serial port hardware to be enabled?" -> "Yes" # "Finish"
Now locate the UART on the MR18, which already has some pins soldered. In our illustration, pin 1 is to the right, and pin 4 to the left is supply voltage which will go unused.
Connect these to the Raspberry Pi's own UART using jumper wire:
Raspberry Pi | MR18 | Purpose |
---|---|---|
6 | UART 1 | Ground |
8 | UART 2 | Pi to MR18 |
10 | UART 3 | MR18 to Pi |
Note that TX and RX are symmetrical; that is TX (transmit) connects to RX (receive) on the other device and vice-versa.
We can now view the console of the MR18. Run the following command on the Raspberry Pi.
$ sudo apt install picocom $ sudo picocom --baud 115200 /dev/ttyS0 # swap for /dev/ttyAMA0 for older Raspberry Pi
and now connect power to the device; you should see the boot logs. Leave this running in a terminal; it survives reboots of the MR18 and gives a live display on what the device is doing, which you'll need later.
If you have any problem here, check that TX and RX are round the correct way. Information on the Meraki MR18 main page reports TX and RX on the MR18 to be the other way around. This either means there are different revisions of the board or, more likely, confusion over connecting TX to RX and vice-versa.
JTAG connection (OpenOCD)
Identify pin 1 on the JTAG because it has the square (not circular) solder pad and is the left-most pin in this illustratation:
Connect the Raspberry Pi to the device's UART and JTAG by conecting these pins:
Raspberry Pi | MR18 | Purpose |
---|---|---|
19 | JTAG 3 | TDI |
21 | JTAG 5 | TDO |
22 | JTAG 7 | TMS |
23 | JTAG 9 | TCK |
26 | JTAG 1 | nTRST |
The actual mapping is derived from the description in OpenOCD's interface/raspberrypi-native.cfg file (see the commands below). We're going to make a slight modification to the file to prepare it for use:
$ apt install openocd $ cd ~ $ cp /usr/share/openocd/scripts/interface/raspberrypi-native.cfg ./
We need to tell the Raspberry Pi that it has access to some kind of reset pin. Edit the raspberrypi-native.cfg (eg. nano) and uncomment this line:
bcm2835gpio_trst_num 7
Note that the raspberrypi-native.cfg is written for the Raspberry Pi models 1 and zero. Modify the peripheral address and speed coefficients to match your Raspberry Pi model as follow:
RPI 1A/A+/B/B+ with 700 MHz base clock
bcm2835gpio_peripheral_base 0x20000000 bcm2835gpio_speed_coeffs 113714 28
RPI 2B with 900 MHz base clock
bcm2835gpio_peripheral_base 0x3E000000 bcm2835gpio_speed_coeffs 146203 36
RPI 3B with 1200 MHz base clock
bcm2835gpio_peripheral_base 0x3E000000 bcm2835gpio_speed_coeffs 194938 48
RPI 3B+ with 1400 MHz base clock (despite the higher clock seems to be the same as RPi 3B)
bcm2835gpio_peripheral_base 0x3E000000 bcm2835gpio_speed_coeffs 194938 48
RPI 4 with 1500 MHz base clock
bcm2835gpio_peripheral_base 0xFE000000 bcm2835gpio_speed_coeffs 236181 60
Write a new file in the same directory, called mr18.cfg:
if { [info exists CHIPNAME] } { set _CHIPNAME $_CHIPNAME } else { set _CHIPNAME ar9344 } if { [info exists CPUTAPID] } { set _CPUTAPID $CPUTAPID } else { set _CPUTAPID 0x00000001 } jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id $_CPUTAPID set _TARGETNAME $_CHIPNAME.cpu target create $_TARGETNAME mips_m4k -endian big -chain-position $_TARGETNAME # Set a working area for the device to make fastload etc. work $_TARGETNAME configure -work-area-phys 0x81000000 -work-area-size 0x4000 -work-area-backup 1 transport select jtag adapter_khz 1000
Now we'll verify that everything so far is working by taking basic control of the device.
Connect the power to the device (whether from external PSU or PoE) and during the boot sequence run the following on the Pi:
$ sudo openocd -f raspberrypi-native.cfg -f mr18.cfg -c "init; halt"
You should see the boot sequence halt in your serial console. And you can connect in to OpenOCD and issue more commands, such as resume, which you'll see has the effect of the boot sequence continuing in the serial console:
$ telnet localhost 4444 > resume
If this doesn't work out, stop now and work out why before continuing.
Uploading a kernel
Download the kernel from https://github.com/riptidewave93/Openwrt-MR18/releases. Don't worry that this is not the latest OpenWrt release; it is used only to bootstrap the installation. Extract the files to the home directory on the Pi.
Our plan to upload this code into the RAM of the device, and then point the CPU at it to execute, much like a bootloader would.
Firstly, we need to catch the boot process early. This is important, as we need to boot our own kernel before any hardware peripherals in the device have begun to initialize, but late enough that the processor has been set up correctly by the existing Meraki bootloader.
On some devices this time window is very small. Too early or too late will result in openocd failing to communicate properly with the processor.
Reports show the place to interrupt the boot process is during part1
of the Meraki bootloader Copying image to memory ... ...
as shown below:
Note: It appears to be impossible to get the correct timing if using POE.
Provide power to the MR18 and at that exact moment, run OpenOCD:
$ sudo openocd -f raspberrypi-native.cfg -f mr18.cfg -c "init; halt"
We're looking to interrupt the device very early in its boot sequence. You should be able to 'catch' it during the second stage bootloader, and certainly before of the Cisco's Linux code has run.
Now execute the following commands, one by one, by hand on the OpenOCD command line which is started by connecting into the running OpenOCD process:
$ telnet localhost 4444 >
First, disable the hardware watchdog, which normally is there to re-set the device automatically if it thinks the Cisco operating system has crashed (and it will detect this when the device is halted):
> mww 0xb8060008 0x0
Now upload the code to the device. The distributed file begins with a 1Kb header, normally used by the bootloader. Since we aren't the bootloader we'd like to strip this first 1Kb of the file. The easiest way is to upload it 1Kb earlier in the device's memory than the code we're going to use (0x8005FC00 = 0x80060000 - 1Kb):
> load_image openwrt-ar71xx-nand-mr18-initramfs-kernel.bin 0x8005FC00
It should return something along the lines of:
downloaded 6353004 bytes in 43.844112s (141.504 KiB/s)
> verify_image openwrt-ar71xx-nand-mr18-initramfs-kernel.bin 0x8005FC00
Again you should be able to see something like this:
verified 6353004 bytes in 1.027853s (6035.985 KiB/s)
Now reset some registers and execute the kernel:
> reg r4 0 > reg r5 0 > reg r6 0 > reg r7 0 > resume 0x80060000
You should see, immediately on your serial console, the booting of the Linux kernel for OpenWrt.
Troubleshooting
I found the process above can be somewhat unreliable, not working 100% of the time. The result is that the serial console either shows nothing, or perhaps some garbled characters. Mainly this can be resolved by doing the final steps again soon after:
halt resume 0x80060000
If this still doesn't work, here are some other things you could try:
- Make sure you're halting with OpenOCD straight after powering the MR18 on, not too late.
- Executing the commands in the telnet connection by hand (not scripted) seemed to help, and also gives a feel for what's happening.
- Don't forgot to reset the registers before “resume”
The issue is likely to be that we're not resetting some register or memory, so the results are non-deterministic. It would be good to automate more of this process to 100% reliability by understanding this. But, for now it may be ok; you only need to boot like this once per device, so if you got it to work just once, then continue to the next step.
Flashing OpenWrt
The device is now running OpenWrt, but a reboot will return to the original Cisco firmware. Now we're going to actually flash the device.
On your PC, download the latest OpenWrt from the Meraki MR18 page.
Now, disconnect your PC from the internet and connect it to the ethernet of the device. The device acts as a DHCP server as if it were to provide internet access.
Navigate to http://192.168.1.1/, where you can log in and carry out the regular upgrade procedure; in summary:
- “System”
- “Backup/Flash firmware”
- “Flash new firmware image” using the file just downloaded.
Wait for the device to reboot of its own accord, and verify that OpenWrt boots, using the serial console.
Reassembly
You now have a working MR18 with OpenWrt installed! Now you can re-assemble the device. Don't forget to reconnect all of the antennae, including the smaller one which is on the side of the case.
Notes
Some notes here on this type of installation, which you may find useful.
Installation using "sysupgrade"
For the sake of automation it would have been nice to do the installation on the command line. But this didn't appear to work, so falling back to the web interface is necessary. The output is:
$ sysupgrade -n -v openwrt-19.07.2-ar71xx-nand-mr18-squashfs-sysupgrade.tar killall: watchdog: no process killed ubinfo: error!: cannot get information about volume "rootfs_data" on ubi0 error 2 (No such file or directory) Command failed: Request timed out
By contrast, an installation via the web interface looks like:
Watchdog handover: fd=3 - watchdog - killall: telnetd: no process killed killall: dropbear: no process killed Sending TERM to remaining processes ... logd rpcd netifd odhcpd ntpd dnsmasq sh sysupgrade ubus ubusd Sending KILL to remaining processes ... Unlocking kernel ... Writing from <stdin> to kernel ...
Automating the whole OpenOCD process
Ideally it should be possible to script the whole process; reboot the device to a known state and automate the installation. This would require some consistency and reproducability to the steps above, too.
The device didn't appear to respond to system reset pins on the JTAG (after editing raspberrypi-native.cfg). The only method to reset the device appears to be via the watchdog:
mww 0xb806001C 0x01000000
However this sort of reset did not seem to be as 'hard' as removing the power, and was not able to boot the new kernel. Plus, OpenOCD code itself (not configuration) acts on a 1 second pause after the reboot, by which time the device is well into its second stage bootloader.
Ideally, if the process were automated reliably then connecting to the UART would be unnecessary. But then this would require connecting the ground pin from the JTAG which is in a slightly more awkward position.