Click here to Skip to main content
15,867,568 members
Articles / Internet of Things

A Novice's Guide to the BeagleBone Platform and Software Development Using C++

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
8 Mar 2017CPOL11 min read 31.3K   162   24   4
A crash course in working with the BeagleBone and its built-in A/D converter, GPIO, and I2C bus using C++

OS Images

There are a few flavors of official images you can choose to install on the BeagleBone. Generally, for IOT types of activities, I would recommend starting with a “console” image, rather than a full blown LXDE desktop. You can always install X.Org and GUI libraries later if you are doing some kind of kiosk type of project. Recently, “iot” image spins have been created, which basically contain all the packages that come with the LXDE version like bonescript and node.js, but without LXDE or X.Org. “Console” images eliminate all the node.js and bonescript tooling, and weigh in at less than 50MB, but may lack some compilers and tooling you may need. It's a Debian system, so you can always add what you need with apt-get.

You may want to install any necessary drivers for you OS if you would like to access the BeagleBone Black via SSH over the mini-USB connector: 192.168.7.2 . If you are working with the BeagleBone Green variant, its a micro-USB connector and just about any cell phone charging cable will do. Otherwise, use the DHCP ethernet address or connect a FTDI serial debugging cable. I haven't needed the HDMI output of the BeagleBone Black, so I've grown accustomed to working with them over SSH and SFTP. If you are a Windows user, I would highly recommend MobaXterm as it makes it extremely easy to SSH and copy files via an integrated file explorer that implements SFTP, otherwise Putty. Obviously a Linux user can do it all on the command line, but I have found PAC Manager to also be useful.

Built-in Hardware Options

In order to get the most out of the BeagleBone, you can stack “capes” on it in order to expose additional I/O. You should be warned though, that not all third-party capes will work with one another, occasional you may run into a conflict in GPIO usage, particularity when dealing with LCD screen capes. If you stick to the capes defined in the official overlay repository, you will have more success.

On the other hand, exposing additional I/O functionality built into the BeagleBone's processor is pretty easy. Refer to the official diagrams here in order to identify the pin locations of all I/O. The functionality and accessibility of I/O can be controlled by editing /boot/uEnv.txt. This file controls the loading of various hardware overlays, allowing operating system access to different sets of I/O or hardware configurations. For example, to access the internal 12-bit A/D converter, add the following built-in overlay to the /boot/uEnv.txt file:

cape_enable=bone_capemgr.enable_partno=BB-ADC

On the next reboot, the A/D pins will be accessible in the Linux file system at:

/sys/bus/iio/devices/iio:device0 

You should see a series of files in_voltage0_raw...in_voltage6_raw, which correspond to the 7 channels of A/D conversion available to you. The maximum A/D reference voltage is 1.8V which corresponds to a max 12-bit digital value of 4096, so taking a measurement just means applying your reference voltage to VDD_ADC (P8_32), ground to GND_ADC (P9_34), and the output of whatever you are measuring to one of AIN0...AIN6.

Image 1

Potentiometer Demo

In this demonstration of a voltage divider, a fixed resistor of 1k ohms is wired in series with a potentiometer that can vary between 100 and 10k ohms.

Image 2

The white wire at the connection between the components is the sample point which will change in voltage as the dial on the potentiometer is turned. When the potentiometer is at its maximum resistance, the sample voltage will be around R2/(R1+R2) * VADC = 10,000/(10,000+1,000) * 1.8 ~ 1.64 V. When the potentiometer is set to a low resistance, then the sample voltage will approach 0.1 V.  My fixed resistor actually measured 0.98k ohms, and the potentiometer measured 10.4 k ohms at max and 98 ohms at min.

You can read back the current ADC value on the bash command line quite simply:

/sys/bus/iio/devices/iio:device0$ cat in_voltage0_raw
4093

