Real-Time I/O#
Sometimes, when BeagleBone Black interacts with the physical world, it needs to respond in a timely manner.
For example, your robot has just detected that one of the driving motors needs to turn a bit faster.
Systems that can respond quickly to a real event are known as real-time
systems. There are two broad
categories of real-time systems: soft and hard.
In a soft real-time
system, the real-time requirements should be met most
of the time, where most
depends on the system. A video playback system is a good example. The goal might be to display 60 frames
per second, but it doesn’t matter much if you miss a frame now and then. In a 100 percent hard real-time
system, you can never fail to respond in time. Think of an airbag deployment system on a car. You can’t even be 50 ms late.
Systems running Linux generally can’t do 100 percent hard real-time processing, because Linux gets in the way. However, the Bone has an ARM processor running Linux and two additional 32-bit programmable real-time units (PRUs Ti AM33XX PRUSSv2) available to do real-time processing. Although the PRUs can achieve 100 percent hard real-time, they take some effort to use.
This chapter shows several ways to do real-time input/output (I/O), starting with the effortless, yet slower JavaScript and moving up with increasing speed (and effort) to using the PRUs.
Note
In this chapter, as in the others, we assume that you are logged in as debian (as indicated by the bone$ prompt). This gives you quick access to the general-purpose input/output (GPIO) ports but you may have to use sudo some times.
I/O with Python and JavaScript#
Problem#
You want to read an input pin and write it to the output as quickly as possible with JavaScript.
Solution#
Reading the Status of a Pushbutton or Magnetic Switch (Passive On/Off Sensor) shows how to read a pushbutton switch and Toggling an External LED controls an external LED. This recipe combines the two to read the switch and turn on the LED in response to it. To make this recipe, you will need:
Breadboard and jumper wires
Pushbutton switch
220R resistor
LED
Wire up the pushbutton and LED as shown in Diagram for wiring a pushbutton and LED with the LED attached to P9_14.
The code in Monitoring a pushbutton (pushLED.py) reads GPIO port P9_42, which is attached to the pushbutton, and turns on the LED attached to P9_12 when the button is pushed.
1#!/usr/bin/env python
2# ////////////////////////////////////////
3# // pushLED.py
4# // Blinks an LED attached to P9_12 when the button at P9_42 is pressed
5# // Wiring:
6# // Setup:
7# // See:
8# ////////////////////////////////////////
9import time
10import os
11
12ms = 50 # Read time in ms
13
14LED="50" # Look up P9.14 using gpioinfo | grep -e chip -e P9.14. chip 1, line 18 maps to 50
15button="7" # P9_42 mapps to 7
16
17GPIOPATH="/sys/class/gpio/"
18
19# Make sure LED is exported
20if (not os.path.exists(GPIOPATH+"gpio"+LED)):
21 f = open(GPIOPATH+"export", "w")
22 f.write(LED)
23 f.close()
24
25# Make it an output pin
26f = open(GPIOPATH+"gpio"+LED+"/direction", "w")
27f.write("out")
28f.close()
29
30# Make sure button is exported
31if (not os.path.exists(GPIOPATH+"gpio"+button)):
32 f = open(GPIOPATH+"export", "w")
33 f.write(button)
34 f.close()
35
36# Make it an output pin
37f = open(GPIOPATH+"gpio"+button+"/direction", "w")
38f.write("in")
39f.close()
40
41# Read every ms
42fin = open(GPIOPATH+"gpio"+button+"/value", "r")
43fout = open(GPIOPATH+"gpio"+LED+"/value", "w")
44
45while True:
46 fin.seek(0)
47 fout.seek(0)
48 fout.write(fin.read())
49 time.sleep(ms/1000)
1////////////////////////////////////////
2// blinkLED.c
3// Blinks the P9_14 pin based on the P9_42 pin
4// Wiring:
5// Setup:
6// See:
7////////////////////////////////////////
8#include <stdio.h>
9#include <string.h>
10#include <unistd.h>
11#define MAXSTR 100
12
13int main() {
14 FILE *fpbutton, *fpLED;
15 char LED[] = "50"; // Look up P9.14 using gpioinfo | grep -e chip -e P9.14. chip 1, line 18 maps to 50
16 char button[] = "7"; // Look up P9.42 using gpioinfo | grep -e chip -e P9.42. chip 0, line 7 maps to 7
17 char GPIOPATH[] = "/sys/class/gpio";
18 char path[MAXSTR] = "";
19
20 // Make sure LED is exported
21 snprintf(path, MAXSTR, "%s%s%s", GPIOPATH, "/gpio", LED);
22 if (!access(path, F_OK) == 0) {
23 snprintf(path, MAXSTR, "%s%s", GPIOPATH, "/export");
24 fpLED = fopen(path, "w");
25 fprintf(fpLED, "%s", LED);
26 fclose(fpLED);
27 }
28
29 // Make it an output LED
30 snprintf(path, MAXSTR, "%s%s%s%s", GPIOPATH, "/gpio", LED, "/direction");
31 fpLED = fopen(path, "w");
32 fprintf(fpLED, "out");
33 fclose(fpLED);
34
35 // Make sure bbuttonutton is exported
36 snprintf(path, MAXSTR, "%s%s%s", GPIOPATH, "/gpio", button);
37 if (!access(path, F_OK) == 0) {
38 snprintf(path, MAXSTR, "%s%s", GPIOPATH, "/export");
39 fpbutton = fopen(path, "w");
40 fprintf(fpbutton, "%s", button);
41 fclose(fpbutton);
42 }
43
44 // Make it an input button
45 snprintf(path, MAXSTR, "%s%s%s%s", GPIOPATH, "/gpio", button, "/direction");
46 fpbutton = fopen(path, "w");
47 fprintf(fpbutton, "in");
48 fclose(fpbutton);
49
50 // I don't know why I can open the LED outside the loop and use fseek before
51 // each read, but I can't do the same for the button. It appears it needs
52 // to be opened every time.
53 snprintf(path, MAXSTR, "%s%s%s%s", GPIOPATH, "/gpio", LED, "/value");
54 fpLED = fopen(path, "w");
55
56 char state = '0';
57
58 while (1) {
59 snprintf(path, MAXSTR, "%s%s%s%s", GPIOPATH, "/gpio", button, "/value");
60 fpbutton = fopen(path, "r");
61 fseek(fpLED, 0L, SEEK_SET);
62 fscanf(fpbutton, "%c", &state);
63 printf("state: %c\n", state);
64 fprintf(fpLED, "%c", state);
65 fclose(fpbutton);
66 usleep(250000); // sleep time in microseconds
67 }
68}
bone$ gcc -o pushLED pushLED.c -lgpiod
bone$ ./pushLED
1
1
0
0
0
1
^C
1#!/usr/bin/env node
2////////////////////////////////////////
3// pushLED.js
4// Blinks an LED attached to P9_12 when the button at P9_42 is pressed
5// Wiring:
6// Setup:
7// See:
8////////////////////////////////////////
9const fs = require("fs");
10
11const ms = 500 // Read time in ms
12
13const LED="50"; // Look up P9.14 using gpioinfo | grep -e chip -e P9.14. chip 1, line 18 maps to 50
14const button="7"; // P9_42 mapps to 7
15
16GPIOPATH="/sys/class/gpio/";
17
18// Make sure LED is exported
19if(!fs.existsSync(GPIOPATH+"gpio"+LED)) {
20 fs.writeFileSync(GPIOPATH+"export", LED);
21}
22// Make it an output pin
23fs.writeFileSync(GPIOPATH+"gpio"+LED+"/direction", "out");
24
25// Make sure button is exported
26if(!fs.existsSync(GPIOPATH+"gpio"+button)) {
27 fs.writeFileSync(GPIOPATH+"export", button);
28}
29// Make it an input pin
30fs.writeFileSync(GPIOPATH+"gpio"+button+"/direction", "in");
31
32// Read every ms
33setInterval(flashLED, ms);
34
35function flashLED() {
36 var data = fs.readFileSync(GPIOPATH+"gpio"+button+"/value").slice(0, -1);
37 console.log('data = ' + data);
38 fs.writeFileSync(GPIOPATH+"gpio"+LED+"/value", data);
39 }
Add the code to a file named pushLED.py
and run it by using the following commands:
bone$ chmod *x pushLED.py
bone$ ./pushLED.py
Hit ^C to stop
0
0
1
1
^C
Press ^C (Ctrl-C) to stop the code.
I/O with devmem2#
Problem#
Your C code isn’t responding fast enough to the input signal. You want to read the GPIO registers directly.
Solution#
The solution is to use a simple utility called devmem2, with which you can read and write registers from the command line.
Warning
This solution is much more involved than the previous ones. You need to understand binary and hex numbers and be able to read the AM335x Technical Reference Manual.
First, download and install devmem2:
bone$ wget http://bootlin.com/pub/mirror/devmem2.c
bone$ gcc -o devmem2 devmem2.c
bone$ sudo mv devmem2 /usr/bin
This solution will read a pushbutton attached to P9_42 and flash an LED attached to P9_13. Note that this is a change from the previous solutions that makes the code used here much simpler. Wire up your Bone as shown in Diagram for wiring a pushbutton and LED with the LED attached to P9_13.
Now, flash the LED attached to P9_13 using the Linux sysfs interface (Controlling GPIOs by Using SYSFS Entries). To do this, first look up which GPIO number P9_13 is attached to by referring to Mapping from header pin to internal GPIO number. Finding P9_13 at GPIO 31, export GPIO 31 and make it an output:
bone$ cd cd /sys/class/gpio/
bone$ echo 31 > export
bone$ cd gpio31
bone$ echo out > direction
bone$ echo 1 > value
bone$ echo 0 > value
The LED will turn on when 1 is echoed into value and off when 0 is echoed.
Now that you know the LED is working, look up its memory address. This is where things get very detailed. First, download the AM335x Technical Reference Manual. Look up GPIO0 in the Memory Map chapter (sensors). Table 2-2 indicates that GPIO0 starts at address 0x44E0_7000. Then go to Section 25.4.1, “GPIO Registers.” This shows that GPIO_DATAIN has an offset of 0x138, GPIO_CLEARDATAOUT has an offset of 0x190, and GPIO_SETDATAOUT has an offset of 0x194.
This means you read from address 0x44E0_7000 + 0x138 = 0x44E0_7138 to see the status of the LED:
bone$ sudo devmem2 0x44E07138
/dev/mem opened.
Memory mapped at address 0xb6f8e000.
Value at address 0x44E07138 (0xb6f8e138): 0xC000C404
The returned value 0xC000C404 (1100 0000 0000 0000 1100 0100 0000 0100 in binary) has bit 31 set to 1, which means the LED is on. Turn the LED off by writing 0x80000000 (1000 0000 0000 0000 0000 0000 0000 0000 binary) to the GPIO_CLEARDATA register at 0x44E0_7000 + 0x190 = 0x44E0_7190:
bone$ sudo devmem2 0x44E07190 w 0x80000000
/dev/mem opened.
Memory mapped at address 0xb6fd7000.
Value at address 0x44E07190 (0xb6fd7190): 0x80000000
Written 0x80000000; readback 0x0
The LED is now off.
You read the pushbutton switch in a similar way. Mapping from header pin to internal GPIO number says P9_42 is GPIO 7, which means bit 7 is the state of P9_42. The devmem2 in this example reads 0x0, which means all bits are 0, including GPIO 7. Section 25.4.1 of the Technical Reference Manual instructs you to use offset 0x13C to read GPIO_DATAOUT. Push the pushbutton and run devmem2:
bone$ sudo devmem2 0x44e07138
/dev/mem opened.
Memory mapped at address 0xb6fe2000.
Value at address 0x44E07138 (0xb6fe2138): 0x4000C484
Here, bit 7 is set in 0x4000C484, showing the button is pushed.
This is much more tedious than the previous methods, but it’s what’s necessary if you need to minimize the time to read an input. I/O with C and mmap() shows how to read and write these addresses from C.
I/O with C and mmap()#
Problem#
Your C code isn’t responding fast enough to the input signal.
Solution#
In smaller processors that aren’t running an operating system, you can read and write a given memory address directly from C. With Linux running on Bone, many of the memory locations are hardware protected, so you can’t accidentally access them directly.
This recipe shows how to use mmap() (memory map) to map the GPIO registers to an array in C. Then all you need t o do is access the array to read and write the registers.
Warning
This solution is much more involved than the previous ones. You need to understand binary and hex numbers and be able to read the AM335x Technical Reference Manual.
This solution will read a pushbutton attached to P9_42 and flash an LED attached to P9_13. Note that this is a change from the previous solutions that makes the code used here much simpler.
Tip
See I/O with devmem2 for details on mapping the GPIO numbers to memory addresses.
Add the code in Memory address definitions (pushLEDmmap.h) to a file named pushLEDmmap.h
.
1// From: http://stackoverflow.com/questions/13124271/driving-beaglebone-gpio
2// -through-dev-mem
3// user contributions licensed under cc by-sa 3.0 with attribution required
4// http://creativecommons.org/licenses/by-sa/3.0/
5// http://blog.stackoverflow.com/2009/06/attribution-required/
6// Author: madscientist159 (http://stackoverflow.com/users/3000377/madscientist159)
7
8#ifndef _BEAGLEBONE_GPIO_H_
9#define _BEAGLEBONE_GPIO_H_
10
11#define GPIO0_START_ADDR 0x44e07000
12#define GPIO0_END_ADDR 0x44e08000
13#define GPIO0_SIZE (GPIO0_END_ADDR - GPIO0_START_ADDR)
14
15#define GPIO1_START_ADDR 0x4804C000
16#define GPIO1_END_ADDR 0x4804D000
17#define GPIO1_SIZE (GPIO1_END_ADDR - GPIO1_START_ADDR)
18
19#define GPIO2_START_ADDR 0x41A4C000
20#define GPIO2_END_ADDR 0x41A4D000
21#define GPIO2_SIZE (GPIO2_END_ADDR - GPIO2_START_ADDR)
22
23#define GPIO3_START_ADDR 0x41A4E000
24#define GPIO3_END_ADDR 0x41A4F000
25#define GPIO3_SIZE (GPIO3_END_ADDR - GPIO3_START_ADDR)
26
27#define GPIO_DATAIN 0x138
28#define GPIO_SETDATAOUT 0x194
29#define GPIO_CLEARDATAOUT 0x190
30
31#define GPIO_03 (1<<3)
32#define GPIO_07 (1<<7)
33#define GPIO_31 (1<<31)
34#define GPIO_60 (1<<28)
35#endif
Add the code in Code for directly reading memory addresses (pushLEDmmap.c) to a file named pushLEDmmap.c
.
1// From: http://stackoverflow.com/questions/13124271/driving-beaglebone-gpio
2// -through-dev-mem
3// user contributions licensed under cc by-sa 3.0 with attribution required
4// http://creativecommons.org/licenses/by-sa/3.0/
5// http://blog.stackoverflow.com/2009/06/attribution-required/
6// Author: madscientist159 (http://stackoverflow.com/users/3000377/madscientist159)
7//
8// Read one gpio pin and write it out to another using mmap.
9// Be sure to set -O3 when compiling.
10#include <stdio.h>
11#include <stdlib.h>
12#include <sys/mman.h>
13#include <fcntl.h>
14#include <signal.h> // Defines signal-handling functions (i.e. trap Ctrl-C)
15#include "pushLEDmmap.h"
16
17// Global variables
18int keepgoing = 1; // Set to 0 when Ctrl-c is pressed
19
20// Callback called when SIGINT is sent to the process (Ctrl-C)
21void signal_handler(int sig) {
22 printf( "\nCtrl-C pressed, cleaning up and exiting...\n" );
23 keepgoing = 0;
24}
25
26int main(int argc, char *argv[]) {
27 volatile void *gpio_addr;
28 volatile unsigned int *gpio_datain;
29 volatile unsigned int *gpio_setdataout_addr;
30 volatile unsigned int *gpio_cleardataout_addr;
31
32 // Set the signal callback for Ctrl-C
33 signal(SIGINT, signal_handler);
34
35 int fd = open("/dev/mem", O_RDWR);
36
37 printf("Mapping %X - %X (size: %X)\n", GPIO0_START_ADDR, GPIO0_END_ADDR,
38 GPIO0_SIZE);
39
40 gpio_addr = mmap(0, GPIO0_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
41 GPIO0_START_ADDR);
42
43 gpio_datain = gpio_addr + GPIO_DATAIN;
44 gpio_setdataout_addr = gpio_addr + GPIO_SETDATAOUT;
45 gpio_cleardataout_addr = gpio_addr + GPIO_CLEARDATAOUT;
46
47 if(gpio_addr == MAP_FAILED) {
48 printf("Unable to map GPIO\n");
49 exit(1);
50 }
51 printf("GPIO mapped to %p\n", gpio_addr);
52 printf("GPIO SETDATAOUTADDR mapped to %p\n", gpio_setdataout_addr);
53 printf("GPIO CLEARDATAOUT mapped to %p\n", gpio_cleardataout_addr);
54
55 printf("Start copying GPIO_07 to GPIO_31\n");
56 while(keepgoing) {
57 if(*gpio_datain & GPIO_07) {
58 *gpio_setdataout_addr= GPIO_31;
59 } else {
60 *gpio_cleardataout_addr = GPIO_31;
61 }
62 //usleep(1);
63 }
64
65 munmap((void *)gpio_addr, GPIO0_SIZE);
66 close(fd);
67 return 0;
68}
Now, compile and run the code:
bone$ gcc -O3 pushLEDmmap.c -o pushLEDmmap
bone$ sudo ./pushLEDmmap
Mapping 44E07000 - 44E08000 (size: 1000)
GPIO mapped to 0xb6fac000
GPIO SETDATAOUTADDR mapped to 0xb6fac194
GPIO CLEARDATAOUT mapped to 0xb6fac190
Start copying GPIO_07 to GPIO_31
^C
Ctrl-C pressed, cleaning up and exiting...
The code is in a tight while loop that checks the status of GPIO 7 and copies it to GPIO 31.
Tighter Delay Bounds with the PREEMPT_RT Kernel#
Problem#
You want to run real-time processes on the Beagle, but the OS is slowing things down.
Solution#
The Kernel can be compiled with PREEMPT_RT enabled which reduces the delay from when a thread is scheduled to when it runs.
Switching to a PREEMPT_RT kernel is rather easy, but be sure to follow the steps in the Discussion to see how much the latencies are reduced.
First see which kernel you are running:
bone$ uname -a
Linux breadboard-home 5.10.120-ti-r47 #1bullseye SMP PREEMPT Tue Jul 12 18:59:38 UTC 2022 armv7l GNU/Linux
I’m running a 5.10 kernel. Remember the whole string, 5.10.120-ti-r47, for later.
Go to kernel update and look for 5.10.
In The regular and RT kernels you see the reular kernel on top and the RT below.
We want the RT one.
bone$ sudo apt update
bone$ sudo apt install bbb.io-kernel-5.10-ti-rt-am335x
Note
Use the am57xx if you are using the BeagleBoard AI or AI64.
Before rebooting, edit /boot/uEnv.txt to start with:
#Docs: http://elinux.org/Beagleboard:U-boot_partitioning_layout_2.0
# uname_r=5.10.120-ti-r47
uname_r=5.10.120-ti-rt-r47
#uuid=
#dtb=
uname_r tells the boot loader which kernel to boot. Here we’ve commented out the regular kernel and left in the RT kernel. Next time you boot you’ll be running the RT kernel. Don’t reboot just yet. Let’s gather some latency data first.
Bootlin’s preempt_rt workshop looks like a good workshop on PREEMPT RT. Their slides say:
One way to implement a multi-task Real-Time Operating System is to have a preemptible system
Any task can be interrupted at any point so that higher priority tasks can run
Userspace preemption already exists in Linux
The Linux Kernel also supports real-time scheduling policies
However, code that runs in kernel mode isn’t fully preemptible
The Preempt-RT patch aims at making all code running in kernel mode preemptible
The workshop goes into many details on how to get real-time performance on Linux. Checkout their slides and labs. Though you can skip the first lab since we present a simpler way to get the RT kernel running.
Cyclictest#
cyclictest is one tool for measuring the latency from when a thread is schduled and when it runs. The code/rt directory in the git repo has some scripts for gathering latency data and plotting it. Here’s how to run the scripts.
First look in rt/install.sh to see what to install.
1sudo apt install rt-tests
2# You can run gnuplot on the host
3sudo apt install gnuplot
Open up another window and start something that will create a load on the Bone, then run the following:
bone$ time sudo ./hist.gen > nort.hist
hist.gen shows what’s being run. It defaults to 100,000 loops, so it takes a while. The data is saved in nort.hist, which stands for no RT histogram.
1#!/bin/sh
2# This code is from Julia Cartwright julia@kernel.org
3
4cyclictest -m -S -p 90 -h 400 -l "${1:-100000}"
Note
If you get an error:
Unable to change scheduling policy! Probably missing capabilities, either run as root or increase RLIMIT_RTPRIO limits
try running ./setup.sh. If that doesn’t work try:
bone$ sudo bash
bone# ulimit -r unlimited
bone# ./hist.gen > nort.hist
bone# exit
Now you are ready to reboot into the RT kernel and run the test again.
bone$ reboot
After rebooting:
bone$ uname -a
Linux breadboard-home 5.10.120-ti-rt-r47 #1bullseye SMP PREEMPT RT Tue Jul 12 18:59:38 UTC 2022 armv7l GNU/Linux
Congratulations you are running the RT kernel.
Note
If the Beagle appears to be running (the LEDs are flashing) but you are having trouble connecting via ssh 192.168.7.2, you can try connecting using the approach shown in Viewing and Debugging the Kernel and u-boot Messages at Boot Time.
Now run the script again (note it’s being saved in rt.hist this time.)
bone$ time sudo ./hist.gen > rt.hist
Note
At this point yoou can edit /boot/uEnt.txt to boot the non RT kernel and reboot.
Now it’s time to plot the results.
bone$ gnuplot hist.plt
This will generate the file cyclictest.png which contains your plot. It should look like:
Notice the NON-RT data have much longer latenices. They may not happen often (fewer than 10 times in each bin), but they are occurring and may be enough to miss a real-time deadline.
The PREEMPT-RT times are all under a 150 us.
I/O with simpPRU#
Problem#
You require better timing than running C on the ARM can give you.
Solution#
The AM335x processor on the Bone has an ARM processor that is running Linux, but it also has two 32-bit PRUs that are available for processing I/O. It takes a fair amount of understanding to program the PRU. Fortunately, simpPRU is an intuitive language for PRU which compiles down to PRU C. This solution shows how to use it.
Background#
simpPRU