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
Part | QTY | Description |
---|---|---|
BeagleBone Black Rev C | 1 | The impressive microcontroller by TI. Supply has been touch-and-go. Check Amazon and Mouser. |
Panel-Mount LED | 1 | Tri-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 Transistor | 2 | I used my existing inventory. For similar, see this kit on Amazon, part 2N2222. |
BJT PNP Transistor | 2 | I used my existing inventory. For similar, see the kit linked above, part 2N3906. |
Resistor 2.2K Ohm | 2 | I used my existing inventory. For similar, see this kit on Amazon. |
Resistor 4.7K Ohm | 2 | I used my existing inventory. For similar, see the kit linked above. |
Resistor 10K Ohm | 4 | I used my existing inventory. For similar, see the kit linked above. |
TTL Serial to USB Cable | 1 | This cable allows review of boot messages, which is helpful for device tree debugging. Purchase on Amazon. |
Shop supplies | N/A | 5V 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.

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 State | Red 12V+ Wire | Green 12V+ Wire |
---|---|---|
Off | Low | Low |
Red | High | Low |
Amber | High | High |
Green | Low | High |
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.

The specifications of the LED state the operating current will be 20 mA. Here are my measurements:
SPST1 | SPST2 | Current (mA) | Observation |
---|---|---|---|
OFF | OFF | 0.0 | Off |
ON | OFF | 20.9 | Green |
OFF | ON | 20.5 | Red |
ON | ON | 40.7 | Amber |
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.

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
(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
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
# 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!
many thanks for your deep knowledge tutorial, we shouldn’t build the castle on sand. Thank you again
You are welcome!