Tri-Color LED Control Using the BeagleBone Black

Ten years ago I bought a BeagleBone Black (BBB) to investigate a hard real-time Linux API. When that research concluded, I put the bone in the back of a drawer. Eight years ago, I set my mind to learning circuits and returned to the BBB once again to explore hardware interfacing. The problem, though, was that my wife was pregnant with our first child — very pregnant. She made her way down to our home office to visit me shortly before the big night. I had breadboards, batteries, relays, resistors and capacitors spread across the coffee table. She tactfully tried to prepare me, “You know your life is going to change, right?” I didn’t think it would, but boy did it ever. Fast forward another child and six more years later, and the bone bug has hit me once again. I clearly haven’t learned, because this time I believe I have the capacity to run a blog and learn electronics…

And here we are. This is a post about how to control an indicator LED using the BeagleBone Black microcontroller.

Bill of Materials

PartQTYDescription
BeagleBone Black Rev C1The impressive microcontroller by TI. Supply has been touch-and-go. Check Amazon and Mouser.
Panel-Mount LED1Tri-Color LED with fastening nut. It operates on 12V power and has an integral resistor. Available at Digi-Key. See MFR part Q8P3BZZRYG12E.
BJT NPN Transistor2I used my existing inventory. For similar, see this kit on Amazon, part 2N2222.
BJT PNP Transistor2I used my existing inventory. For similar, see the kit linked above, part 2N3906.
Resistor 2.2K Ohm2I used my existing inventory. For similar, see this kit on Amazon.
Resistor 4.7K Ohm2I used my existing inventory. For similar, see the kit linked above.
Resistor 10K Ohm4I used my existing inventory. For similar, see the kit linked above.
TTL Serial to USB Cable1This cable allows review of boot messages, which is helpful for device tree debugging. Purchase on Amazon.
Shop suppliesN/A5V power, 12V power, breadboards, hook-up wire, etc

The High-Level Design

This lamp will serve as an indicator for a prototype I am building. I have mounted the LED on the corner of the panel using the included fastening hardware. Red will indicate a critical error. Amber will indicate an offline status, and green will indicate normal operation.

A prototype panel with an LED mounted by hex nut.

The LED has factory-installed leads – one red, one green, and one black. The black wire is 12V DC negative. The red wire is 12V DC positive. The green wire is also 12V DC positive. To illuminate a color, switch the positive leads on or off accordingly.

LED StateRed 12V+ WireGreen 12V+ Wire
OffLowLow
RedHighLow
AmberHighHigh
GreenLowHigh

This particular LED has an integrated resistor (resistors?), so it is safe to supply 12V directly. Always check the data sheet yourself, though, because without a resistor the LED will be toast!

Circuits

My first setup won’t include the BeagleBone. Instead, I’m simply going to wire the leads up to a breadboard and verify the information from the datasheet.

A circuit diagram using SPST switches to control DC 12V positive flow through the green and red leads.

The specifications of the LED state the operating current will be 20 mA. Here are my measurements:

SPST1SPST2Current (mA)Observation
OFFOFF0.0Off
ONOFF20.9Green
OFFON20.5Red
ONON40.7Amber

All observations are normal, and all currents are as-expected. The next step is to wire up the BBB to switch the positive current on and off for each LED lead. I’m doing this using two of the BBB’s General Purpose Input/Output (GPIO) pins. I have labeled them GPIO1 and GPIO2 in the diagram below. These are placeholder names – not specific header locations on the BBB.

A circuit diagram using NPN and PNP transistors and GPIO current to control the LEDs.

Because the LED has a shared ground lead, a PNP transistor is required to switch the 12V+ on and off. To control the base of the PNP transistor, I’m using an NPN transistor connected to the BBB 3.3V GPIO. When the GPIO voltage is high, the NPN transistor provides ground to the base of the PNP transistor. The PNP transistor then supplies load to the LED, and the lamp illuminates.

I used Ohm’s law to calculate resistor values throughout the circuit, with the exception of R4 and R9. For these values, I started with a 1K resistor and measured current. I increased resistance to 10K and observed a loss of functionality – the PNP base current was too low. So, I decreased resistance to 4.7K, and the circuit became operational again. I observed approximately 2.2 mA of current with the 4.7K resistor in-place, and I’m comfortable with that value. Please feel free to comment on the post if you can explain a more scientific approach to selecting this resistor value!

