Switch From Scratch
I recently received went ahead and bought yet another variation of the Nintendo
Switch. This one is different though.
.
It's a devkit for the Tegra X1, the SoC underlying the Nintendo Switch. I originally got this new piece of hardware to port KFS, my Horizon/NX kernel reimplementation, to ARM64. But having just finished the god-awful PR to bring documentation coverage to 100%, I needed a break from the Kernel and some time to work on something silly.
So I have this devkit for the same SoC that the Nintendo Switch OS, Horizon/NX, is built for. Soo... How hard could it be to run the original Horizon/NX on this thing? This might sound like a silly idea at first, but there are some really good reason to do this. The Jetson TX1 has UART exposed and very easily connected to, providing a very simple way to get debug output. It also sports a JTAG debugger, which might allow debugging the kernel, trustzone, and everything in-between (although JTAG debuggers for the Cortex-A57 are ridiculously expensive).
So with this silly idea in mind, I went ahead...
First steps
The first thing I did was try booting Hekate with Fusee-Gelee. FG is absolutely useless on a Jetson-TX1: RCM is usable and in fact the first thing we're kind of supposed to do when receiving a Jetson-TX1 is use RCM mode to flash an up-to-date Linux.
So anyways, I went ahead and ran fusee-launcher with Hekate. At first, fusee-launcher wouldn't find the Jetson-TX1, but that was easily fixed: the devkit uses a different USB VID/PID pair than the Nintendo Switch. Fusee-Launcher is already well-equipped to deal with this:
# python3 fusee-launcher.py -V 955 -P 7721 hekate-weird
Setting ourselves up to smash the stack...
Uploading payload...
Smashing the stack...
The USB device stopped responding-- sure smells like we've smashed its stack. :)
Launch complete!
No way, it works! But... no output on the UART console. Looks like hekate just froze. What's wrong? I dug into the source code, and discovered it's supposed to print a message to UART very early on:
void ipl_main()
{
config_hw();
//Pivot the stack so we have enough space.
pivot_stack(0x90010000);
//Tegra/Horizon configuration goes to 0x80000000+, package2 goes to 0xA9800000, we place our heap in between.
heap_init(0x90020000);
uart_send(DEBUG_UART_PORT, (u8 *)"Hekate: Hello!\r\n", 18);
// ...
}
Four functions in and I was failing already. I figured I needed a debugging primitive so I could figure out if the flow of execution reached one of those points. I remembered that there exists a register I could write to in order to cause the CPU to reboot to RCM. I could probably use this to figure out if I reached some code or not:
// Stolen shamelessly from stuckpixel's reboot_to_rcm.
void reboot_to_rcm() {
u64 virt_addr = 0x7000E400;
u64 scratch0 = virt_addr + 0x50; //Scratch register 0 is at PMC base + 0x50
*(u32 *)scratch0 = 1 << 1; //Bit 1 set to 1 makes the SoC go into RCM on reboot
*(u32 *)virt_addr |= 1 << 4; //reboot without clearing scratch 0
while(1); //we should never reach here
}
I put this function right at the start of the main, and lo and behold, we
rebooted to RCM. Good, so at least we do reach Hekate's ipl_main! By moving the
reboot_to_rcm around, I pin-pointed the culprit to when I was returning from
heap_init. At that point, I started asking around, and CTCaer immediately had
the right insight: the SDRAM configuration must be wrong.
You see, before using the RAM, it needs to be configured. When hekate starts,
it's using IRAM (integrated RAM). config_hw configures various parts of the
SoC, such as the DRAM, and pivot_stack moves the stack to the DRAM. But if the
DRAM was misconfigured, we might end up freezing here.
After a bit of fighting, I managed to extract the SDRAM configuration from the Linux installed on the Jetson-TX1. It is stored in the BCT, and the UART console tells us which entry in the BCT (there are multiple config) it is using. By copying the config into hekate, I managed to get DRAM working!
At this point, hekate worked, but I had no way to interface with it. I had to
write a bit of code that redirected the GUI to the UART so I could dig into the
menus. I didn't spend too much time on it, so it can be a bit buggy.

