StarPro64 EIC7700X RISC-V SBC: Maybe LLM on NPU on NuttX?

📝 2 Mar 2025

StarPro64 EIC7700X RISC-V SBC: Maybe LLM on NPU on NuttX?

StarPro64 EIC7700X is the (literally) Hot New RISC-V SBC by PINE64. In this article we chat about…

We begin with the RISC-V SoC…

(Watch the NuttX Demo on YouTube)

(Thanks to PINE64 for providing the Prototype StarPro64)

StarPro64 EIC7700X RISC-V SBC

§1 ESWIN EIC7700X RISC-V SoC

StarPro64: Isn’t it a souped-up Star64?

Nope it’s a totally different beast! (From a different SoC Maker)

Inside StarPro64 is the ESWIN EIC7700X SoC. EIC7700X has Four RISC-V Cores and it’s based on SiFive Architecture (a bit like JH7110 SoC)

ESWIN EIC7700X SoC

But its super-speedy Neural Processing Unit (NPU) makes it a very special (llama?) beast. Later we’ll talk about the Fun LLM Experiments that we can run on the NPU.

(20 TOPS INT8 = 20 Trillion Ops Per Second for 8-bit Integers)

ESWIN EIC7700X NPU

EIC7700X Technical Reference Manual is probably the best among the RISC-V SoCs (BL808, SG2000, JH7110)

We go hands-on…

Connecting USB UART Dongle to StarPro64

§2 Boot Without MicroSD

What happens if we boot StarPro64? Fresh from the box?

We monitor the UART0 Port for Debug Messages. Connect our USB UART Dongle (CH340 or CP2102) to these pins (pic above)

StarPro64USB UARTColour
GND (Pin 6)GNDYellow
TX (Pin 8)RXBlue
RX (Pin 10)TXGreen

(Same Pins as the GPIO Header on Oz64 SG2000 and Star64 JH7110)

Connect to the USB UART at 115.2 kbps

screen /dev/ttyUSB0 115200

Power up the board with a Power Adapter. (Same one as Star64 JH7110)

We’ll see OpenSBI

OpenSBI v1.5
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|
Platform Name             : ESWIN EIC7700 EVB
Platform Features         : medeleg
Platform HART Count       : 4
Platform Console Device   : uart8250
Firmware Base             : 0x80000000

Domain0 Boot HART         : 2
Domain0 HARTs             : 0*,1*,2*,3*
Domain0 Next Address      : 0x0000000080200000

Boot HART ID              : 2
Boot HART Base ISA        : rv64imafdchx
Boot HART ISA Extensions  : sscofpmf,zihpm,sdtrig
Boot HART MIDELEG         : 0x0000000000002666
Boot HART MEDELEG         : 0x0000000000f0b509

Then U-Boot Bootloader

U-Boot 2024.01-gaa36f0b4 (Jan 23 2025 - 02:49:59 +0000)
CPU:     rv64imafdc_zba_zbb
Model:   ESWIN EIC7700 EVB
DRAM:    32 GiB (effective 16 GiB)
llCore:  143 devices, 31 uclasses, devicetree: separate
Warning: Device tree includes old 'u-boot,dm-' tags: please fix by 2023.07!
MMC:    sdhci@50450000: 0, sd@50460000: 1

Loading Environment from SPIFlash...
SF: Detected w25q128fw with page size 256 Bytes, erase size 4 KiB, total 16 MiB
*** Warning - bad CRC, using default environment
No SATA device found!
Hit any key to stop autoboot:  0
=>

And it stops at U-Boot, waiting to boot from MicroSD or eMMC. Let’s init our eMMC…

(See the Boot Log)

HDMI Output will show U-Boot, but not OpenSBI

HDMI Output will show U-Boot, but not OpenSBI

§3 Download the Linux Image

Is there a Linux Image for StarPro64?

The fine folks at PLCT Lab RockOS are busy preparing the Linux Image for StarPro64. Thanks to @icenowy, we have a Preview Version of the Linux Image…

  1. Bootloader (OpenSBI + U-Boot)

    bootloader_secboot_ddr5_pine64-starpro64.bin

  2. Linux Boot Image (Linux Kernel)

    boot-rockos-20250123-210346.ext4.zst

  3. Linux Root Image (Linux Filesystem)

    root-rockos-20250123-210346.ext4.zst

Uncompress the files and rename them. Copy them to a USB Drive (not MicroSD)

$ ls -lh *.bin *.zst
4.2M  bootloader_secboot_ddr5_pine64-starpro64.bin
154M  boot-rockos-20250123-210346.ext4.zst
2.3G  root-rockos-20250123-210346.ext4.zst

$ unzstd boot-rockos-20250123-210346.ext4.zst
boot-rockos-20250123-210346.ext4.zst: 524288000 bytes

$ unzstd root-rockos-20250123-210346.ext4.zst
root-rockos-20250123-210346.ext4.zst: 7516192768 bytes

$ mv boot-rockos-20250123-210346.ext4 boot.ext4
$ mv root-rockos-20250123-210346.ext4 root.ext4

$ ls -lh *.bin *.ext4
4.2M  bootloader_secboot_ddr5_pine64-starpro64.bin
500M  boot.ext4
7.0G  root.ext4

$ cp *.bin *.ext4 /media/$USER/YOUR_USB_DRIVE

We’ll skip the MicroSD Image, because MicroSD Interface wasn’t working reliably on our Prototype StarPro64.

StarPro64 with eMMC

§4 Prepare the Linux Image

How to load the Linux Image into eMMC?