So just take the ratio of this digital value over 4096 and multiply by 1.8 to obtain a voltage measurement. Here is the basic concept in C++ that will continuously sample ADC0 every 500 milliseconds. Accuracy in my tests were within 5-10mV of the value measured on my multimeter.

C++
#include <iostream>
#include <fstream>
#include <string>
#include <chrono>
#include <thread>
#include <iomanip>

using namespace std;

int main()
{
    const string adc0_dev_ = "/sys/bus/iio/devices/iio:device0/in_voltage0_raw";
    const unsigned int MAX_ADC_BB_OUT = 4096;
    const double MAX_VADC_BB = 1.8;

    while (true)
    {
        //open the ADC0 pin as a file object
        std::ifstream ain0(adc0_dev_);
        if (ain0)
        {
            //read the raw value
            string line;
            if(getline(ain0, line))
            {
                auto raw = std::stoul( line );
                double measured_voltage = 
                ((double)raw / MAX_ADC_BB_OUT ) * MAX_VADC_BB; //12-bit ADC

                cout << setprecision(3) << measured_voltage << " V" << endl;
            }
        }

        this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    return 0;
}

GPIO

Additional GPIO options can be exposed and manipulated on the command line in a similar way. The GPIOs are arranged in banks of 32 at the filesystem location:

/sys/class/gpio

Image 3

GPIO Output LED Demo

A GPIO can be set to either output, if you want to write to it in order to flip the pin voltage from 0 to 3V, or input, if you want to detect a change in voltage applied to the pin. We can demonstrate an output easily be connecting an LED and at least a 220 ohm resistor with one of the GPIO pins, and a DGND pin. In this example, I have an LED connected on its shorter leg (cathode) to DGND, and the anode side to GPIO_30 (P9 pin 11).

Image 4

To expose GPIO_30 control on the file system:

echo 30 > /sys/class/gpio/export

A new folder will appear, “gpio30”. Inside are a few files we can read/write to in order to control functionality. Writing a 1 or 0 to the value file will trigger voltage on the pin and blink the LED on and off.

echo out > /sys/class/gpio/gpio30/direction
echo 1 > /sys/class/gpio/gpio30/value
echo 0 > /sys/class/gpio/gpio30/value

GPIO Input Button Demo

An example of an input GPIO would be sensing a button push. Apply 3V from VDD_3V3 (P9 pins 3 or 4) to one leg of the button, and a 1k ohm resistor connected to DGND (pin 1 or 2) to the other leg. Run a wire from between the resistor and the button to GPIO_30. This “pull down” resistor setup will send the voltage to the input pin when the button is depressed.

Image 5

Set GPIO_30 as an input:

echo in > /sys/class/gpio/gpio30/direction

When you press the button, the file named "value: should have a value of 1, and a value of 0 when not pressed.

If you want to be efficient with the use of system resources, I would not recommend continuously looping code to read the value of the GPIO to look for value of 1. Embedded developers frequently use “interrupts” to detect these kinds of events. Linux can wait on file system events using the poll() function. The poll function can block for a specified amount of time, but will not significantly tax system resources while doing so. A C++ example of the button demonstration follows.

C++
#include <iostream>
#include <stdio.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

using namespace std;

int main()
{
    // the button is connected to GPIO_30
    int fd = open("/sys/class/gpio/gpio30/value", O_RDONLY);

    struct pollfd fdset;

    memset((void*) &fdset, 0, sizeof(fdset));
    fdset.fd = fd;
    fdset.events = POLLPRI;
    fdset.revents = 0;
    char c[1];

    while (true)
    {
        //1 ms-polling
        int ready = poll(&fdset, 1, 1);

        if (ready < 0)
        {
            cout << "poll() failed" << endl;
        }

        // polling will trigger on the rising edge of the GPIO
        // (transition from 0 -> 1)
        if ((fdset.revents & POLLPRI))
        {
            c[0] = 0;
            lseek( fdset.fd, 0, 0 );
            int size = read(fdset.fd, c, 1);
            int value = -1;
            if (size != -1)
            {
                value = atoi(c);
            }

            if (value == 1)
            {
                cout << "Button is pressed" << endl;
            }
        }
    }
}

Device trees can also be built to load specific GPIO pin configurations at boot so you do not have to expose them via bash commands, they get pre-loaded for you. Fortunately, people have created device tree generation tools to do this especially for the BeagleBone. You just need to make sure you enter the right pin information into the generator. You can then compile the device tree and add them to the list of device trees on the “cape_enable=bone_capemgr.enable_partno=” line in /boot/uEnv.txt. We will review this a little more in the next section.

Additional Expansion Options Using I2C – Adding a Real Time Clock

The BeagleBone does not have a built-in battery backed-up RTC, and a number of the existing tutorials on adding one are out of date or load the clock rather late in the boot process and require hacks in rc.local to keep it synced. Your best bet is to pick an RTC that has built in linux driver support, or has a module that is easy to compile without rebuilding the entire kernel.

Linux uses “device trees” to describe and initialize non-discoverable hardware. Devices that are connected to I2C or SPI buses generally fall into this category. In order to properly use an add-on I2C RTC clock with the BeagleBone, you will want to load a device tree for it in uEnv.txt. If you are not using the standard BeagleBone RTC cape, you may have to generate your own.

I have had success with an Epson RX-8900, it's small, cheap, reasonably accurate, and does not give off a lot of EMF. This RTC is a bit of a special case, in that its I2C interface is register-compatible with another device driver that is already built into the stock OS image “rtc-rv8803”. So we can tell Linux to load that driver module on boot by adding rtc-rv8803 to the list of devices in /etc/modules.

We now need to create a device tree so the OS knows what the I2C and pin configuration should be to communicate with the RTC. We can just use the BeagleBone RTC cape BB-RTC-01-00A0.dts file as a starting point, and change all references from its chip manufacturer to whatever you are using, just make sure you define the right pins for the I2C bus you are using. If you use i2c1, then you only need to change the driver name. Change the line:

C++
compatible = "maxim,ds1338";

to:

C++
compatible = "epson,rv8803";

If you are using i2c2, then remember the corresponding pins are instead P9_19 and P9_20.  I have attached an example of an overlay I use on i2c2. Finally, change the part-number line “part-number = "BB-RTC-01";” to a new name, and save it. You can then compile it to a device tree. If you pull down the BeagleBone overlays via git, you can just add it to the src/arm directory, and follow the simple directions in that repository to rebuild and reinstall all the latest overlays.
Basically:

git clone https://github.com/beagleboard/bb.org-overlays
cd ./bb.org-overlays
./dtc-overlay.sh
./install.sh

Just add the new DTC name to the “cape_enable=bone_capemgr.enable_partno” line in uEnv.txt, and the RTC driver will load from boot.

The last step is to make sure Linux uses /dev/rtc1 as its primary RTC device. If you look in /dev, you will see that the /dev/rtc file is a symlink to /dev/rtc0, which is the built-in BeagleBone RTC that has no battery backup. The Linux component “udev” is responsible for this at boot, to switch it to use rtc1, create a udev rule by issuing the following command as root:

echo "SUBSYSTEM=="rtc", KERNEL=="rtc1", SYMLINK+="rtc", 
      OPTIONS+="link_priority=10", TAG+="systemd"" > /etc/udev/rules.d/55-i2c-rtc.rules

Systemd has built in functionality for syncing system clocks with NTP servers. To be sure it's enabled, issue the command:

timedatectl set-ntp true

Once it has synced up the system time, you can then set the RTC and read back its time with:

hwclock -w -f /dev/rtc1
hwclock -r -f /dev/rtc1

From here on out, the OS should automatically sync up /dev/rtc with the system time.

Direct Control of I2C Devices

I would just briefly note, just because you might not have a driver available to you, you can still work with I2C devices directly in code very easily in Linux. In fact, sometimes you are better off rolling your own code, depending on your application needs. Once again, its similar to reading and writing on the file system. 

Step one, open a file that corresponds to the I2C bus you want to communicate on:

C++
g_i2cFile = open("/dev/i2c-2", O_RDWR);

Step two, write the address of the device you want to talk to using “ioctl”. Every device on the bus should have a unique address.

C++
auto res = ioctl(g_i2cFile, I2C_SLAVE, DEVICE_ADDR);

Step three, write the data to the device on the target register:

C++
const int BUFSIZE = 2;
char buffer[BUFSIZE];
buffer[0] = REG_ADDR_TARGET;
buffer[1] = data_byte;

if(write(g_i2cFile, buffer, BUFSIZE) != BUFSIZE) 
{
    //error
}

Alternately, to read data from a target register, you just write a single byte containing the target address, and then read back a byte from that target.

C++
char read_byte;
if(write(g_i2cFile, buffer, 1) == 1) 
{
    if(read(g_i2cFile, &read_byte, 1) !=1)
    {
        // successfully read a byte into read_byte;
    }
}

I have included a simple C++ class that implements this functionality.

Options for Compiling Software on the BeagleBone

Cross Compiler via QEMU

One of the easiest ways to compile software for the BeagleBone on a different machine is via QEMU. On another machine running Debian:

apt-get install qemu-user-static

Now you need to get a copy of the BeagleBone file system you are using. Use rsync or tar up the image.

mkdir -p /opt/beagle-root
cd /opt/beagle-root
rsync -Wa --progress --exclude=/proc --exclude=/sys 
      --exclude=/home/debian --delete bms@192.168.7.2:/* .

Copy the static ARM binary that provides emulation to this location:

cp $(which qemu-arm-static) /opt/beagle-root/usr/bin

At this point, you can enter the commands:

cd /opt
sudo chroot beagle-root /usr/bin/qemu-arm-static /bin/bash

You will then be running BeagleBone ARM binaries on the new system, through QEMU. You can now build any software just like normal. Any libs or binaries you create can be copied back onto the BeagleBone and will run normally.

Swap Space Tip

If you are patient and want to compile larger software packages directly on the BeagleBone, you may run up against memory limitations. In such a case, try creating some temporary swap space. Here is a bash script I use instead of calling “make” directly:

Bash
#!/bin/sh -e
#
# compile software directly on BB if swap space is needed
#

free

#64 MB swap file
sudo dd if=/dev/zero of=/var/swap.img bs=1024 count=65535
sudo mkswap /var/swap.img
sudo swapon /var/swap.img

free

make

sudo swapoff /var/swap.img
sudo rm /var/swap.img

exit 0

Conclusion

Most of the information presented here I discovered over a number of months of digging through documentation and blogs, and a lot of trial and error. Hopefully, this gives you a bit of a head start. To dive into some of these topics in greater detail, I have cited a number of sources from which I leaned this material.

Images

Kernels, drivers and device trees

RTC Examples

Detailed Demos

Useful SSH and SFTP Tools

History

  • 7th August, 2016 - Initial publication
  • 8th March, 2017 - Updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I have extensive experience developing software on both Linux and Windows in C++ and Python. I have also done a lot of work in the C#/.NET ecosystem. I currently work in the fields of robotics and machine learning, and also have a strong background in business automation/rules engines.

Comments and Discussions

 
SuggestionSimplifying the RTC configuration by making the offboard RTC into /dev/rtc0 (so that ntpd and the kernel uses it by default). Pin
Tim Small14-Sep-17 3:15
Tim Small14-Sep-17 3:15 
PraiseOutstanding! Pin
koothkeeper10-Mar-17 6:31
professionalkoothkeeper10-Mar-17 6:31 
QuestionVery nice reference guide! Thanks for making it available. Pin
John Marion10-Mar-17 6:17
John Marion10-Mar-17 6:17 
PraiseThank you for this excellent article! Pin
terryd10-Mar-17 5:36
terryd10-Mar-17 5:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.