Sonoff NSPanel Pro 120: Loader and Maskrom mode

Sonoff NSPanel Pro 120: Loader and Maskrom mode

MaskROM and Loader mode are special Rockchip modes that allow a user to read/write data to memory using USB. MaskROM is internal firmware that runs on the Rockchip SoC when no bootable device is found or when a special pin is tied to ground. Since MaskROM is built into the SOM, it is always available, ensuring that the device could be recovered if bricked. However, MaskROM requires a special miniLoader uploaded into RAM to function.

Loader mode, on the other hand, is not built into the Rockchip SOM; it is a special mode of U-Boot that allows read/write access. If the device is unable to load into U-Boot, then Loader mode will be unavailable.

In this article, we will identify the pads for Loader and MaskROM modes and connect them to an FT232RL to enable easy control via a Python script:

  • Reboot
  • Reboot into Loader Mode
  • Reboot into MaskROM Mode

Reboot

In order to reboot the NSPanel RST should be connected to GND

Loader Mode

In order to reboot the NSPanel into Loader mode UBOOT should be connected to GND. To enter Loader mode, pull both the RST and UBOOT lines simultaneously. Then, release RST while keeping the UBOOT line held low for a short period before releasing it.

MaskRom Mode

In order to reboot the NSPanel into MaskROM mode, D0 must be connected to GND. I don't have an oscilloscope to verify, but it appears that this is the DAT0 line from the eMMC. Shorting it to GND stops the communication between the RK3326 and the EMMC, causing the device to enter MaskROM mode. To enter MaskROM mode, pull both the RST and D0 lines simultaneously. Then, release RST while keeping the D0 line held low for a short period before releasing it.

UART Console

Also, to view the boot log, you need to connect to the UART console. The baud rate is 1500000 (8N1), and the logic level is 1.8V.

Ok, so the final setup looks like this: it includes a USB cable, RST, Loader, and MaskROM signals, as well as a UART console. The module uses optocouplers to isolate the controlling voltages from the FT232RL and effectively acts as a switch by connecting the RST, Loader, and MaskROM pins to GND.

import ftd2xx
import time
import sys
import argparse
from enum import Enum, auto

class PINNames(Enum):
    TXD = 0  # PIN_1 | D0
    RXD = 1  # PIN_5 | D1   Loader
    RTS = 2  # PIN_3 | D2   Maskrom
    CTS = 3  # PIN_11| D3   RESET
    DTR = 4  # PIN_2 | D4
    DSR = 5  # PIN_9 | D5
    DCD = 6  # PIN_10| D6
    RI  = 7  # PIN_6 | D7

class FT232RLDevice:
    def __init__(self, device_index=0):
        try:
            self.dev = ftd2xx.open(device_index)
            print("FT232RL device opened successfully.")
            self.previous_state = 0x00  # Initialize all pins to LOW
            self.dev.setBitMode(0x00, 0x01)  # Set specific pins as outputs later
            print("Bit mode set to asynchronous bit-bang mode.")
        except ftd2xx.DeviceError as e:
            print(f"No FT232RL device found or an error occurred: {e}")
            sys.exit(1)
    
    def write_pin(self, pin: PINNames, value: bool):
        """
        Set or clear a specific pin while remembering the previous state.
        
        Args:
            pin (PINNames): The pin to modify.
            value (bool): True to set HIGH, False to set LOW.
        """
        if not isinstance(pin, PINNames):
            raise ValueError("Invalid pin name provided.")
        
        bit_position = pin.value
        print(f"Modifying pin {pin.name} at bit position {bit_position} to {'HIGH' if value else 'LOW'}.")

        if value:
            self.previous_state |= (1 << bit_position)  # Set the bit
        else:
            self.previous_state &= ~(1 << bit_position)  # Clear the bit
        
        try:
            self.dev.write(bytes([self.previous_state]))
            print(f"Updated state written to device: {bin(self.previous_state)}")
        except ftd2xx.DeviceError as e:
            print(f"Failed to write to device: {e}")
    
    def set_pins_as_output(self, pins):
        """
        Set specific pins as output by configuring the bitmask.
        
        Args:
            pins (list of PINNames): The pins to set as output.
        """
        bitmask = 0x00
        for pin in pins:
            bitmask |= (1 << pin.value)
        try:
            self.dev.setBitMode(bitmask, 0x01)  # 0x01 for asynchronous bit-bang mode
            print(f"Pins {[pin.name for pin in pins]} set as OUTPUT.")
        except ftd2xx.DeviceError as e:
            print(f"Failed to set bit mode: {e}")
            sys.exit(1)
    
    def close(self):
        try:
            # self.dev.setBitMode(0x00, 0x00)  # Reset bit mode to reset state
            self.dev.close()
            print("FT232RL device closed.")
        except ftd2xx.DeviceError as e:
            print(f"Failed to close device: {e}")

