Andrew B. Wright, S. M. ’88, Ph. D.
This code can be placed in the /root/code/realtime/realtime directory and directly unpacked (tar -xzf <archive name>). Make sure the pru0 and pru1 subdirectories exist before compiling.
After debugging tasks, the next most important task in real-time control is to figure a way to have a real-time loop.
The classic methods are
- to program a timer and fire an interrupt
- to poll a timer at the bottom of a loop
- to calculate the number of instructions and enter a loop of nops to fill in the remaining time
- use PRU0 as an accurate timer to update PRU1
The first method won’t work on the PRU, since the PRU is not interruptible by design.
The second method may work, but the PRU doesn’t have an on-board timer and must look through one of the busses to get to a timer count (dmtimer). This introduces uncertain bus delays into the system and could reduce accuracy unacceptably.
With a 5 ns instruction time, method 3 is going to yield the most accurate results. Even if a few instructions are miscounted, the error in the loop won’t be much more than 15 ns. If you’re controlling at the msec order of magnitude, that’s going to be difficult to detect and won’t influence the problem.
Method 4 would be a waste of a PRU.
The following intrinsic function on the PRU would be nice
void __delay_cycles(unsigned int cycles);
But, it needs to know the number of cycles at compile time. So, it’s useless for the current problem (variable precise delay).
The CONTROL register has a CYCLE count. This is how many cycles since the PRU was enabled. The CNT_EN bit has to be set to enable the counter. CYCLE = 0 resets the counter.
So, a loop like
should count with minimal non-deterministic “depends on the calculation” instructions and give a pretty precise delta time. Perhaps the time won’t be exactly 1000 cycles, but cycle-to-cycle variations should be fall within a few nanoseconds.
The enforcement of the timing will be done at the bottom of the while(1) loop, by determining how many cycles have been used and counting the remainder of the cycles until the desired number of cycles have passed. The toggle will be moved to the top of the loop, where the number of cycles will always be the same.
The ARM side code takes care of many things that used to be done manually. The new sysfs remoteproc interface includes files under /sys/class/remoteclass/remoteclass1 which govern PRU0’s behavior. The file, state, tells you the state of the processor (offline, running). By writing ‘stop’ to this file, PRU0 is stopped. By writing ‘start’ to this file, PRU0 is started. A slight wrinkle, the OS does not like you to start an already running PRU. So, you need to read the state, compare to ‘offline’ or ‘running’ and decide what to do (stop-start or just start). The logic in realtime.c covers this.
A second wrinkle is that the code to be loaded onto the PRU has to be located in /lib/firmware. Unlike the earlier remoteproc interface, you can tell the system what file in /lib/firmware to load into PRU0 by writing that name into /sys/class/remoteproc/remoteproc1/firmware. So, if you put am335x-pru0-fw into /lib/firmware and write ‘am335x-pru0-fw’ into /sys/class/remoteproc/remoteproc1/firmware, then am335x-pru0-fw will be loaded into PRU0 at run time.
Slight wrinkle, the PRU must be stopped to update the firmware file. This has also been included in realtime.c.
If you decide you do not like ‘am335x-pru0-fw’, you can create a completely new firmware file name, place it into /lib/firmware and write the name to /sys/class/remoteproc/remoteproc1/firmware, and this completely new firmware file will be loaded into the PRU. For instance, you may want to name things like realtime-pru0-fw, so that you have a sense of what the program is supposed to do.
This is a nice new feature of the new remoteproc system.
The Makefile has a slightly different set up due to the inclusion of the start-stop logic in realtime.c. Now, ‘make install’ makes the PRU executables, but does not start the PRU. This action is executed in realtime.c. This action also configures the digital output pin to be accessible by the PRU (config-pin P8.12 pruout).
The entries, ‘make stop’ and ‘make run’ allow you to toggle the state of the PRU without typing the long command.
The entry, ‘make status’ tells you the state of the PRU, so that you can decide whether to ‘make start’ or ‘make stop’/’make start’
Make clean tidies everything up for your departure.
The PRU side code initializes the rpmsg interface (rpmsg_init) within an infinite loop (while(1)).
It calls listen() – defined in pru_comm.c – to gather data from the rpmsg interface.
It calls parse_the_message() – defined in pru_comm.c – to determine if a valid command code has been received. The action_state variable is updated once a valid command code has been received. In this case, ‘afS’ starts the PRU and ‘afs” stops the PRU.
If action_state == ‘S’ then the while(1) loop will run through that branch until ‘afs’ is received. While in the loop, __R30 bit 14 (the bit corresponding to P8.12) will be toggled. This will give a square wave with the width set by the variable LOOP (10000 in this example).
At the bottom of the loop, a simple while(PRU0_CTRL.CYCLE < LOOP); will run until the CYCLE variable exceeds LOOP. At this point, looping will occur. Ideally, the first command at the top of the while(1) loop would be to update digital outputs, as this would give the maximum cycle-to-cycle timing accuracy; however, in this test, some variability is tolerated by some variation in how many instructions are executed in listen and in parse_the_message. In the true control applications later, this will be tightened up.
When you run the programs, a 2000*5 ns = .1 msec square wave (down for 1000 cycles then up for 1000 cycles) will be output on P8.12. You can connect to an oscilloscope and see the output. You can measure the period with the cursor function to see how accurate your square wave will be.