The PRU microcontrollers are part of the processor chip, yet operate independently.
In the blink demo, the main processor runs a loader program that downloads the PRU code to the microcontroller, signals the PRU to start executing, and then waits for the PRU to finish. But what happens behind the scenes to make this work?
The PRU Linux Application Library (PRUSSDRV) provides the API between code running under Linux on the BeagleBone and the PRU.
The prussdrv_exec_program() API call loads a binary program
(in our case, the blink program) into the PRU so it can be executed.
To understand how this works requires a bit of background on how memory is set up.
Each PRU microcontroller has 8K (yes, just 8K) of instruction memory, starting at address 0.
Each PRU also has 8K of data memory, starting at address 0 (see TRM 4.3).
Since the main processor can’t access all these memories at address 0,
they are mapped into different parts of the main processor’s memory as defined by the “global memory map”.
For instance, PRU0’s instruction memory is accessed by the host starting at physical address 0x4a334000.
Thus, to load the binary code into the PRU, the API code copies the PRU binary file to that address.
Then, to start execution, the API code starts the PRU running by setting the enable bit in the PRU’s control register, which the host can access at a specific physical address (see TRM 126.96.36.199).
To summarize, the loader program running on the main processor loads the executable file into the PRU
by writing it to special memory addresses. It then starts up the PRU by writing to another special memory address that is the PRU’s control register.
At this point, you may wonder how the loader program can access to physical memory and low-level chip registers – usually is not possible from a user-level program.
This is accomplished through UIO, the Linux Userspace I/O driver (details).
The idea behind a UIO driver is that many devices (the PRU in our case) are controlled through a set of registers.
Instead of writing a complex kernel device driver to control these registers, simply expose the registers to user code, and then user-level code can access the registers to control the device.
The advantage of UIO is that user-level code is easier to write and debug than kernel code.
The disadvantage is there’s no control over device usage—buggy or malicious user-level code can easily mess up the device operation.
For the PRU, the uio_pruss driver provides UIO access.
This driver exposes the physical memory associated with the PRU as a device /dev/uio0.
The user-level PRUSSDRV API library code does a mmap on this device to map the address space into its own memory. This gives the loader code access to the PRU memory through addresses in its own address space.
The uio_pruss driver exposes information on the mapping to the user-level code through
the directory /sys/class/uio/uio0/maps.