Optional Pull Down Resistors


The resistors R2 and R7 are pulldown resistors. These ensure that the transistor quickly and reliably is provided low voltage when the GPIO value is off. Without them, the base is considered “floating” and will malfunction. The resistors R2 and R7 are optional, because the BeagleBone provides internal resistors that can be enabled on GPIO pins. This guide will enable the internal resistors later on to replace R2 and R7.

Basic GPIO Control

I’m using the 10.3 Debian IoT image from BeagleBoard.org. This image includes the config-pin command line utility.

debian@beaglebone:~$ config-pin -h

GPIO Pin Configurator

Usage: config-pin -c <filename>
       config-pin -l <pin>
       config-pin -q <pin>
       config-pin <pin> <mode>

Suppose we want to use pin 12 of the P9 header as GPIO1. First, we’ll list the available modes:

debian@beaglebone:~$ config-pin -l p9.12

Available modes for P9_12 are: default gpio gpio_pu gpio_pd gpio_input

Now we query the current mode.

debian@beaglebone:~$ config-pin -q p9.12

Current mode for P9_12 is:     default

This pin is in the default mode. Unfortunately, you’ll need to know the details of the default mode for that to mean anything. I consulted the BeagleBone Bible for help, and the following preinstalled script is advised:

debian@beaglebone:/opt/scripts/device/bone$ ./show-pins.pl | grep P9.12
P9.12                             30 U18 fast rx  up  7 gpio 1.28        ocp/P9_12_pinmux (pinmux_P9_12_default_pin)

This output shows pin 12 on the P9 header is GPIO 28 on the 2nd processor bank of GPIOs (1.28). This pin is set as an input (rx) with a pullup resistor enabled (up).

GPIO Numbering: Bank vs System

The BeagleBone Black’s processor, the AM335x, has four banks of GPIOs. The four banks are numbered 0-3. Each bank has 32 GPIOs that are numbered 0-31. You can thus calculate the system GPIO number with the following equation:

(Bank Number * 32) + GPIO Number = GPIO System Number

So, for example, the P9.12 pin equates:

(1 * 32) + 28 = 60

Consult the System Reference Manual for more information.

Using the circuit above, I need the pin configured as an output with no internal pullup or pulldown resistor enabled. Here are some commands that achieve that:

debian@beaglebone:~$ config-pin p9.12 gpio

Current mode for P9_12 is:     gpio

debian@beaglebone:~$ cd /sys/class/gpio/gpio60/
debian@beaglebone:/sys/class/gpio/gpio60$ cat label
P9_12
debian@beaglebone:/sys/class/gpio/gpio60$ cat direction
in
debian@beaglebone:/sys/class/gpio/gpio60$ echo out > direction

First I used config-pin to change the mode of pin 12 on the P9 header to GPIO with no pulldown/pullup resistor. Then, I changed directories to the sysfs location for GPIO 60. I confirmed I have identified the system GPIO number correctly by checking the label. Finally, I changed the direction from input to output.

Command Persistence Notice

It’s important to understand the above commands result in changes that are transient. The next time the board boots, the P9.12 pin will again be in the default mode.

Now, turning the LED on and off is as-simple as changing the value.

debian@beaglebone:/sys/class/gpio/gpio60$ echo 1 > value
debian@beaglebone:/sys/class/gpio/gpio60$ echo 0 > value

After confirming the green lamp did indeed illuminate, I went ahead and repeated these steps using P8.26, which is system GPIO 61.

Once I had the circuit fully operational, I removed resistors R2 and R7. Then I changed each of the GPIO modes to use the BBB’s internal pulldown resistors.

debian@beaglebone:~$ config-pin P9.12 gpio_pd

Current mode for P9_12 is:     gpio_pd

debian@beaglebone:~$ config-pin P8.26 gpio_pd

Current mode for P8_26 is:     gpio_pd

The functionality is unchanged, but the final design is now simpler with fewer components required externally.

Reconfiguring the Default GPIO Settings