Based on the ESWIN Official Doc

  1. Connect our eMMC to StarPro64 (pic above)

  2. Connect our USB Drive (Previous Section)

  3. At U-Boot: Press Ctrl-C until U-Boot stops

  4. Verify that the eMMC is OK

    $ ls mmc 0
    [ Nothing ]
    
    $ mmc part
    [ Nothing ]
  5. First Time Only: GPT Partition our eMMC…

    $ echo $partitions
    partitions=
      name=boot,start=1MiB,size=2048MiB,type=${typeid_filesystem},uuid=${uuid_boot};
      name=swap,size=4096MiB,type=${typeid_swap},uuid=${uuid_swap};
      name=root,size=-,type=${typeid_filesystem},uuid=${uuid_root}
    
    $ run gpt_partition
    $ mmc part
      1 0x00000800 0x001007ff "boot"
      2 0x00100800 0x009007ff "swap"
      3 0x00900800 0x0e677fde "root"
  6. Verify that our USB Drive works…

    $ ls usb 0
     524288000 boot.ext4
    7516192768 root.ext4
       4380760 bootloader_secboot_ddr5_pine64-starpro64.bin   
  7. Install the Bootloader, Boot Image and Root Image, from USB Drive to eMMC…

    $ es_fs update usb 0 boot.ext4 mmc 0:1
    mmc has been successfully writen in mmc 0:1
    
    $ es_fs update usb 0 root.ext4 mmc 0:3
    mmc has been successfully writen in mmc 0:3
    
    $ ext4load usb 0 0x100000000 bootloader_secboot_ddr5_pine64-starpro64.bin
    4380760 bytes read in 162 ms (25.8 MiB/s)
    
    $ es_burn write 0x100000000 flash
    bootloader write OK

    (See the eMMC Log)

  8. Beware of Overheating! Keep StarPro64 cool, or the previous step might corrupt the SPI Boot Flash and cause unspeakable agony…

StarPro64 with USB Fan

§5 StarPro64 Gets Smokin’ Hot!

Something is smelling like barbecue?

Whoa StarPro64 is on fire: Drop it, stop it and power off! StarPro64 will show PLL Errors when it overheats…

pll failed.
pll failed.
pll failed.

Also watch for Thermal Errors when booting Linux…

thermal thermal_zone0: thermal0:
critical temperature reached, shutting down
reboot: HARDWARE PROTECTION shutdown (Temperature too high)

Install a USB Fan, preferably something stronger. (Pic above, boxed up with IKEA 365+)

But don’t power it with the USB Port on StarPro64! Instead, connect it to our Smart Power Plug.

Anything else we should worry about?

The MicroSD Interface wasn’t working well on our Prototype StarPro64. The MicroSD Card deactivated itself after a bit of U-Boot Access.

Hence the Headless Ironman: USB Drive on StarPro64…

Headless Ironman: USB Drive on StarPro64

§6 Boot the Linux Image

Earlier we flashed Linux to eMMC. Can we boot Linux now?

Yep just power up StarPro64. eMMC will Boot Linux

U-Boot menu
1:      RockOS GNU/Linux 6.6.73-win2030
2:      RockOS GNU/Linux 6.6.73-win2030 (rescue target)
Enter choice: 1:        RockOS GNU/Linux 6.6.73-win2030
Retrieving file: /vmlinuz-6.6.73-win2030
Retrieving file: /initrd.img-6.6.73-win2030
append: root=PARTUUID=b0f77ad6-36cd-4a99-a8c0-31d73649aa08 console=ttyS0,115200 root=PARTUUID=b0f77ad6-36cd-4a99-a8c0-31d73649aa08 rootfstype=ext4 rootwait rw earlycon selinux=0 LANG=en_US.UTF-8

Retrieving file: /dtbs/linux-image-6.6.73-win2030/eswin/eic7700-pine64-starpro64.dtb
   Uncompressing Kernel Image
Moving Image from 0x84000000 to 0x80200000, end=81e63000
## Flattened Device Tree blob at 88000000
   Booting using the fdt blob at 0x88000000
Working FDT set to 88000000
ERROR: reserving fdt memory region failed (addr=fffff000 size=1000 flags=4)
   Using Device Tree in place at 0000000088000000, end 0000000088027af4
Working FDT set to 88000000

Starting kernel ...
Linux version 6.6.73-win2030 (riscv@riscv-builder) (riscv64-unknown-linux-gnu-gcc () 13.2.0, GNU ld (GNU Binutils) 2.42) #2025.01.23.02.46+aeb0f375c SMP Thu Jan 23 03:08:39 UTC 2025
Machine model: Pine64 StarPro64
...
mmc0: Timeout waiting for hardware interrupt.
mmc0: sdhci: ============ SDHCI REGISTER DUMP ===========
mmc0: sdhci: Sys addr:  0x00000008 | Version:  0x00000005
mmc0: sdhci: Blk size:  0x00007200 | Blk cnt:  0x00000000

Sadly the Preview Version of RockOS won’t boot correctly on our Prototype StarPro64 (pic below). Hopefully we’ll sort this out real soon and do some Serious NPU LLM!

(See the Boot Log)

RockOS won’t boot correctly on our Prototype StarPro64

§7 Settings for U-Boot Bootloader

Bummer. What else can we boot on StarPro64?

Let’s snoop around U-Boot Bootloader. And figure out how to boot Apache NuttX RTOS.

Power up StarPro64 and press Ctrl-C until U-Boot stops. At the U-Boot Prompt: We enter these commands…

$ help
printenv  - print environment variables
saveenv   - save environment variables to persistent storage
net       - NET sub-system
dhcp      - boot image via network using DHCP/TFTP protocol
tftpboot  - load file via network using TFTP protocol
fdt       - flattened device tree utility commands
booti     - boot Linux kernel 'Image' format from memory

$ printenv
fdt_addr_r=0x88000000
kernel_addr_r=0x84000000
loadaddr=0x80200000

(See the U-Boot Log)

A-ha! This says…

Thanks U-Boot! You told us everything we need to Boot NuttX…

