Introduction
One of the most powerful features of the BeagleBone Black is the Linux operating system running on it. This can be demonstrated by using both a kernel driver for a sensor and the utilities provided in the Debian distribution of Linux, in this case to make temperature measurements at regular intervals without user invention. For me this was also an exercise to learn more about the Device Tree and the overlays used to reconfigure and add devices. For this example a LM75 temperature sensor is connected to the I2C2 bus accessible on the P9 header by default. To connect the sensor I used the Embedded Linux Wiki reference for the pinout and the LM75D datasheet.
My BeagleBone related content |
---|
The BeagleBone Black is mounted next to a breadboard to make experiments like these easier.
Motivation
On embedded computers the peripherals are often connected as "platform" devices, this often mean that they are just connected to an address in memory along with an interrupt line. For devices connected this way the kernel has no way of knowing where they are connected, how they are connected or even what they are. The way this problem is solved in Linux is by using a Device Tree, which as the name suggest is a tree structure over how all the devices are connected together along with which device drivers they are compatible with.
When measuring something at a regular interval it's very inconvenient if the users has to be logged in or worse have to log back in and restart the program after a power failure. In Linux there are a few ways to schedule something to run at a regular interval, a classic way of doing this is by using cron(8). Every user on the system have their own crontab which can be used to schedule jobs with cron. These jobs are performed automatically when the system is on and without the need to be logged in. To demonstrate this a small shell script is used to read the sysfs-file with the temperature value from the LM75 kernel driver.
Device Tree overlay
On embedded platforms there are a lot of devices that are not attached to a bus that can be iterated in a way similar to the PCIe bus in your PC. This means that the kernel has to know in advance how the peripherals are connected and which drivers are used to talk with them. In Linux this is done with a Device Tree that's loaded into the memory along with the kernel by U-Boot during boot. Once the kernel is running the Device Tree lives in memory, and we can add more device or reconfigure pins using an overlay. This overlay is made of fragments (of a Device Tree) that are added to the Device Tree in memory. For more information on how the overlays work see the Device Tree Overlay notes from the Linux documentation.
Getting the address of the I²C device
When you're reading the data sheet for your I²C device you should be aware that the I²C device have a 7-bit or 10-bit address and how the least significant bit (read/write flag) is represented varies between manufactures. It might be the case that the hexadecimal address you found is shifted one bit to the left, so scanning the I²C bus for your device is a good idea.
The utility i2cdetect can be used to scan the second I²C bus for devices.
$ i2cdetect -r 2 WARNING! This program can confuse your I2C bus, cause data loss and worse! I will probe file /dev/i2c-2 using read byte commands. I will probe address range 0x03-0x77. Continue? [Y/n] y 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 50: -- -- -- -- UU UU UU UU -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- --
Your user has to be in the i2c group to be allowed to write to the I²C bus. Alternatively you can run his command as a super user with sudo.
The scan shows an unconfigured device at address 0x48, the default address of the LM75 device when all three address pins are set to 0.
Writing the overlay
This is the first device tree overlay I've written and because of this I think it will be easier to show the code first and then explain it line by line, along with all the references I used to write it.
The contents of I2C2-TEMP-LM75.dts
/dts-v1/; /plugin/; / { compatible = "ti,beaglebone", "ti,beaglebone-black"; fragment@0 { target = <&i2c2>; __overlay__ { status = "okay"; #address-cells = <1>; #size-cells = <0>; lm75@48 { reg = <0x48>; compatible = "national,lm75"; }; }; }; };
The first line say that the overlay conform to version 1.0 of device tree source file format. The second line says that this is being plugged in to a full tree, this way we can use the relative &i2c2 address defined outside our overlay. The compatible property was something I found the blank cape example with the BeagleBone specific properties needed for an overlay, this will make sure the overlay is only loaded if the device is compatible.
The overlay source file can contain many fragments, only the first one is used here (zero indexed). As stated previously the overlay is a fragment that's added to the tree in memory, so to know where in the tree the fragment should be added we need to specify a target. The name of the target devices can be found in the cape interface specification. Per default the I2C2 bus is muxed to the pins on the P9 header, if this wasn't the case a fragment would be required that changed the pinmux settings before the fragment with our sensor.
At the target we want to add an __overlay__ to the existing tree, as if we were editing the tree. status = "okay" says that the device is operational and assumed to be ready to use.
#address-cells says how many cells of the device's reg property make up the address field and #size-cells says how many cells were used for the size field in the same reg property. If you're curious you can read more about how addressing works in the device tree.
The device name lm75@48 has to end with @48 so that it is the same as the first cell in the reg property. This is the I²C address in hex that we found earlier. The compatible property tells the kernel which driver to load by matching a manufacture and device name to one that a device driver supports. The format of this can be found in the source code for the Linux driver you're going to use, in my case the LM75 driver source code.
Compiling the overlay
The overlay we've written has to be compiled before it can be loaded and a copy the binary overlay should be placed in /lib/firmware
$ dtc -O dtb -o I2C2-TEMP-LM75.dtbo -b 0 -@ I2C2-TEMP-LM75.dts $ sudo cp I2C2-TEMP-LM75.dtbo /lib/firmware/
When I run the compile command I get a warning about the fragment not having a reg property. I failed to find a good solution or answer to this.
Configuring U-Boot
The overlay can be loaded automatically by the U-boot bootloader. This bootloader is configured with the /boot/uEnv.txt file and it's important that overlay loading is enabled by the setting: enable_uboot_overlays=1. Then adding the line: dtb_overlay=/lib/firmware/I2C2-TEMP-LM75.dtbo to ensure that our overlay is loaded by U-Boot on the next boot. Now is a good time to reboot to make sure this worked.
If everything worked there should be a message in dmesg from udev that a hwmon device was initialized:
$ dmesg | grep lm75 [ 31.943077] lm75 2-0048: hwmon0: sensor 'lm75'
Assuming the LM75 sensor is the only hardware monitoring device, the temperature reading from the sensor can be accessed through sysfs:
$ cat /sys/class/hwmon/hwmon0/temp1_input 24000
It's currently 24000 milidegree Celcius under my desk lamp.
If you have more than one sensor the uevent file in the sysfs directory for hwmon0 will mention the address and device name.
A bit more on sysfs
The files inside the /sys/class/hwmon/hwmon0 directory contains the values from some of the variables/objects used by the device driver. For hardware monitoring devices in Linux the temp1_input file contains the temperature, always in millidegree Celsius, from the first temperature sensor on the device. This is a way for device drivers to expose internal values to userland (the world outside the kernel) where applications can make use of them. The sysfs files for hardware monitoring devices all use the same naming and data formats.
There is some correlation between the registers in the LM75 device and the files in sysfs, although their values are converted to decimal numbers.
LM75 register | Sysfs file |
---|---|
Temperature | temp1_input |
THYST | temp1_max_hyst |
TOS | temp1_max |
Setting up your crontab
Every user on a Linux machine have their own crontab with which they can queue jobs to be run at: some time on certain dates, some time of the day or in this case at a regular interval. Jobs queued in your crontab will run with your user's permissions and as long as the system is powered on or until they are removed from the crontab. This means that you don't have to be logged in to run your jobs and that they'll also restart automatically after a reboot or the next time you power on your BeagleBone.
The crontab(1) command is used to setup a user's crontab. This can be done either by loading it in from a textfile: crontab filename or by using the interactive editor: crontab -e and crontab -r can be used to clear all queued jobs at once.
The syntax of the crontab is described in the man-page crontab(5) and consists of rows with five time/date columns, separated by spaces, followed by the command to execute. The five time/date columns are in the order: minute, hour, day of month, month and day of week. The values in these columns can be single time points or ranges: 5, 1-5 or * (the asterisk is a wildcard corresponding to the range: first to last) and you can combine multiple times and ranges with a comma: 1,3-5,7. There's also a skip symbol: /, as an example */5 in the minute field would only do something every fifth minute (0, 5, ..., 50, 55), in effect skipping all values except every fifth from the range 0-59.
To run something once every minute you'd write this:
*/1 * * * * ~/bin/read_temp.sh
This syntax is very dense, so I like to use the crontab guru online checker to make sure I got it right.
Example shell script
For this demonstration I wrote a small shell script that reads in the temperature and outputs a formatted row with the current the time and date along with the temperature. The script first checks if a file named the current date exists, if not then it writes a header to a new file and then the date, time and temperature row.
#!/bin/bash FILE_NAME=~/$(date +%F).csv TIMESTAMP=$(date +"%F %T") SYSFS_FILE=/sys/class/hwmon/hwmon0/temp1_input if [ -f "$SYSFS_FILE" ]; then TEMP=$(<$SYSFS_FILE) else echo "$SYSFS_FILE does not exist!" exit -1 fi if [ ! -f "$FILE_NAME" ]; then echo "Date,Temperature" > $FILE_NAME fi printf "%s,%s.%s\n" "$TIMESTAMP" \ $(($TEMP/1000)) $(($TEMP%1000)) \ >> $FILE_NAME
Bash supports mathematical operations with integers, so the second to last line extract the thousands of millidegree Celsius and its remainder separately. This is then formatted into a decimal number with printf.
Don't forget to make your shell script executable with: chmod +x read_temp.sh
Results
After I let the script run overnight, while taking a break from writing this blog, the resulting CSV-file was imported into LibreOffice Calc. The date format I chose to use is one that can be imported as proper date value in a spreadsheet, with LibreOffice Calc this is done by checking the "Detect Special numbers" box in the Import file dialogue.
The LM75 can only resolve up to half a degree Celsius, making for a very jagged graph.
Top Comments