The operating system, Linux in this case, uses a configuration file that describes the hardware to online the system correctly. The bootloader, the program responsible for starting the operating system, reads the same configuration file during the board bring-up and passes the file along to Linux to finish the job. This configuration file is known as the device tree. You can read more about it on Wikipedia.

Where to Find the Device Tree

The device tree is specified in one or more source files, with extension dts, that use a defined syntax to describe the hardware and settings. A compiler transforms the source into a binary file, known as a blob, with extension dtb. The image I’m using provides the BeagleBone device tree blobs on the filesystem in the boot folder.

debian@beaglebone:/boot/dtbs/4.19.94-ti-r42$ ls
am335x-abbbi.dtb                am335x-boneblack-prusuart.dtb               am335x-bonegreen-gateway.dtb              am335x-lxm.dtb                am335x-sbc-t335.dtb     am43x-epos-evm.dtb                    am57xx-sbc-am57x.dtb
am335x-baltos-ir2110.dtb        am335x-boneblack-roboticscape.dtb           am335x-bonegreen-wireless.dtb             am335x-moxa-uc-8100-me-t.dtb  am335x-shc.dtb          am43x-epos-evm-hdmi.dtb               dra71-evm.dtb
am335x-baltos-ir3220.dtb        am335x-boneblack-uboot.dtb                  am335x-bonegreen-wireless-uboot-univ.dtb  am335x-nano.dtb               am335x-sl50.dtb         am5729-beagleboneai.dtb               dra72-evm.dtb
am335x-baltos-ir5221.dtb        am335x-boneblack-uboot-univ.dtb             am335x-bone-uboot-univ.dtb                am335x-osd3358-sm-red.dtb     am335x-wega-rdk.dtb     am5729-beagleboneai-roboticscape.dtb  dra72-evm-revc.dtb
am335x-base0033.dtb             am335x-boneblack-wireless.dtb               am335x-chiliboard.dtb                     am335x-pdu001.dtb             am437x-cm-t43.dtb       am572x-idk.dtb                        dra76-evm.dtb
am335x-boneblack-audio.dtb      am335x-boneblack-wireless-roboticscape.dtb  am335x-cm-t335.dtb                        am335x-pepper.dtb             am437x-gp-evm.dtb       am574x-idk.dtb                        dra7-evm.dtb
am335x-boneblack-bbb-exp-c.dtb  am335x-boneblack-wl1835mod.dtb              am335x-evm.dtb                            am335x-phycore-rdk.dtb        am437x-gp-evm-hdmi.dtb  am57xx-beagle-x15.dtb                 omap5-cm-t54.dtb
am335x-boneblack-bbb-exp-r.dtb  am335x-boneblue.dtb                         am335x-evmsk.dtb                          am335x-pocketbeagle.dtb       am437x-idk-evm.dtb      am57xx-beagle-x15-revb1.dtb           omap5-igep0050.dtb
am335x-boneblack-bbbmini.dtb    am335x-bone.dtb                             am335x-icev2.dtb                          am335x-revolve.dtb            am437x-sbc-t43.dtb      am57xx-beagle-x15-revc.dtb            omap5-sbc-t54.dtb
am335x-boneblack.dtb            am335x-bonegreen.dtb                        am335x-icev2-prueth.dtb                   am335x-sancloud-bbe.dtb       am437x-sk-evm.dtb       am57xx-cl-som-am57x.dtb               omap5-uevm.dtb

The bootloader will identify the board as the BBB, and then select an appropriate device tree from this location. Several options apply to this board, including the files am335x-boneblack.dtb, am335x-boneblack-uboot.dtb, and am335x-boneblack-uboot-univ.dtb. The bootloader decides which of these files to load based on the evaluation of conditional statements in its environment variables.

How to View and Modify the Device Tree

The device tree is a bit of an oddity to us software folk in that it compiles both ways. The kernel, Linux, reads the blob to manage the board. Developers can compile the blob back into a source to modify it. When they are ready to use it, they compile it back to a blob and reboot the board for the kernel to load it.

Informational Only

The workflow below is shown for the reader’s information only. There is a modernized procedure to modify the device tree using overlays, which is covered in the following section of this guide.
# Convert the file from a blob to a source
debian@beaglebone:~$ dtc -I dtb -O dts --sort -o ~/am335x-boneblack.dts /boot/dtbs/4.19.94-ti-r42/am335x-boneblack.dtb