Booting NuttX over TFTP

§8 Boot NuttX over TFTP

How to boot NuttX over TFTP? (Pic above)

  1. Install our TFTP Server: Follow the instructions here

  2. Copy these files to our TFTP Server…

    NuttX Image: Image

    Device Tree: eic7700-evb.dtb

    Like so…

    ## Download the NuttX Image and Device Tree
    wget https://github.com/lupyuen2/wip-nuttx/releases/download/starpro64-1/Image
    wget https://github.com/lupyuen/nuttx-starpro64/raw/refs/heads/main/eic7700-evb.dtb
    
    ## Copy the NuttX Image and Device Tree to our TFTP Server
    scp Image tftpserver:/tftpboot/Image-starpro64
    scp eic7700-evb.dtb tftpserver:/tftpboot/
    ssh tftpserver ls -l /tftpboot/

    (How to Build NuttX ourselves)

    (NuttX won’t read the Device Tree)

  3. Power up StarPro64 and press Ctrl-C until U-Boot stops

  4. At the U-Boot Prompt: Enter these commands…

    ## Check if the Network Adapter is alive
    ## "eth0 : ethernet@50400000 f6:70:f9:6e:73:ae active"
    net list
    
    ## Set the U-Boot TFTP Server
    ## TODO: Change to your TFTP Server
    setenv tftp_server 192.168.31.10
    
    ## Save the U-Boot Config for future reboots
    saveenv
    
    ## Fetch the IP Address over DHCP
    ## Load the NuttX Image from TFTP Server
    ## kernel_addr_r=0x84000000
    dhcp ${kernel_addr_r} ${tftp_server}:Image-starpro64
    
    ## Load the Device Tree from TFTP Server
    ## fdt_addr_r=0x88000000
    ## TODO: Fix the Device Tree, it's not needed by NuttX
    tftpboot ${fdt_addr_r} ${tftp_server}:eic7700-evb.dtb
    
    ## Set the RAM Address of Device Tree
    ## fdt_addr_r=0x88000000
    ## TODO: Fix the Device Tree, it's not needed by NuttX
    fdt addr ${fdt_addr_r}
    
    ## Boot the NuttX Image with the Device Tree
    ## kernel_addr_r=0x84000000
    ## fdt_addr_r=0x88000000
    ## TODO: Fix the Device Tree, it's not needed by NuttX
    booti ${kernel_addr_r} - ${fdt_addr_r}

    (U-Boot dropping chars? Try iTerm > Edit > Paste Special > Paste Slowly)

  5. NuttX boots OK on StarPro64 and passes OSTest yay! (Pic below)

    NuttShell (NSH) NuttX-12.4.0
    nsh> uname -a
    NuttX 12.4.0 83424f8d26 Feb 24 2025 06:50:22 risc-v starpro64
    
    nsh> hello
    Hello, World!!
    
    nsh> getprime
    getprime took 148 msec    
    
    nsh> ostest
    ostest_main: Exiting with status 0

    (See the NuttX Log)

    (Watch the Demo on YouTube)

  6. How did we port NuttX to StarPro64? Check the details here…

    “Port NuttX to StarPro64”

NuttX boots OK on StarPro64 yay!

We type these commands EVERY TIME we boot?

We can automate: Just do this once, and NuttX will Auto-Boot whenever we power up…

## Add the Boot Command for TFTP
setenv bootcmd_tftp 'dhcp ${kernel_addr_r} ${tftp_server}:Image-starpro64 ; tftpboot ${fdt_addr_r} ${tftp_server}:eic7700-evb.dtb ; fdt addr ${fdt_addr_r} ; booti ${kernel_addr_r} - ${fdt_addr_r}'

## Save it for future reboots
saveenv

## Test the Boot Command for TFTP, then reboot
run bootcmd_tftp

## Remember the Original Boot Command: `bootflow scan -lb`
setenv orig_bootcmd "$bootcmd"

## Prepend TFTP to the Boot Command: `run bootcmd_tftp ; bootflow scan -lb`
setenv bootcmd "run bootcmd_tftp ; $bootcmd"

## Save it for future reboots
saveenv

Next comes the fun part that turns StarPro64 into a totally different beast from Star64…

(U-Boot dropping chars? Try iTerm > Edit > Paste Special > Paste Slowly)

(How to Undo Auto-Boot? Allow Static IP?)

StarPro64 with Touchscreen

§9 LLM on NPU on NuttX?

Oh really? Large Language Model on Single-Board Computer? (Eyes roll)

Hear me out…

  1. 20 TOPS INT8: That’s the spec of the speedy Neural Processing Unit (NPU) inside StarPro64. (20 Trillion Ops Per Second for 8-bit Integers)

    Yeah an Offline Disconnected LLM will run (somewhat) OK on any CPU. But this NPU is designed for such LLMs. (Goodbye “TensorFlow Lite”)

  2. Qwen LLM runs locally on EIC7700X NPU today. Probably Next: Llama LLM and DeepSeek LLM?

    (Qwen 2 with 0.5 Billion Parameters, pic below)

    Qwen LLM on ETC7700X NPU

  3. Offline Disconnected LLM on SBC might be useful for Smart Home Security

    “Hi LLM: Please connect my Home Security System to this Doorbell Camera and my IKEA Zigbee Lights and Xiaomi Motion Sensor and Samsung TV”

  4. Creature Sensor Maybe? A Remote Sensor that uses Cameras to identify Rainforest Critters and Underwater Creatures. But everything it sees becomes ultra-compressed into 16 bytes of text

    “DUCK!” “OCTOPUS!” (Pic below)

  5. EIC7700X NPU Driver is Dual-Licensed: BSD and GPL. Which means we can run it on all kinds of software platforms and create interesting apps.

  6. Will it be Expensive? We hear that StarPro64 will be priced super affordably. Works with a Touchscreen too. (Pic above)

    This is the right time to experiment with an Offline Disconnected LLM!

LLM Creature Sensor: A Remote Sensor that uses Cameras to identify Rainforest Critters and Underwater Creatures. But everything it sees becomes ultra-compressed into 16 bytes of text

(Here’s an idea for Sci-Fi Horror: We install an LLM Sensor in a Remote Uninhabited Island. One day we receive sinister words from our LLM Sensor: “EVIL!”, “DEATH!”, “DOOM!”…)

Isn’t Linux a little wonky on StarPro64?

Ah here’s our opportunity to create a “Power Efficient” (?) LLM with NuttX…

Odd name innit: Qwen?

Qwen will sound confusing to Bilingual Folks…

StarPro64 with Smart Power Plug

§10 Smart Power Plug

Flipping StarPro64 on and off. Again and again. Must be an easier way?

Try a Smart Power Plug (pic above), integrated with our Build Script.

In our Demo Video: Skip to 00:35 and watch our Build Script auto-power up StarPro64…

  1. Our Script will build the NuttX Image and copy to TFTP Server

  2. Power StarPro64 Off then On

  3. Wait Manually for Testing to Complete (“Press Enter”)

  4. And Power Off StarPro64

How it works? Here’s our Build Script: run.sh

## Omitted: Build the NuttX Image and copy to TFTP Server
## make -j ...

## Get the Home Assistant Token, copied from http://localhost:8123/profile/security
## export token=xxxx
. $HOME/home-assistant-token.sh

## Power Off the SBC
curl \
  -X POST \
  -H "Authorization: Bearer $token" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "automation.starpro64_off"}' \
  http://localhost:8123/api/services/automation/trigger

## Power On the SBC
curl \
  -X POST \
  -H "Authorization: Bearer $token" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "automation.starpro64_on"}' \
  http://localhost:8123/api/services/automation/trigger

## Wait Manually for SBC Testing to complete
## Don't wait too long, it will overheat!
echo Press Enter to Power Off
read

## Power Off the SBC, because it will overheat!
## Excessive Heatiness needs Oldenlandia Cooling Water?  
curl \
  -X POST \
  -H "Authorization: Bearer $token" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "automation.starpro64_off"}' \
  http://localhost:8123/api/services/automation/trigger

(See the Build Script)

(See the Build Log)

Smart Power Plug in IKEA App and Google Home

This script assumes that we have…

Smart Power Plug in Home Assistant

Smart Power Plug might disconnect USB UART sometimes?

To work around this: We run a loop for the UART Terminal

## First Time Only
echo "defscrollback 1000000" >> ~/.screenrc

## On Power Off: USB Serial might disconnect
## So we reconnect forever
set -x
for (( ; ; )) do 
  screen /dev/ttyUSB* 115200
  sleep 5
done

How to save the Entire Console Log from screen?

Inside screen: Press this Magic Konami Sequence…

