Using the Radxa Rock Pi N10 NPU on Mainline Linux

| 7 min read

So you ditched the old v4.4 Rockchip vendor kernel in favor of Mainline Linux and now half of your SBC doesn't work! Well, trouble no more, at least I'll help you get a working NPU for accelerated AI inference.

First, you will need to download the latest NPU firmware and matching rknn-api from this official Rockchip repository.

Second, you will need to manually apply the following patch to your Mainline Linux kernel and build it. I should have probably reworked it a bit to not modify rk3399.dtsi and rk3399pro-vmarc-som.dtsi but instead modify only rk3399pro-rock-pi-n10.dts. Also, the PCIe bits are unimportant since the Rock Pi N10 RK3399Pro NPU talks to the CPU over USB, USB2 when in maskrom mode then USB3 after we push the firmware with our modified npu_upgrade script. The RTC commenting out of a check may be uneeded, but make sure you enable the clk modifications. Beware, messing out with your clocks via sysfs without knowing what you are doing may permanently damage your device at the hardware level, so if you're clueless, don't mess around. Most of the commands in this guide require root privileges.

    From: Geraldo Nascimento <geraldogabriel@gmail.com>
	Date: Thu, 31 Aug 2023 01:24:38 -0300
	Subject: [PATCH 9/9] Activate weird options for NPU - Dangerous!
	
	---
	 arch/arm64/boot/dts/rockchip/rk3399.dtsi      |  4 +-
	 .../dts/rockchip/rk3399pro-vmarc-som.dtsi     | 52 +++++++++++++++++--
	 drivers/clk/clk.c                             |  1 +
	 drivers/rtc/rtc-hym8563.c                     |  4 +-
	 4 files changed, 52 insertions(+), 9 deletions(-)
	
	diff --git a/arch/arm64/boot/dts/rockchip/rk3399.dtsi b/arch/arm64/boot/dts/rockchip/rk3399.dtsi
	index be0838c85563..581081ba3698 100644
	--- a/arch/arm64/boot/dts/rockchip/rk3399.dtsi
	+++ b/arch/arm64/boot/dts/rockchip/rk3399.dtsi
	@@ -489,7 +489,7 @@ usbdrd_dwc3_1: usb@fe900000 {
	 			clocks = <&cru SCLK_USB3OTG1_REF>, <&cru ACLK_USB3OTG1>,
	 				 <&cru SCLK_USB3OTG1_SUSPEND>;
	 			clock-names = "ref", "bus_early", "suspend";
	-			dr_mode = "otg";
	+			dr_mode = "host";
	 			phys = <&u2phy1_otg>, <&tcphy1_usb3>;
	 			phy-names = "usb2-phy", "usb3-phy";
	 			phy_type = "utmi_wide";
	@@ -2897,7 +2897,7 @@ pcie_clkreqn_cpm: pci-clkreqn-cpm {
	 
	 			pcie_clkreqnb_cpm: pci-clkreqnb-cpm {
	 				rockchip,pins =
	-					<4 RK_PD0 RK_FUNC_GPIO &pcfg_pull_none>;
	+					<4 RK_PD0 1 &pcfg_pull_none>;
	 			};
	 		};
	 
	diff --git a/arch/arm64/boot/dts/rockchip/rk3399pro-vmarc-som.dtsi b/arch/arm64/boot/dts/rockchip/rk3399pro-vmarc-som.dtsi
	index c487dcb46ad4..58bc25a18953 100644
	--- a/arch/arm64/boot/dts/rockchip/rk3399pro-vmarc-som.dtsi
	+++ b/arch/arm64/boot/dts/rockchip/rk3399pro-vmarc-som.dtsi
	@@ -105,7 +105,7 @@ rk809: pmic@20 {
	 		#clock-cells = <1>;
	 		clock-output-names = "rk808-clkout1", "rk808-clkout2";
	 		pinctrl-names = "default";
	-		pinctrl-0 = <&pmic_int_l>;
	+		pinctrl-0 = <&pmic_int_l>, <&clk_32k>;
	 		rockchip,system-power-controller;
	 		wakeup-source;
	 
	@@ -329,10 +329,10 @@ &i2c2 {
	 	hym8563: rtc@51 {
	 		compatible = "haoyu,hym8563";
	 		reg = <0x51>;
	+		pinctrl-names = "default";
	+		pinctrl-0 = <&hym8563_int>, <&npu_ref_clk>;
	 		#clock-cells = <0>;
	 		clock-output-names = "hym8563";
	-		pinctrl-names = "default";
	-		pinctrl-0 = <&hym8563_int>;
	 		interrupt-parent = <&gpio4>;
	 		interrupts = <RK_PD6 IRQ_TYPE_LEVEL_LOW>;
	 	};
	@@ -385,7 +385,7 @@ &pcie_phy {
	 &pcie0 {
	 	ep-gpios = <&gpio0 RK_PB4 GPIO_ACTIVE_HIGH>;
	 	num-lanes = <4>;
	-	pinctrl-0 = <&pcie_clkreqnb_cpm>;
	+	pinctrl-0 = <&pcie_clkreqnb_cpm>, <&pcie_perst>;
	 	pinctrl-names = "default";
	 	vpcie0v9-supply = <&vcca_0v9>;	/* VCC_0V9_S0 */
	 	vpcie1v8-supply = <&vcca_1v8>;	/* VCC_1V8_S0 */
	@@ -394,6 +394,7 @@ &pcie0 {
	 };
	 
	 &pinctrl {
	+	
	 	hym8563 {
	 		hym8563_int: hym8563-int {
	 			rockchip,pins = <4 RK_PD6 0 &pcfg_pull_up>;
	@@ -410,6 +411,10 @@ pcie {
	 		pcie_pwr: pcie-pwr {
	 			rockchip,pins = <4 RK_PD4 RK_FUNC_GPIO &pcfg_pull_up>;
	 		};
	+
	+		pcie_perst: pcie-perst {
	+			rockchip,pins = <0 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>;
	+		};
	 	};
	 
	 	pmic {
	@@ -435,6 +440,12 @@ usb0_en_oc: usb0-en-oc {
	 			rockchip,pins = <4 RK_PD2 RK_FUNC_GPIO &pcfg_pull_up>;
	 		};
	 	};
	+
	+	npu_clk {
	+		npu_ref_clk: npu-ref-clk {
	+		     rockchip,pins = <0 RK_PA2 1 &pcfg_pull_none>;
	+	     };
	+	};
	 };
	 
	 &pmu_io_domains {
	@@ -469,6 +480,10 @@ &tcphy0 {
	 	status = "okay";
	 };
	 
	+&tcphy1 {
	+	status = "okay";
	+};
	+
	 &tsadc {
	 	rockchip,hw-tshut-mode = <1>;
	 	rockchip,hw-tshut-polarity = <1>;
	@@ -489,10 +504,14 @@ u2phy0_host: host-port {
	 	};
	 };
	 
	-
	 &u2phy1 {
	 	status = "okay";
	 
	+	u2phy1_otg: otg-port {
	+		phy-supply = <&vbus_typec>;
	+		status = "okay";
	+	};
	+
	 	u2phy1_host: host-port {
	 		phy-supply = <&vbus_host>;
	 		status = "okay";
	@@ -519,10 +538,18 @@ &usbdrd3_0 {
	 	status = "okay";
	 };
	 
	+&usbdrd3_1 {
	+	status = "okay";
	+};
	+
	 &usbdrd_dwc3_0 {
	 	status = "okay";
	 };
	 
	+&usbdrd_dwc3_1 {
	+	status = "okay";
	+};
	+
	 &gpu {
	 	status = "okay";
	 };
	@@ -531,6 +558,21 @@ &hdmi_sound {
	 	status = "okay";
	 };
	 
	+/*&spi1 {
	+	status = "okay";
	+
	+	flash@0 {
	+		#address-cells = <1>;
	+		#size-cells = <1>;
	+		compatible = "jedec,spi-nor";
	+		reg = <0x0>;
	+		spi-max-frequency = <40000000>;
	+		spi-cpha;
	+		spi-cpol;
	+		status = "okay";
	+	};
	+};*/
	+
	 &vbus_host {
	 	enable-active-high;
	 	gpio = <&gpio4 RK_PD1 GPIO_ACTIVE_HIGH>; /* USB1_EN_OC# */
	diff --git a/drivers/clk/clk.c b/drivers/clk/clk.c
	index c249f9791ae8..71ce5c69849c 100644
	--- a/drivers/clk/clk.c
	+++ b/drivers/clk/clk.c
	@@ -3309,6 +3309,7 @@ static int clk_dump_show(struct seq_file *s, void *data)
	 DEFINE_SHOW_ATTRIBUTE(clk_dump);
	 
	 #undef CLOCK_ALLOW_WRITE_DEBUGFS
	+#define CLOCK_ALLOW_WRITE_DEBUGFS
	 #ifdef CLOCK_ALLOW_WRITE_DEBUGFS
	 /*
	  * This can be dangerous, therefore don't provide any real compile time
	diff --git a/drivers/rtc/rtc-hym8563.c b/drivers/rtc/rtc-hym8563.c
	index b018535c842b..c14c54a3c4bf 100644
	--- a/drivers/rtc/rtc-hym8563.c
	+++ b/drivers/rtc/rtc-hym8563.c
	@@ -97,11 +97,11 @@ static int hym8563_rtc_read_time(struct device *dev, struct rtc_time *tm)
	 	if (ret < 0)
	 		return ret;
	 
	-	if (buf[0] & HYM8563_SEC_VL) {
	+	/*if (buf[0] & HYM8563_SEC_VL) {
	 		dev_warn(&client->dev,
	 			 "no valid clock/calendar values available\n");
	 		return -EINVAL;
	-	}
	+	}*/
	 
	 	tm->tm_sec = bcd2bin(buf[0] & HYM8563_SEC_MASK);
	 	tm->tm_min = bcd2bin(buf[1] & HYM8563_MIN_MASK);
	-- 
	2.39.0

That's it for the Mainline Linux kernel/dtb parts.

Once you boot with your modified kernel and dtb, set the date with either the date command by running for example date 083116202023, that'll set the date to Thu Aug 31 16:20:00 -03 2023 or optionally do it via ntp. Just make sure your clock is set to current time. Then run hwclock --systohc to write the correct time to the RTC. Don't skip this step.

Moving on, you will need a working copy of libgpiod. Either compíle it yourself or get it from your distro. I compiled my own so I will cd to libgpiod/tools and run the following script, which you can call rk3399pro_npu_powerctl.sh for example:

    echo "1" > /sys/kernel/debug/clk/rk808-clkout2/clk_prepare_enable
	echo "24000000" > /sys/kernel/debug/clk/clk_wifi_pmu/clk_rate
	#echo "0" > /sys/kernel/debug/clk/clk_wifi_pmu/clk_prepare_enable
	./gpioset -zc 0 10=0
	sleep 0.2000
	./gpioset -zc 1 22=0
	./gpioset -zc 1 23=0
	./gpioset -zc 1 24=0
	./gpioset -zc 0 11=0
	./gpioset -zc 0 4=0
	./gpioset -zc 1 3=0
	./gpioset -zc 1 0=0
	sleep 0.2000
	killall -15 gpioset
	./gpioset -zc 0 4=1
	sleep 0.2000
	./gpioset -zc 0 10=1
	sleep 0.2000
	./gpioset -zc 0 11=1
	sleep 0.2000
	echo "1" > /sys/kernel/debug/clk/clk_wifi_pmu/clk_prepare_enable
	./gpioset -zc 1 22=1
	sleep 0.2000
	./gpioset -zc 1 23=1
	sleep 0.2000
	./gpioset -zc 1 24=1
	sleep 2.5000
	./gpioset -zc 1 0=1

This script mostly replicates the functionality of npu_powerctrl binary shipped by Rockchip / Radxa, except with libgpiod gpioset tool instead of deprecated sysfs gpio manipulation.

A point of note: the NPU requires a 24MHz clock or it won't work - it definitely doesn't work with the default WiFi 26MHz clock. I have no idea what happens if you're using the Radxa WiFi module together with this script, so again, beware.

After running the script you should see something similar to this in your dmesg:

    [  119.665991] usb 3-1: new high-speed USB device number 2 using xhci-hcd
    [  119.792904] usb 3-1: New USB device found, idVendor=2207, idProduct=180a, bcdDevice= 1.00
    [  119.792930] usb 3-1: New USB device strings: Mfr=0, Product=0, SerialNumber=0

This means the NPU is active and on Maskrom mode, ready to receive a firmware update.

Next, we will execute the following modified npu_upgrade script. It's a little rough around the edges and it depends on a Rockchip binary called upgrade_tool which I have extracted from the official Radxa image in case you don't want to bother with that - just rename from upgrade_tool.bin to upgrade_tool. Make sure to modify the paths at the script header to match your local machine paths. Usage should be ./npu_upgrade MiniLoaderAll.bin uboot.img trust.img boot.img:

    #!/bin/bash
	PROGRAM=${0##*/}
	
	#reset npu
	#/usr/bin/npu_powerctrl -i
	#/usr/bin/npu_powerctrl -o
	sleep 1
	
	if [ $# -ne 4 ]; then
		echo 'Usage: '$PROGRAM' loader uboot trust boot'
		exit
	fi
	DIR="/pathTo/rknpu/drivers/npu_firmware/npu_fw"
	UPGRADE_TOOL=/pathTo/upgrade_tool
	
	LOADER=$DIR/$1
	UBOOT=$DIR/$2
	TRUST=$DIR/$3
	BOOT=$DIR/$4
	UBOOT_ADDR=0x40000
	TRUST_ADDR=0x40800
	BOOT_ADDR=0x20000
	
	function download_func()
	{
		local RET1=1
		echo 'start to download loader...' >>  /tmp/npu.log
		$UPGRADE_TOOL db $LOADER > /dev/null
		if [ $? -ne 0 ]; then
			echo 'failed to download loader!' >>  /tmp/npu.log
			return $RET1;
		fi
		echo 'download loader ok' >>  /tmp/npu.log
	
		sleep 1
		echo 'start to wait loader...' >>  /tmp/npu.log
		$UPGRADE_TOOL td > /dev/null
		if [ $? -ne 0 ]; then
			echo 'failed to wait loader!' >>  /tmp/npu.log
			return $RET1
		fi
		echo 'loader is ready'  >>  /tmp/npu.log
	
		echo 'start to write uboot...' >>  /tmp/npu.log
		$UPGRADE_TOOL wl $UBOOT_ADDR $UBOOT > /dev/null
		if [ $? -ne 0 ]; then
			echo 'failed to write uboot!' >>  /tmp/npu.log
			return $RET1
		fi
		echo 'write uboot ok' >>  /tmp/npu.log
	
		echo 'start to write trust...' >> /tmp/npu.log
		$UPGRADE_TOOL wl $TRUST_ADDR $TRUST > /dev/null
		if [ $? -ne 0 ]; then
			echo 'failed to write trust!' >>  /tmp/npu.log
			return $RET1
		fi
		echo 'write trust ok' >>  /tmp/npu.log
	
		echo 'start to write boot...' >>  /tmp/npu.log
		$UPGRADE_TOOL wl $BOOT_ADDR $BOOT > /dev/null
		if [ $? -ne 0 ]; then
			echo 'failed to write boot!' >>  /tmp/npu.log
			return $RET1
		fi
		echo 'write boot ok' >>  /tmp/npu.log
		RET1=0
		return $RET1
	}
	
	function check_device_ready_func()
	{
		echo 'start to wait device...' >  /tmp/npu.log
		local i=0
		local RET=1
		while [ $i -lt 10 ]; do
			$UPGRADE_TOOL ld > /dev/null
			if [ $? -ne 0 ]; then
				i=$((i+1))
				echo $i
				sleep 0.1
			else
				sleep 0.1
				break
			fi
			if [ $i -eq 5 ]; then
				/usr/bin/npu_powerctrl -o
				sleep 3
				echo 'reset npu to retry!!!' >> /tmp/npu.log
			fi
		done
		if [ $i -ge 10 ]; then
			echo 'failed to wait device!'  >>  /tmp/npu.log
			return $RET
		fi
		echo 'device is ready' >>  /tmp/npu.log
		RET=0
		return $RET
	}
	
	function poweron_Npu_func()
	{
		/usr/bin/npu_powerctrl -i
		sleep 0.1
		/usr/bin/npu_powerctrl -o
		sleep 1
	}
	
	if [ ! -f $UPGRADE_TOOL ]; then
		echo $UPGRADE_TOOL 'is not existed!'
		exit
	fi
	
	if [ ! -f $LOADER ]; then
		echo $LOADER 'is not existed!'
		exit
	fi
	
	if [ ! -f $UBOOT ]; then
		echo $UBOOT 'is not existed!'
		exit
	fi
	
	if [ ! -f $TRUST ]; then
		echo $TRUST 'is not existed!'
		exit
	fi
	
	if [ ! -f $BOOT ]; then
		echo $BOOT 'is not existed!'
		exit
	fi
	
	check_device_ready_func
	if [ $? = 1 ];then
		echo "check_device_ready error!!!" >> /tmp/npu.log
		poweron_Npu_func
		check_device_ready_func
	fi
	
	download_func
	if [ $? = 1 ];then
		echo "reset download_func"
		echo 'down load error!!! reset usb hub' >> /tmp/npu.log
		#echo 0 > /sys/class/gpio/gpio149/value
		#sleep 3
		#echo 1 > /sys/class/gpio/gpio149/value
	
		#poweron_Npu_func
		#check_device_ready_func
		#download_func
	fi
	
	echo 'start to run system...' >>  /tmp/npu.log
	$UPGRADE_TOOL rs $UBOOT_ADDR $TRUST_ADDR $BOOT_ADDR $UBOOT $TRUST $BOOT > /dev/null
	if [ $? -ne 0 ]; then
		echo 'failed to run system!' >>  /tmp/npu.log
		exit
	fi
	echo 'run system ok' >>  /tmp/npu.log

If it works you should see something similar to the following on your dmesg:

    [  129.886423] usb 3-1: reset high-speed USB device number 2 using xhci-hcd
    [  130.007927] usb 3-1: device descriptor read/64, error -71
    [  130.236951] usb 3-1: device firmware changed
    [  130.239958] usb 3-1: USB disconnect, device number 2
    [  130.354991] usb 3-1: new high-speed USB device number 3 using xhci-hcd
    [  130.483908] usb 3-1: New USB device found, idVendor=2207, idProduct=180a, bcdDevice= 1.00
    [  130.483933] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
    [  130.483940] usb 3-1: Product: USB-MSC
    [  130.483946] usb 3-1: Manufacturer: RockChip
    [  130.483952] usb 3-1: SerialNumber: rockchip
    [  135.170210] usb 3-1: USB disconnect, device number 3
    [  137.859118] usb 4-1: new SuperSpeed USB device number 2 using xhci-hcd
    [  137.873716] usb 4-1: New USB device found, idVendor=2207, idProduct=0019, bcdDevice= 4.04
    [  137.873740] usb 4-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
    [  137.873747] usb 4-1: Product: rk3xxx
    [  137.873753] usb 4-1: Manufacturer: rockchip
    [  137.873805] usb 4-1: SerialNumber: 526aef7c692b0fed

This means it worked. npu_transfer_proxy devices should show attached NPU via USB3 and npu_tranfer_proxy will now work and the matching rknn-api C Demos should work.

Happy AI Hacking!