# Edit the file
debian@beaglebone:~$ nano am335x-boneblack.dts

# Convert the source back to a blob
debian@beaglebone:~$ sudo cp /boot/dtbs/4.19.94-ti-r42/am335x-boneblack.dtb /boot/dtbs/4.19.94-ti-r42/am335x-boneblack.dtb.save
debian@beaglebone:~$ sudo dtc -I dts -O dtb -o /boot/dtbs/4.19.94-ti-r42/am335x-boneblack.dtb ~/am335x-boneblack.dts

Device Tree Overlays

The modern approach to changing a GPIO’s default configuration is to overlay the device tree, as opposed to modifying and replacing it directly. A device tree overlay will have extension dtbo. The bootloader first prepares the standard device tree, then processes the configured overlay files to further extend that configuration. This approach improves maintainability, because the overlay files are isolated from the default device tree.

Overlays are the configuration mechanism used to enable capes, which are add-on daughter boards for the BeagleBone products. Here are the capes available from beagleboard.org. The overlays for these capes are preinstalled in the image:

debian@beaglebone:~$ ls -l /lib/firmware | grep BBORG
-rw-r--r--  1 root root          1702 Apr  3  2020 BBORG_COMMS-00A2.dtbo
-rw-r--r--  1 root root          3558 Apr  3  2020 BBORG_DISPLAY18-00A2.dtbo
-rw-r--r--  1 root root          5574 Apr  3  2020 BBORG_DISPLAY70-00A2.dtbo
-rw-r--r--  1 root root          9842 Apr  3  2020 BBORG_GAMEPUP-00A2.dtbo
-rw-r--r--  1 root root          2939 Apr  3  2020 BBORG_MOTOR-00A2.dtbo
-rw-r--r--  1 root root          2505 Apr  3  2020 BBORG_PROTO-00A2.dtbo
-rw-r--r--  1 root root          1939 Apr  3  2020 BBORG_RELAY-00A2.dtbo
-rw-r--r--  1 root root          4221 Apr  3  2020 BBORG_TECHLAB-00A2.dtbo

You can look on the filesystem to see which overlays were applied:

debian@beaglebone:/proc/device-tree/chosen/overlays$ ls -l
total 0
-r--r--r-- 1 root root 25 Jan 28 18:33 AM335X-PRU-RPROC-4-19-TI-00A0
-r--r--r-- 1 root root 25 Jan 28 18:33 BB-ADC-00A0
-r--r--r-- 1 root root 25 Jan 28 18:33 BB-BONE-eMMC1-01-00A0
-r--r--r-- 1 root root 25 Jan 28 18:33 BB-HDMI-TDA998x-00A0
-r--r--r-- 1 root root  9 Jan 28 18:33 name

Here we can see the default image applied 4 different overlays.

Creating a New Device Tree Overlay

Don’t worry; this is still a post about controlling an LED. As with most things embedded, simple projects can get complicated quickly. Should you find yourself with more questions than this article answers, I recommend reading Adafruit’s Introduction to the BBB Device Tree as a primer.

I came up with the following overlay source in a new file named ToT-GPIO.dts:

/dts-v1/;
/plugin/;

/ {
        compatible = "ti,beaglebone", "ti,beaglebone-black";

        /* identification */
        part-number = "ToT-GPIO";
        version = "00A0";

        /* Show with loaded overlays in /proc/device-tree/chosen/overlays */
        fragment@0 {
            target-path = "/chosen";
            __overlay__ {
                overlays {
                    ToT-GPIO-00A0 = "Mon Jan 30 08:00:00 2023";
                };
            };
        };

        /* Configure the GPIO for the cape */
        fragment@1 {
            target = <&am33xx_pinmux>;
            __overlay__ {
                /* P8.26 is GPIO 61 and used to illuminate the red LED of the indicator lamp */
                pinctrl_tot_P8_26: pinmux_tot_P8_26 {
                    /* Set to Fast Slew, Output, Pulldown, Resistor Enabled, GPIO Mux Mode */
                    pinctrl-single,pins = <0x07c 0x07>;
                };

                /* P9.12 is GPIO 60 and used to illuminate the green LED of indicator lamp */
                pinctrl_tot_P9_12: pinmux_tot_P9_12 {
                    /* Set to Fast Slew, Output, Pulldown, Resistor Enabled, GPIO Mux Mode */
                    pinctrl-single,pins = <0x078 0x07>;
                };
            };
        };

        fragment@2 {
            target = <&ocp>;
            __overlay__ {
                P8_26_pinmux {
                    compatible = "bone-pinmux-helper";
                    pinctrl-0 = <&pinctrl_tot_P8_26>;
                    pinctrl-names = "default";
                    status = "okay";
                };

                P9_12_pinmux {
                    compatible = "bone-pinmux-helper";
                    pinctrl-0 = <&pinctrl_tot_P9_12>;
                    pinctrl-names = "default";
                    status = "okay";
                };
            };
        };
    };