Ctrl-a [
g <Space>
G <Enter>
Ctrl-a :writebuf

Everything gets saved into /tmp/screen-exchange

(We could actually allow a Remote Developer to boot and test NuttX on StarPro64… From anywhere in the world!)

Remember our USB Fan? It goes into our Smart Power Plug as a Power Jenga like so…

USB Fan goes into our Smart Power Plug as a Power Jenga

§11 What’s Next

We’re seeking volunteers to build NuttX Drivers for StarPro64 (GPIO, SPI, I2C, MIPI CSI / DSI, Ethernet, WiFi, NPU, …) Please lemme know!

Right now we’re upstreaming StarPro64 to NuttX Mainline

Maybe we’ll create a TinyEMU Emulator for StarPro64? 🤔

Special Thanks to My Sponsors for supporting my writing. Your support means so much to me 🙏

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.org/src/starpro64.md

NuttX boots only on Hart 0

§12 Appendix: Multiple Harts on StarPro64

Multiple Harts are problematic. Why?

Inside EIC7700X SoC: We have Four Harts (RISC-V CPU Cores) numbered 0 to 3.

This SoC will boot OpenSBI on Any Random Hart, 0 to 3! Which means U-Boot and NuttX will subsequently boot on the Same Random Hart.

What’s the problem?

NuttX assumes that it always Boots on Hart 0. (Pic above)

When NuttX boots on Harts 1 to 3: Our RISC-V Boot Code calls riscv_set_inital_sp. Which will fail: riscv_macros.S

## Set inital sp for riscv core. This function should be only called when initing.
## TODO: Support Non-Zero Boot Hart.
.macro riscv_set_inital_sp base, size, hartid
  la      t0, \base
  li      t1, \size
  mul     t1, \hartid, t1
  add     t0, t0, t1

  ## Ensure the last XCPTCONTEXT_SIZE is reserved for non boot CPU
  bnez \hartid, 998f
  li   t1, STACK_ALIGN_DOWN(\size)
  j    999f
998:
  li   t1, STACK_ALIGN_DOWN(\size - XCPTCONTEXT_SIZE)
999:
  add  t0, t0, t1
  mv   sp, t0
.endm

How to fix this?

Our workaround is to Always Reboot NuttX on Hart 0

  1. If Boot Hart is Not 0:

    Restart NuttX with Hart 0

  2. If Boot Hart is 0:

    Continue Starting NuttX

Harts vs CPUs: What’s the difference?

NuttX insists on booting with CPU 0. Otherwise it fails with this nx_start Error

[CPU2] dump_assert_info:
Assertion failed up_cpu_index() == 0: 
at file: init/nx_start.c:745 task(CPU2):
CPU2 IDLE process: Kernel 0x802019a6

That’s why we Renumber the CPUs: Boot Hart is always CPU 0. Other Harts become CPUs 1 to 3. For Example: If boot_hart=2 then…

Can’t we use One Hart and ignore the Other Harts?

OK Mister Cold-Harted: We tried Enabling One Hart Only (CPU 0). But OSTest hangs at sem_test

## OSTest hangs for StarPro64 when we enable One Hart only...
user_main: semaphore test
sem_test: Initializing semaphore to 0
sem_test: Starting waiter thread 1
sem_test: Set thread 1 priority to 191
## Oops: Thread 1 is NOT started!

sem_test: Starting waiter thread 2
sem_test: Set thread 2 priority to 128
waiter_func: Thread 2 Started

## Oops: Semaphore Value should be -1!
waiter_func: Thread 2 initial semaphore value = 0
waiter_func: Thread 2 waiting on semaphore
## Hangs here

Compare the above with SG2000 sem_test

## OSTest runs OK for SG2000...
user_main: semaphore test
sem_test: Initializing semaphore to 0
sem_test: Starting waiter thread 1
sem_test: Set thread 1 priority to 191
## Yep Thread 1 is started
waiter_func: Thread 1 Started

sem_test: Starting waiter thread 2
waiter_func: Thread 1 initial semaphore value = 0
sem_test: Set thread 2 priority to 128
waiter_func: Thread 1 waiting on semaphore
waiter_func: Thread 2 Started

## Yep Semaphore Value is -1
waiter_func: Thread 2 initial semaphore value = -1
waiter_func: Thread 2 waiting on semaphore
sem_test: Starting poster thread 3
## Completes successfully

Here’s the problem: sem_test calls nx_pthread_create to create Thread #1

int nx_pthread_create(...) { ...
#ifdef CONFIG_SMP
  // pthread_setup_scheduler() will set the affinity mask by inheriting the
  // setting from the parent task.  We need to override this setting
  // with the value from the pthread attributes unless that value is
  // zero:  Zero is the default value and simply means to inherit the
  // parent thread's affinity mask.
  if (attr->affinity != 0) {
    ptcb->cmn.affinity = attr->affinity;
  }
#endif

But…

Hence sem_test loops forever waiting for the Semaphore Value to change.

(Watch the Demo on YouTube)

(See the NuttX Log)

In Future: How to enable Multiple Harts?

To Enable Multiple Harts in future, we undo these changes…

Remember to update the StarPro64 defconfig

## Enable SMP with 4 CPUs
CONFIG_SMP=y
CONFIG_SMP_NCPUS=4

And remember to fix riscv_set_inital_sp. Meanwhile let’s run everything on Hart 0…

NuttX Build for StarPro64

§13 Appendix: Build NuttX for StarPro64

Earlier we booted Image-starpro64 over TFTP. How to get the file?

Download the file Image from below and rename it Image-starpro64

If we prefer to build NuttX ourselves…

  1. Install the Build Prerequisites, skip the RISC-V Toolchain…

    “Install Prerequisites”

  2. Download the RISC-V Toolchain for riscv-none-elf (xPack)…

    “xPack GNU RISC-V Embedded GCC Toolchain for 64-bit RISC-V”

  3. Download and Build NuttX for StarPro64 (work-in-progress)…

    git clone https://github.com/lupyuen2/wip-nuttx nuttx --branch starpro64
    git clone https://github.com/lupyuen2/wip-nuttx-apps apps --branch starpro64
    cd nuttx
    tools/configure.sh milkv_duos:nsh
    
    ## Build the NuttX Kernel and Apps
    make -j
    make -j export
    pushd ../apps
    ./tools/mkimport.sh -z -x ../nuttx/nuttx-export-*.tar.gz
    make -j import
    popd
    
    ## Generate Initial RAM Disk
    ## Prepare a Padding with 64 KB of zeroes
    ## Append Padding and Initial RAM Disk to NuttX Kernel
    genromfs -f initrd -d ../apps/bin -V "NuttXBootVol"
    head -c 65536 /dev/zero >/tmp/nuttx.pad
    cat nuttx.bin /tmp/nuttx.pad initrd \
      >Image
    
    ## Copy the NuttX Image and Device Tree to TFTP Server
    wget https://github.com/lupyuen/nuttx-starpro64/raw/refs/heads/main/eic7700-evb.dtb
    scp Image tftpserver:/tftpboot/Image-starpro64
    scp eic7700-evb.dtb tftpserver:/tftpboot/
    ssh tftpserver ls -l /tftpboot
    
    ## In U-Boot: Boot NuttX over TFTP
    ## setenv tftp_server 192.168.31.10 ; dhcp ${kernel_addr_r} ${tftp_server}:Image-starpro64 ; tftpboot ${fdt_addr_r} ${tftp_server}:eic7700-evb.dtb ; fdt addr ${fdt_addr_r} ; booti ${kernel_addr_r} - ${fdt_addr_r}

    (See the Build Script)

    (See the Build Log)

    (See the Build Outputs)

  4. The steps above assume that we’ve installed our TFTP Server, according to the instructions here

  5. Then follow these steps to boot NuttX on StarPro64…

    “Boot NuttX over TFTP”

  6. Powering StarPro64 on and off can get tiresome. Try a Smart Power Plug, integrated with our Build Script…

    “Smart Power Plug”

  7. How did we port NuttX to StarPro64? Check the details here…

    “Port NuttX to StarPro64”

Virtual Memory for NuttX Apps

Why the RAM Disk? Isn’t NuttX an RTOS?

StarPro64 uses a RAM Disk because it runs in NuttX Kernel Mode (instead of the typical Flat Mode). This means we can do Memory Protection and Virtual Memory for Apps. (Pic above)

But it also means we need to bundle the NuttX Apps as ELF Files, hence the RAM Disk…

Most of the NuttX Platforms run on NuttX Flat Mode, which has NuttX Apps Statically-Linked into the NuttX Kernel.

NuttX Flat Mode works well for Small Microcontrollers. But StarPro64 and other SoCs will need the more sophisticated NuttX Kernel Mode

NuttX boots OK on StarPro64 yay!

§14 Appendix: Port NuttX to StarPro64

How did we port NuttX to StarPro64? In under One Week?

We took the NuttX Port of Milk-V Duo S (Oz64 SG2000) and tweaked it for StarPro64 EIC7700X, with these minor modifications…

Here’s what we changed…

§14.1 RISC-V Boot Code

arch/risc-v/src/eic7700x/eic7700x_head.S

This is the RISC-V Boot Code that runs first when U-Boot Bootloader starts NuttX.

In the Linux Kernel Header: We modified the Kernel Size based on U-Boot (fdt_addr_r - kernel_addr_r)

This ensures that the Entire NuttX Image (including Initial RAM Disk) will be copied correctly from kernel_addr_r (0x8400_0000) to loadaddr (0x8020_0000)

/* Linux Kernel Header*/
__start:
  ...
  .quad  0x4000000  /* Kernel size (fdt_addr_r-kernel_addr_r) */

The Original Code assumes that we always Boot at Hart 0. But EIC7700X will Boot From Any Hart. (0 to 3)

This modification allows NuttX to Boot from any Hart…

  /* TODO SMP: Enable this for SMP
  /* If a0 (hartid) >= t1 (the number of CPUs), stop here
  blt  a0, t1, 3f
  csrw CSR_SIE, zero
  wfi
  */

3:
  /* Set stack pointer to the idle thread stack. Assume Hart 0. */
  li a2, 0
  riscv_set_inital_sp EIC7700X_IDLESTACK_BASE, SMP_STACK_SIZE, a2

  /* TODO SMP: Enable this for SMP
  riscv_set_inital_sp EIC7700X_IDLESTACK_BASE, SMP_STACK_SIZE, a0
  */

(Previously here)

Right now we support One Single Hart for EIC7700X. “TODO SMP” flags the code that will be modified (in future) to support Multiple Harts for EIC7700X.

(Multiple Harts explained)

§14.2 NuttX Start Code

arch/risc-v/src/eic7700x/eic7700x_start.c

NuttX boots here, called by the RISC-V Boot Code (from above). We made these changes to allow Booting from Any Hart

  1. If Boot Hart is Not 0:

    Restart NuttX with Hart 0

  2. If Boot Hart is 0:

    Continue Starting NuttX

// We remember the Boot Hart ID (0 to 3)
int g_eic7700x_boot_hart = -1;

// NuttX boots here, called by the RISC-V Assembly Boot Code
void eic7700x_start(int mhartid) {

  // If Boot Hart is not 0: Restart NuttX with Hart 0
  if (mhartid != 0) {

    //  Clear the BSS and Restart with Hart 0
    //  __start points to our RISC-V Assembly Start Code
    eic7700x_clear_bss();
    boot_secondary(0, (uintptr_t)&__start);

    // Let this Hart idle forever (while Hart 0 runs)
    while (true) { asm("WFI"); }  
    PANIC();  // Should never come here
  }

  // Else Boot Hart is 0: We have successfully booted NuttX on Hart 0!
  if (g_eic7700x_boot_hart < 0) {

    // Init the globals once only. Remember the Boot Hart.
    // Clear the BSS
    g_eic7700x_boot_hart = mhartid;
    eic7700x_clear_bss();

    // TODO SMP: Start the Other Harts by calling OpenSBI
    // eic7700x_boot_secondary();

    // Copy the RAM Disk
    // Initialize the per CPU areas
    eic7700x_copy_ramdisk();
    riscv_percpu_add_hart(mhartid);
  }
  // Omitted: Call eic7700x_start_s

(Previously here)

The code below will be used (in future) to support Multiple Harts

// Boot NuttX on the Hart
void eic7700x_start_s(int mhartid) {

  // Configure the FPU
  // If this is not the Boot Hart: Jump to cpux
  riscv_fpuconfig();
  if (mhartid != g_eic7700x_boot_hart) { goto cpux; }

  // Omitted: Boot Hart starts here and calls nx_start()
  ...

cpux:
  // TODO SMP: Non-Boot Hart starts here
  // We init the NuttX CPU
  riscv_cpu_boot(mhartid);

(Previously here)

How to Restart NuttX on Hart 0? By calling OpenSBI, adapted from riscv_sbi.c

// We start a Hart (0 to 3) by calling OpenSBI
// addr points to our RISC-V Assembly Start Code
static int boot_secondary(uintreg_t hartid, uintreg_t addr) {

  // Make an ECALL to OpenSBI
  sbiret_t ret = sbi_ecall(
    SBI_EXT_HSM, SBI_EXT_HSM_HART_START,
    hartid, addr, 0, 0, 0, 0
  );

  // Check for OpenSBI Errors
  if (ret.error < 0) { _err("Boot Hart %d failed\n", hartid); PANIC(); }
  return 0;
}

// Make an ECALL to OpenSBI
static sbiret_t sbi_ecall(unsigned int extid, unsigned int fid, uintreg_t parm0, uintreg_t parm1, uintreg_t parm2, uintreg_t parm3, uintreg_t parm4, uintreg_t parm5) {
  register long r0 asm("a0") = (long)(parm0);
  register long r1 asm("a1") = (long)(parm1);
  register long r2 asm("a2") = (long)(parm2);
  register long r3 asm("a3") = (long)(parm3);
  register long r4 asm("a4") = (long)(parm4);
  register long r5 asm("a5") = (long)(parm5);
  register long r6 asm("a6") = (long)(fid);
  register long r7 asm("a7") = (long)(extid);
  sbiret_t ret;

  asm volatile
    (
     "ecall"
     : "+r"(r0), "+r"(r1)
     : "r"(r2), "r"(r3), "r"(r4), "r"(r5), "r"(r6), "r"(r7)
     : "memory"
     );
  ret.error = r0;
  ret.value = (uintreg_t)r1;
  return ret;
}

// OpenSBI returns an Error Code and Result Value
struct sbiret_s {
  intreg_t    error;
  uintreg_t   value;
};
typedef struct sbiret_s sbiret_t;

// These are the Standard OpenSBI Extension Codes
#define SBI_EXT_HSM (0x0048534D)
#define SBI_EXT_HSM_HART_START (0x0)

(Previously here)

For Multiple Harts in future: We shall start the other Non-Boot Harts by calling OpenSBI…

// TODO SMP: Start the other Non-Boot Harts by calling OpenSBI
static void eic7700x_boot_secondary(void) {
  for (int i = 0; i < CONFIG_SMP_NCPUS; i++) {
    if (i == g_eic7700x_boot_hart) { continue; }
    boot_secondary(i, (uintptr_t)&__start);
  }
}

(Previously here)

For Multiple Harts in future: NuttX insists on Booting with CPU 0 Only. Thus we set Boot Hart as CPU 0, and we Renumber the Other Harts…

// TODO SMP: Convert Hart ID to CPU ID.
// Boot Hart is CPU 0. Renumber the Other Harts.
int weak_function riscv_hartid_to_cpuid(int hart) {
  if (hart == g_eic7700x_boot_hart)
    { return 0; }
  else if (hart < g_eic7700x_boot_hart)
    { return hart + 1; }
  else
    { return hart; }
}

// TODO SMP: Convert CPU ID to Hart ID.
// Boot Hart is CPU 0. Renumber the Other Harts.
int weak_function riscv_cpuid_to_hartid(int cpu) {
  if (cpu == 0)
    { return g_eic7700x_boot_hart; }
  else if (cpu < g_eic7700x_boot_hart + 1)
    { return cpu - 1; }
  else
    { return cpu; }
}

(Previously here)

For Example: If boot_hart=2 then…

(Multiple Harts explained)

§14.3 PLIC Interrupt Controller

arch/risc-v/include/eic7700x/irq.h

// Number of PLIC External Interrupts supported
#define EIC7700X_PLIC_IRQS 458

// Offset by RISCV_IRQ_SEXT
#define NR_IRQS (RISCV_IRQ_SEXT + EIC7700X_PLIC_IRQS)

(Previously here)

That’s because EIC7700X supports 458 External Interrupts

EIC7700X Tech Ref #1Page 374
Max Interrupts458


arch/risc-v/src/eic7700x/hardware/eic7700x_memorymap.h

// PLIC Base Address
#define EIC7700X_PLIC_BASE 0x0C000000ul

(Previously here)

PLIC Base Address is specified here…

EIC7700X Tech Ref #1Page 239
PLIC Memory Map0x0C00_0000


arch/risc-v/src/eic7700x/hardware/eic7700x_plic.h

// PLIC Interrupt Priority: Single Global Register
#define EIC7700X_PLIC_PRIORITY (EIC7700X_PLIC_BASE + 0x000000)

// Hart 0 S-Mode Interrupt Enable and Offset Between Harts
#define EIC7700X_PLIC_ENABLE0     (EIC7700X_PLIC_BASE + 0x002080)
#define EIC7700X_PLIC_ENABLE_HART 0x100

// Hart 0 S-Mode Priority Threshold and Offset Between Harts
#define EIC7700X_PLIC_THRESHOLD0     (EIC7700X_PLIC_BASE + 0x201000)
#define EIC7700X_PLIC_THRESHOLD_HART 0x2000

// Hart 0 S-Mode Claim / Complete and Offset Between Harts
#define EIC7700X_PLIC_CLAIM0     (EIC7700X_PLIC_BASE + 0x201004)
#define EIC7700X_PLIC_CLAIM_HART 0x2000

(Previously here)

Interrupt Enable: PLIC_ENABLE_HART is 0x100 because we skip 0x100 bytes per Hart…

EIC7700X Tech Ref #1Page 240
(Skip the M-Modes)
0x0C00_2080Start Hart 0 S-Mode interrupt enables
0x0C00_2180Start Hart 1 S-Mode interrupt enables
0x0C00_2280Start Hart 2 S-Mode interrupt enables

Priority Threshold: PLIC_THRESHOLD_HART is 0x2000 because we skip 0x2000 bytes per Hart

Claim / Complete: PLIC_CLAIM_HART is 0x2000 because we skip 0x2000 bytes per Hart

Which comes from this…

EIC7700X Tech Ref #1Page 241
(Skip the M-Modes)
0x0C20_1000Hart 0 S-Mode Priority Threshold
0x0C20_1004Hart 0 S-Mode Claim / Complete
0x0C20_3000Hart 1 S-Mode Priority Threshold
0x0C20_3004Hart 1 S-Mode Claim / Complete
0x0C20_5000Hart 2 S-Mode Priority Threshold
0x0C20_5004Hart 2 S-Mode Claim / Complete

(Multiple Harts explained)


arch/risc-v/src/eic7700x/eic7700x_irq.c

In future we shall support Multiple Harts. That’s why we extended this code to Initialize the Interrupts for Harts 0 to 3…

// Initialize the Interrupts
void up_irqinitialize(void) { ...

  // Disable all global interrupts
  for (hart = 0; hart < CONFIG_SMP_NCPUS; hart++) {
    addr = EIC7700X_PLIC_ENABLE0 + (hart * EIC7700X_PLIC_ENABLE_HART);
    for (offset = 0; offset < EIC7700X_PLIC_IRQS >> 3; offset += 4) {
      putreg32(0x0, addr + offset);          
    }
  }

  // Clear pendings in PLIC
  for (hart = 0; hart < CONFIG_SMP_NCPUS; hart++) {
    addr = EIC7700X_PLIC_CLAIM0 + (hart * EIC7700X_PLIC_CLAIM_HART);
    claim = getreg32(addr);
    putreg32(claim, addr);
  }

  // Set irq threshold to 0 (permits all global interrupts)
  for (hart = 0; hart < CONFIG_SMP_NCPUS; hart++) {
    addr = EIC7700X_PLIC_THRESHOLD0 + (hart * EIC7700X_PLIC_THRESHOLD_HART);
    putreg32(0, addr);
  }

(Previously here)

We do this to Disable the Interrupts for Boot Hart 0 to 3 (in future)

// Disable the Interrupt
void up_disable_irq(int irq) { ...

  // Clear enable bit for the irq
  if (0 <= extirq && extirq <= EIC7700X_PLIC_IRQS) {
    addr = EIC7700X_PLIC_ENABLE0 + 
           (g_eic7700x_boot_hart * EIC7700X_PLIC_ENABLE_HART);
    modifyreg32(addr + (4 * (extirq / 32)),
                1 << (extirq % 32), 0);
  }

(Previously here)

And this to Enable the Interrupts for Boot Hart 0 to 3 (in future)

// Enable the Interrupt
void up_enable_irq(int irq) { ...

  // Set enable bit for the irq
  if (0 <= extirq && extirq <= EIC7700X_PLIC_IRQS) {
    addr = EIC7700X_PLIC_ENABLE0 + 
           (g_eic7700x_boot_hart * EIC7700X_PLIC_ENABLE_HART);
    modifyreg32(addr + (4 * (extirq / 32)),
                0, 1 << (extirq % 32));
  }

(Previously here)

(Multiple Harts explained)


arch/risc-v/src/eic7700x/eic7700x_irq_dispatch.c

In future we shall support Multiple Harts. That’s why we extended this code to Dispatch the Interrupt for Boot Hart 0 to 3…

// Dispatch the Interrupt
void *riscv_dispatch_irq(uintptr_t vector, uintptr_t *regs) {
  int irq = (vector >> RV_IRQ_MASK) | (vector & 0xf);
  uintptr_t claim = EIC7700X_PLIC_CLAIM0 + 
                    (g_eic7700x_boot_hart * EIC7700X_PLIC_CLAIM_HART);
  ...
  // Read the PLIC_CLAIM for the Boot Hart
  uintptr_t val = getreg32(claim);
  ...
  // Write PLIC_CLAIM to clear pending for Boot Hart
  putreg32(irq - RISCV_IRQ_EXT, claim);

(Previously here)

(Multiple Harts explained)

§14.4 Memory Map

arch/risc-v/src/eic7700x/eic7700x_mm_init.c

// I/O Memory Map
#define MMU_IO_BASE (0x00000000ul)
#define MMU_IO_SIZE (0x80000000ul)

(Previously here)

We derived the above from the EIC7700X Memory Map

EIC7700X Tech Ref #1Page 380
System Memory Map
System Space (Low)0x0000_0000 to 0x8000_0000
Memory Space0x8000_0000 to 0x10_0000_0000

The rest of the Memory Map is identical to SG2000. We removed all T-Head MMU Extensions, including mmu_flush_cache.

§14.5 NuttX Config

arch/risc-v/Kconfig

In future we shall support Multiple Harts. This Arch Config will enable the Hart-To-CPU Mapping we saw earlier: riscv_hartid_to_cpuid, riscv_cpuid_to_hartid

config ARCH_CHIP_EIC7700X
  ## TODO SMP: Enable Hart-To-CPU Mapping
  ## select ARCH_RV_CPUID_MAP

(Previously here)

Also we removed ARCH_MMU_EXT_THEAD. (T-Head MMU Extensions)

(Multiple Harts explained)


boards/risc-v/eic7700x/starpro64/configs/nsh/defconfig

We modified the NuttX Board Config for UART…

## UART0 Configuration
CONFIG_16550_REGINCR=4
CONFIG_16550_UART0_BASE=0x50900000
CONFIG_16550_UART0_CLOCK=198144000
CONFIG_16550_UART0_IRQ=125

(Previously here)

16550_REGINCR is 4 because the UART Registers are spaced 4 bytes apart…

EIC7700X Tech Ref #4Page 524
UART Register Offset
0x0Receive Buffer Register (RBR)
0x4Interrupt Enable Register (IER)
0x8Interrupt Identification Register (IIR)

UART0 Base Address is here…

EIC7700X Tech Ref #4Page 353
Peripheral Address Space
UART00x5090_0000

Why IRQ 125? UART0 Interrupt Number is 100, we add 25 because of RISCV_IRQ_SEXT

EIC7700X Tech Ref #1Page 366
UART0 Interrupt Number100 (lsp_uart0_intr)

16550_UART0_CLOCK was computed according to these instructions

NuttX UART Debug Log shows:
  dlm = 0x00
  dll = 0x6c

We know that:
  dlm = 0x00 = (div >> 8)
  dll = 0x6c = (div & 0xff)

Which means:
  div = 0x6c

We know that:
  baud = 115200
  div  = (uartclk + (baud << 3)) / (baud << 4)

Therefore:
  0x6c    = (uartclk + 921600) / 1843200
  uartclk = (0x6c * 1843200) - 921600
          = 198144000

arch/risc-v/src/eic7700x/eic7700x_timerisr.c

Finally we changed the RISC-V Timer Frequency. We executed the sleep 10 command in NSH and adjusted the frequency…

// Previously for SG2000: 25000000ul
#define MTIMER_FREQ 1000000ul

(Previously here)

§14.6 Paste Slowly

U-Boot Bootloader is dropping chars when we paste long lines. How now brown cow?

In iTerm: Try Edit > Paste Special > Paste Slowly

But Before That: Click Settings > Advanced > Pasteboard