The sdMMC
The next step was trying to "launch" CFW. I was quickly met with "Failed to mount SDcard". Ugh. CTCaer, once again, had the right insight immediately: "gpio/pinmux nightmare". This wasn't immediately clear to me, but a bit of googling later, I discovered that Nvidia actually had a bit of hardware to allow making each GPIO pin service multiple hardwares. The pinmuxing hardware is in charge of configuring which device a GPIO pin is currently targetting, among other thing.
I spent a looooooot of time staring at the DTS and the _sdmmc_config_sdmmc1
function responsible for setting up the pinmuxing hardware. After several hours
staring at the code and comparing various DTS (notably comparing the jetson TX1
and F0F Switch-Linux Device Trees), I finally figured out that the Enable SD Card
Power pin was indeed different: the Nintendo Switch uses GPIO Port E Pin 4,
whereas the Jetson TX1 uses GPIO Port Z Pin 3.
A quick fix later, and sdMMC was working properly!
Installing Horizon/NX
At this point, I have a fully working Hekate (at least as far as I can see). The next step is going to be to install Horizon/NX on there. I was suddenly hit by a pleasent and an unpleasent surprise: the eMMC was working properly, but it was only 16GB large (vs 32GB on the Switch). This meant I couldn't just take a NAND backup and restore it on the Jetson TX1 eMMC, I'd have to muck around with the GPT. And I knew from previous conversations with rajkosto that the GPT on the switch can be a bit capricious...
I went ahead and took a dump of a real switch in order to access the real GPT. I also got a dump of the GPT table of the normal Jetson-TX1 Linux install. I then went ahead and compared the two. Mostly, what I needed to do was to take the Switch GPT, and change the max_lba (last usable block on disk) and the size of the USER partition so they coincided with the GPT from the Jetson-TX1. And of course, recalculate the CMAC32. Nothing a hex editor couldn't do in a couple minutes... After a few hours of work, I had a GPT partition I could flash!
Meanwhile, I started looking for how I was going to actually access the NAND. @Thog suggested booting u-boot from RCM in order to gain access to UMS (if you've ever used memloader to mount your switch's eMMC on your computer, that's actually u-boot you were using). He even got me a script that would use the Jetson Driver Kit to boot u-boot from RCM <3.
Thanks to this, I could mess around the Jetson's EMMC as if it was a USB on my computer, and even if I messed up the partitioning super badly, I could always flash a new firmware and be good to go. So I went ahead and flashed the GPT:
cat jetsontx1.gpt > /dev/sdc
And loaded the disk into HacDiskMount... Fun discovery: GPT actually has its headers in two different location in order to fight corruption! And HacDiskMount helpfully told me that the primary and secondary GPT weren't matching. I fixed this by opening the disk in fdisk and letting it write the (existing) partition scheme. That would overwrite the secondary GPT with the correct data from primary.
We now have our switch partitioned. Let's get to the flashing! I got ChoiDuJour
to generate all the files I'd need to flash, and went ahead and flashed the
various BCPKG2* partitions, as those required no encryption. I then faced two
problems: I needed to encrypt the other partitions with the BIS Keys, which I did
not have, and I needed to flash the BOOT0 partition, which contained the FUSE
burning code...
I ReFuse
I'm absolutely horrified of the idea that I might accidentally burn a fuse. That would be extremely annoying, even moreso now that SciresM gave me an awesome insight:
SciresM: @roblâbla burnt fuses of zero will allow you to boot early early 1.0.0s
SciresM: (pre 1.0.0-7)
SciresM: so that is pretty cool :)
Oh fuck. That is pretty cool. Now there's a ton of pressure to avoid burning the fuses...
The BOOT0 contains many important things. It contains the package1ldr, the first piece of code to run after the bootROM, which contains the fuse burning logic. It also contains the NX-Bootloader, the Secure Monitor, and the Warmboot. And most importantly, it contains the Keyblobs, which are the master key of the kingdom. Most of those components need to be there for Hekate to successfully boot Horizon/NX.
Also, Keyblobs need to be encrypted too! And somewhat annoyingly, they need the TSEC key (same key necessary to derive the BIS Keys), which requires package1 to be present to be dumped... What a Catch-22. With the help of shchmue, I did a bit of hacky patchwork around the hekate function to dump the TSEC Key to use a package1 file from SD Card instead of attempting to read it from the BOOT0. Thanks to this (and a few hours of debugging later), I got both the TSEC key and the BIS key, which would unlock the situation.
Encrypting the keyblobs turned out to be a bit complicated. Nobody had written
any code to do it. Furthermore, I was faced with a bit of a fun problem: the SBK
on a Jetson TX1 is 00000000000000000000000000000000, and I was intending to
keep it that way. But [hactool] doesn't like that key. It's using it as a marker
for "no key present" in its code. So I wrote a bit of code for [linkle], my
switch multitool, to generate encrypted keyblobs from a keyblob, a
keyblob_key and a keyblob_mac_key. I then modded ChoiDuJour to have it
generate a complete BOOT0 using those keyblobs.
I now had a BOOT0 ready for use on my switch, but there remained a problem: it was a dangerous BOOT0 containing fuse-burning code. I had to do something about this. I decided to take the nuclear option: I zeroed out both instances of the Package1Ldr's .text section in the BOOT0. This section is unused anyways, since hekate fully replaces the bootloader. With this done, I could flash the BOOT0.
I booted ums on the mmc 0.1 partition in order to access BOOT, and started
flashing:
cat BOOT0 > /dev/sdc
Now I just had to reboot hekate...
Initializing...
Identified pkg1 ('20161121183008'),
Keyblob version 0
Loaded pkg1 and keyblob
Generated keys
Decrypting pkg1
Unpacking pkg1
Unpacking warmboot from 90025610 to 8000D000 size 65504
Skipping 1
Unpacking warmboot from 90045534 to 8000D000 size 3828
Decrypted and unpacked pkg1
Patching Warmboot
Patching Security Monitor
Loaded warmboot.bin and secmon
Read pkg2
Parsed ini1
Patching kernel initial processes
Rebuilt and loaded pkg2
Booting...
Success \o/. It'd be nice to verify that we have an actually working kernel though. I went ahead and enabled debug_mode in the hekate IPL, which gave me this:
Break() called. 0100000000000002
Success \o/. NCM is failing, probably because I didn't flash the SYSTEM partition. But we're getting into the kernel, and even getting in userland!
Brick 1
This is where things went south. For some reason, after doing things, my Jetson TX1 started bugging. It started with a life-wrecking experience where it would not respond to RCM, no output on UART, and the CR2 LED (indicating the 3v3/1v8 power rail) was off... I had somehow bricked my Jetson TX1. Welp.
I did a bit of experimenting, and after making sure everything worked correctly with a multimeter, I realized something odd: the 1v8 rail had a power output of 4.8V. That is not normal. Filled with sadness, I decided to RMA the unit to get a replacement.
Attempt 2
Date: 2019-02-15
Received the new board. Got an extra camera module. Nice. Not sure what I'll do with that yet.
After talking to CTCaer a bit more, it was brought to my attention that the PMIC (the thing that manages power on the Jetson and Switch) needed to be configured properly for the board. If it's misconfigured, it might end up sending incorrect voltages to the various devices, potentially frying them! :scream:.
Hekate's PMIC configuration is a bit messy. The good news is, there is a central driver through which everything should go through, in the [max7762x.c]. Bad news is that a lot of Hekate's code kinda just manually talk to the chip, issuing raw I2C commands instead of going through the driver. Especially in the config_hw function.
So here's the plan:
- I'm going to change all those raw
i2c_send_bytecalls to the appropriatemax77620_regulator_set_voltagecalls. - I'm going to fix the
_pmic_regulatorsglobal so that it matches the Jetson TX1 instead of the Nintendo Switch. I'll figure out the correct value using [nvidia's Device Tree]. - Finally, I'm going to need to find all the Pinmux Configuration from hekate, and make sure they are correct as well, since a wrong pinmux configuration might lead to sending the wrong voltage levels to a device. For the pinmux configuration, nvidia has a nice repository with the configs for various boards and script to generate the header files for the different projects using them (linux, uboot, etc...). I'll take the values from there.
Sidenote: Nvidia's naming convention for the boards is, erm, fun. Evidently, my
board is a Jetson TX1, and the "id" for it is a p2371-2180, based on the fact
that in the driver package, the jetson-tx1.conf script is symlinked to the
p2371-2180-devkit.conf file. However, that same file also uses the p2597-2180
DTS... So I guess those two are equivalent.
Thanks
Many thanks to everyone who helped me out here! I wouldn't have done this without you <3.
- Thog and Ac_K, for helping me and listening to my endless rambling :P
- CTCaer, helped me a bunch with the initial hekate debugging
- shchmue, helped me figure out how to dump the TSEC Keys
- And everyone else that motivated me to get this working!
Tools
You can find all the modifications I've made here:
- hekate: https://github.com/roblabla/hekate (missing UART GUI)
- linkle: https://github.com/megaton-hammer/linkle