Then I compiled it:

# First get it to compile
debian@beaglebone:~$ dtc -I dts -O dtb -b 0 -@ -o ToT-GPIO-00A0.dtbo ToT-GPIO.dts

# Then add it to the firmware location
debian@beaglebone:~$ sudo cp ToT-GPIO-00A0.dtbo /lib/firmware/

Finally, I modified /boot/uEnv.txt to include the overlay.

...

###U-Boot Overlays###
###Documentation: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian#U-Boot_Overlays
###Master Enable
enable_uboot_overlays=1
###
###Overide capes with eeprom
#uboot_overlay_addr0=/lib/firmware/<file0>.dtbo
#uboot_overlay_addr1=/lib/firmware/<file1>.dtbo
#uboot_overlay_addr2=/lib/firmware/<file2>.dtbo
#uboot_overlay_addr3=/lib/firmware/<file3>.dtbo
###
###Additional custom capes
uboot_overlay_addr4=/lib/firmware/ToT-GPIO-00A0.dtbo
#uboot_overlay_addr5=/lib/firmware/<file5>.dtbo
#uboot_overlay_addr6=/lib/firmware/<file6>.dtbo
#uboot_overlay_addr7=/lib/firmware/<file7>.dtbo
###
###Custom Cape
#dtb_overlay=/lib/firmware/<file8>.dtbo
###

...

Control with Code

This job won’t be done until I can control the LED using C++ logic. To keep this already long post from getting longer, I am going to build and run a sample program directly on the BeagleBone Black. It’s likely your goals exceed mine with this exercise, so be sure to scale your development environment with my C++ cross-compilation guide.

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>