def reboot_sonoff(device: FT232RLDevice):
    """
    Reboots the Sonoff device by toggling CTS pin.
    
    Args:
        device (FT232RLDevice): The FT232RL device instance.
    """
    print("Starting Sonoff reboot sequence.")
    
    # Set CTS high
    device.write_pin(PINNames.CTS, 1)
    print("CTS set to HIGH.")
    
    # Wait for 1 second
    time.sleep(1)
    
    # Set CTS low
    device.write_pin(PINNames.CTS, 0)
    print("CTS set to LOW.")
    print("Sonoff reboot sequence completed.")

def reboot_to_maskrom_mode(device: FT232RLDevice):
    """
    Reboots the Sonoff device into Maskrom mode by manipulating RTS and CTS pins.
    
    Args:
        device (FT232RLDevice): The FT232RL device instance.
    """
    print("Starting Sonoff reboot to Maskrom mode sequence.")
    
    # Set RTS high
    device.write_pin(PINNames.RTS, 1)
    print("RTS set to HIGH.")
    
    reboot_sonoff(device)
    
    # Wait for 1 second
    time.sleep(1)
    
    # Set RTS low
    device.write_pin(PINNames.RTS, 0)
    print("RTS set to LOW.")
    print("Sonoff reboot to Maskrom mode sequence completed.")

def reboot_to_loader_mode(device: FT232RLDevice):
    """
    Reboots the Sonoff device into Loader mode by manipulating RXD and CTS pins.
    
    Args:
        device (FT232RLDevice): The FT232RL device instance.
    """
    print("Starting Sonoff reboot to Loader mode sequence.")
    
    # Set RXD high
    device.write_pin(PINNames.RXD, 1)
    print("RXD set to HIGH.")

    time.sleep(0.5)
    
    reboot_sonoff(device)
    
    # Wait for 1 second
    time.sleep(2)
    
    # Set RXD low
    device.write_pin(PINNames.RXD, 0)
    print("RXD set to LOW.")
    print("Sonoff reboot to Loader mode sequence completed.")

def parse_arguments():
    """
    Parses command-line arguments.
    
    Returns:
        int: The action code (0, 1, or 2).
    """
    parser = argparse.ArgumentParser(description="Control FT232RL GPIO pins for Sonoff device reboot.")
    parser.add_argument(
        'action',
        type=int,
        choices=[0, 1, 2],
        help='Action to perform: 0 - Reboot, 1 - Reboot to Loader, 2 - Reboot to Maskrom'
    )
    args = parser.parse_args()
    return args.action

def main():
    # Parse command-line arguments
    action = parse_arguments()

    # Instantiate the FT232RL device
    device = FT232RLDevice()

    # Define which pins to set as output
    output_pins = [PINNames.RXD, PINNames.RTS, PINNames.CTS]
    device.set_pins_as_output(output_pins)
    
    try:
        # Initialize all relevant pins to Low
        device.write_pin(PINNames.RXD, 0)
        device.write_pin(PINNames.RTS, 0)
        device.write_pin(PINNames.CTS, 0)
        print("Initialized RXD, RTS, CTS to HIGH.")
        
        # Perform action based on argument
        if action == 0:
            reboot_sonoff(device)
        elif action == 1:
            reboot_to_loader_mode(device)
        elif action == 2:
            reboot_to_maskrom_mode(device)
        else:
            print("Invalid action. Use 0, 1, or 2.")
    
    except Exception as e:
        print(f"An error occurred during GPIO operations: {e}")
    
    finally:
        device.close()

if __name__ == '__main__':
    main()

Finally to reboot we just execute python3 sonoff.py 0, and to get to MasROM mode python3 sonoff.py 2

Thanks for your attention. We will compile and upload the Linux firmware in the next article.