I chose the BeagleBone Black (amongst other things) for the embedded microcontrollers (PRUs): Besides your usual ARM Cortex processor, you can put two “Programmable Real-time Units” to work. These are 2 RISC-cores clocked at 200MHz, optimised for real-time processing:

  • Most instructions take 1 cycle (external memory access takes longer)
  • No async interrupt handling, but provisions to efficiently check for pending interrupts
  • Enhanced GPIO mode, that allows some pins to be directly controlled by the PRU, bypassing the normal GPIO-logic. This boosts performance down to 5ns latencies.

Enabling the PRU

There are two methods for accessing the PRUs from the Linux kernel. The “old” method, “pruss” or “uio”, exposes the entire PRU memory region to userspace. This allows for maximal flexibility, but you need to do everything yourself, such as communications.

The “new” method, “rproc”, works differently: The kernel assumes responsibility for the remote processor, and protects it from userspace. Additional drivers are needed to allow userspace to communicate with the PRU, and these drivers define what is and is not allowed.

I chose to use the rproc-method, but most of what is written below applies to both methods. Newer kernels (4.9.36 at least) don’t start the PRU’s automatically on boot. You need to start the manually by echo start > /sys/class/remoteproc/remoteproc1/state. Stopping the PRU is left as an exercise for the reader 😉 Note that on my setup, PRU0 was assigned remoteproc1 (the M3 power management processor is assigned remoteproc0).

Writing code for the PRU

TI provides an assembler, and even a full IDE suite, but I wanted to stay with my known tools. There is a GCC-variant that compiles to PRU-opcodes. It’s even available as a pre-compiled Debian package called “gcc-pru” from Robert Nelson’s repo. Unfortunately, Robert only has a Jessie-build available, but it seems to work fine on Stretch as well.

As with most embedded projects, the first thing to get working is the blinking LED. I learned a lot from the blinking LED “functionality” I found on GitHub, especially about the rproc-part. It took me countless hours to figure everything out, but in the end it worked.

Getting a blinking LED

GPO 21 of GPIO bank 1 is wired to the blue USR0 LED, normally showing the heartbeat. You can use this output to test the PRU without adding any additional hardware. Start by disabling the heartbeat, so it doesn’t interfere: echo "none" >/sys/class/leds/beaglebone:green:usr0/trigger.

The PRU code is super basic. main.S:

#define CONST_PRUCFG 4
#define GPIO1 0x4804c000
#define GPIO_CLEARDATAOUT 0x190
#define GPIO_SETDATAOUT 0x194

    .text
    .section .init0, "x"
    .global __start
__start:
    // Enable OCP master port to access external memory
    // otherwise the PRU will stall when trying
    LBCO      r0, CONST_PRUCFG, 4, 4    // Load SYSCFG in to r0
    CLR       r0, r0, 4         // Clear SYSCFG[STANDBY_INIT] to enable OCP master port
    SBCO      r0, CONST_PRUCFG, 4, 4    // Write r0 to SYSCFG

main:
    LDI32 r2, 1<<21
    LDI32 r3, GPIO1 | GPIO_SETDATAOUT
    SBBO r2, r3, 0, 4    // write 1<<21 in to GPIO1 SETDATAOUT

    // Delay loop, count down r0 to zero
    LDI32 r0, 0x08000000
DEL1:
    SUB r0, r0, 1
    QBNE DEL1, r0, 0

    LDI32 r2, 1<<21
    LDI32 r3, GPIO1 | GPIO_CLEARDATAOUT
    SBBO r2, r3, 0, 4    // write 1<<21 in to GPIO1 CLEARDATAOUT
 
    // Delay loop, count down r0 to zero
    LDI32 r0, 0x08000000
DEL2:
    SUB r0, r0, 1
    QBNE DEL2, r0, 0

    // repeat
    JMP main

    // unreachable
    LDI r31.b0, 35 // interrupt to host
    HALT

    /* Dummy data, required by remoteproc loader */
    .data
    .section .resource_table,"aw",@progbits
my_resource_table:
    .word 1, 0, 0, 0 /* struct resource_table base */
    .word 0 /* uint32_t offset[1] */

You can compile this by using:

pru-gcc -g -Os -Wall -Wextra -minrt -mmcu=am335x.pru0 -nostdlib -nodefaultlibs -nostartfiles main.S -o am335x-pru0-fw

The pru_rproc kernel driver loads two firmware files: /lib/firmware/am335x-pru0-fw and am335x-pru1-fw in the same directory, one for each PRU. Copy the generated ELF-binary there, and start the PRU by:

echo start > /sys/class/remoteproc/remoteproc1/state

The firmware files contain, besides the actual code to run, a resource table. This table is used to ask the kernel to set up some things before starting the PRU, such as interrupt mappings, communication channels, …