int main()
{
    const static uint16_t GREEN_LED_GPIO = 61;
    const static uint16_t RED_LED_GPIO = 60;

    // Helper function to open the GPIO file
    auto OpenGpioSysfs = [](uint16_t gpioNum, const char* sysfsName) -> FILE*
    {
        char fileName[100] = {0};
        sprintf(
            fileName, 
            "/sys/class/gpio/gpio%d/%s", 
            gpioNum,
            sysfsName
        );
        return fopen(fileName, "w");
    };

    // Helper function to set the direction
    auto SetGpioDirection = [OpenGpioSysfs](uint16_t gpioNum, bool out) -> bool
    {
        FILE* gpioDirectionFile = OpenGpioSysfs(gpioNum, "direction");
        if (gpioDirectionFile != nullptr)
        {
            // Write out the direction
            const char* direction = out ? "out" : "in";
            size_t writeResult = fwrite(
                direction,
                1,
                strlen(direction),
                gpioDirectionFile
            );
            
            // Finish up
            fclose(gpioDirectionFile);
            gpioDirectionFile = nullptr;
            
            return (writeResult > 0) && fflush(gpioDirectionFile) == 0;
        }
        return false; // fail
    };

    // Helper function to set a value
    auto SetGpioValue = [](FILE* gpioValueFile, bool high) -> bool
    {
        if (gpioValueFile != nullptr)
        {
            // Assuming a rewind is good medicine
            rewind(gpioValueFile);
            
            // Write out the value
            const char* value = high ? "1" : "0";
            size_t writeResult = fwrite(
                value,
                1,
                strlen(value),
                gpioValueFile
            );
            return (writeResult > 0) && fflush(gpioValueFile) == 0;
        }
        return false; // fail
    };

    printf("Forcing GPIO direction to out - sysfs bug?\n");
    SetGpioDirection(GREEN_LED_GPIO, true);
    SetGpioDirection(RED_LED_GPIO, true);
    
    printf("Opening the GPIO files...\n");
    FILE* greenLedGpioFile = OpenGpioSysfs(GREEN_LED_GPIO, "value");
    FILE* redLedGpioFile = OpenGpioSysfs(RED_LED_GPIO, "value");

    if ((greenLedGpioFile == nullptr) || (redLedGpioFile == nullptr))
    {
        printf("Failed!\n");
        return 1;
    }

    printf("Looping forever!\n");
    srand(static_cast<uint32_t>(time(nullptr)));

    bool stop = false;
    while (!stop)
    {
        bool ok = false;

        switch (rand() % 4)
        {
        case 0:
            {
                printf("Turn off the lamp...\n");
                ok = SetGpioValue(redLedGpioFile, false);
                ok = ok && SetGpioValue(greenLedGpioFile, false);
            }
            break;
        case 1:
            {
                printf("Turn on the green lamp...\n");
                ok = SetGpioValue(redLedGpioFile, false);
                ok = ok && SetGpioValue(greenLedGpioFile, true);
            }
            break;
        case 2:
            {
                printf("Turn on the amber lamp...\n");
                ok = SetGpioValue(redLedGpioFile, true);
                ok = ok && SetGpioValue(greenLedGpioFile, true);
            }
            break;
        case 3:
        default:
            {
                printf("Turn on the red lamp...\n");
                ok = SetGpioValue(redLedGpioFile, true);
                ok = ok && SetGpioValue(greenLedGpioFile, false);
            }
            break;
        } 

        if (!ok)
        {
            printf("  SetGpioValue has failed! errno=%d msg=\"%s\"\n", 
                errno,
                strerror(errno)
            );
        }

        sleep(1); // seconds until next change
    }

    fclose(greenLedGpioFile);
    greenLedGpioFile = nullptr;

    fclose(redLedGpioFile);
    redLedGpioFile = nullptr;

    return 0;
}

Compile and run this application to see the lamp change states in a random order.

debian@beaglebone:~/demo/tricolor-led$ g++ main.cpp
debian@beaglebone:~/demo/tricolor-led$ ./a.out
Forcing GPIO direction to out - sysfs bug?
Opening the GPIO files...
Looping forever!
Turn off the lamp...
Turn on the amber lamp...
Turn on the red lamp...
Turn on the green lamp...
Turn on the green lamp...
Turn off the lamp...
Turn off the lamp...
Turn on the green lamp...
Turn on the red lamp...

Known Issues

There are still a couple of problems I have yet to sort out.

Boot Sequence Pinmux

When I reboot the BBB, there is a period of time where both controlling GPIOs are set high. The lamp illuminates just after issuing the restart command to Linux, and it continues to be illuminated until the next boot cycle when the Linux kernel processes the overlay supplied by u-boot. I am not sure if there is a way to fix this problem by changing the u-boot environment, or if something more complicated, like rebuilding the bootloader(s), is the only avenue for a software fix. Since these GPIOs are only controlling an indicator lamp, I have decided to let it be for now.

Invalid Direction Exported to Sysfs

I have triple-checked my device tree overlay. I am confident I have set the pinmux for both GPIOs as outputs. However, when Linux exports these GPIOs to Sysfs, the direction files always initially have contents “in”. To compensate, I have modified my code above to force the direction files to “out”. I am surprised the device tree values are not interrogated to initialize the direction, and I wonder if this is simply a bug in the Linux kernel’s implementation of Sysfs.

Closing Thoughts

It only took me 10 years to get around to it, but I have finally designed a circuit and controlled it with a BeagleBone Black! Now, programmatic control of a single indicator LED is just a beginning for me, but I must admit the accomplishment is satisfying. Moreover, the two kids that delayed my exploration of circuit interfacing are pumped to see what is next!

2 thoughts on “Tri-Color LED Control Using the BeagleBone Black”

  1. many thanks for your deep knowledge tutorial, we shouldn’t build the castle on sand. Thank you again

Leave a Reply

Microsoft Workplace Discount Program
Scroll to Top
%d bloggers like this: