by Tykling
06. feb 2021 15:54 UTC
I2C
is a communication protocol which was originally designed and used for inter-board communications between peripherals, like the processor talking to an EEPROM in a system. It is used extensively in modern electronics, including laptops, cars etc. These days it is also used for short (well, short to medium - I occationally use 50cm cables) distance communication with sensors - not what it was designed for, but it works surprisingly well anyway. Wikipedia has this to say about I2C
:
I2C (Inter-Integrated Circuit), pronounced I-squared-C, is a synchronous, multi-master, multi-slave, packet switched, single-ended, serial communication bus invented in 1982 by Philips Semiconductor (now NXP Semiconductors). It is widely used for attaching lower-speed peripheral ICs to processors and microcontrollers in short-distance, intra-board communication. Alternatively, I2C is spelled I2C (pronounced I-two-C) or IIC (pronounced I-I-C). (thanks Wikipedia!)
I've been playing around with some I2C
based sensors from SparkFun. They have QWIIC connectors making it easy to speak to them from SparkFuns OpenLog Artemis. The OpenLog Artemis can log sensor data to SD card and also presents itself as a serial device where it streams data from the sensors in a CSV-like format. It works great and I can highly recommend the whole QWIIC
ecosystem.
Occationally I need to use one of the QWIIC
sensors from some FreeBSD based system, without the OpenLog Artemis
. In this case I am using a Raspberry PI Zero
with FreeBSD a 13-CURRENT from December. This is easy because the QWIIC
system is just plain I2C
underneath, so it is easy to hook a sensor up directly to the GPIO
pins of the RPI with one of these.
In FreeBSD land we have i2c(8) which is a userspace utility for reading and writing to I2C
devices. I will start by going over basic usage before diving deeper into the actual sensors I am playing with in this learning session.
My test rig for this blogpost looks like this:
First things first, I need some hardware with an I2C
bus which is exposed to FreeBSD. I am using a Raspberry PI Zero
and the I2C
bus looks like this in dmesg
:
[root@pizero1 ~]# grep I2C /var/run/dmesg.boot iicbus0: <OFW I2C bus> on iichb0 iic0: <I2C generic I/O> on iicbus0 [root@pizero1 ~]#
The first sensor I've hooked up is a SparkFun TMP117
temperature sensor. According to the hookup guide the default I2C
address of this sensor is 0x48
which matches what I am seeing after hooking up the hardware and running i2c(8)
with the -s
flag to make it scan the I2C
bus for devices:
[root@pizero1 ~]# i2c -s Hardware may not support START/STOP scanning; trying less-reliable read method. Scanning I2C devices on /dev/iic0: 48 [root@pizero1 ~]#
After connecting an additional sensor which has a default I2C
address of 0x60
the bus scan can see that device too:
[root@pizero1 ~]# i2c -sv dev: /dev/iic0, addr: 0x0, r/w: r, offset: 0x00, width: 8, count: 1 Hardware may not support START/STOP scanning; trying less-reliable read method. Scanning I2C devices on /dev/iic0: 48 60 [root@pizero1 ~]#
Reading from the sensor is straightforward. I specify that I want to read with -d r
(meaning read
), the length of data to get with -c 2
(meaning 2 bytes) and the address of the device to read from with -a 48
:
[root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr 0d 67 [root@pizero1 ~]# while true; do date; i2c -a 48 -d r -c 2 -m tr; sleep 1; done Sun Dec 20 00:51:45 UTC 2020 0d 67 Sun Dec 20 00:51:46 UTC 2020 0d 67 Sun Dec 20 00:51:48 UTC 2020 0d 66 Sun Dec 20 00:51:49 UTC 2020 0d 66 ^C [root@pizero1 ~]#
After some trial and error I also add -m tr
to set the addressing mode
to complete-transfer
which is needed on this I2C bus to make everything work.
The reading shown is a hex value, exactly two bytes as requested with -c 2
.
When reading temperature from this sensor I don't need to add -o
to the i2c(8)
command to specify an offset, since the temperature is stored in the first register in the device. If I wanted to read from the next register (which for the TMP117
is where the configuration is stored) I would use -o 1
:
[root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr -o 1 22 20 [root@pizero1 ~]#
All the registers in the TMP117
are 16 bits (or two bytes), but the I2C
command can read or write any number of bytes, just specify the width with -c
.
Trying to read more data than the register size just results in FF bytes being returned:
[root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr 0c bc [root@pizero1 ~]# i2c -a 48 -d r -c 4 -m tr 0c bc ff ff [root@pizero1 ~]# i2c -a 48 -d r -c 40 -m tr 0c bd ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff [root@pizero1 ~]#
The i2c(8)
utility also makes it easy to write values back to the device. The TMP117
sensor has a programmable low and high thresholds in registers 2 and 3. The first one defaults to 0x6000
and changing it is much like reading, except we want the bytes to be written on stdin
:
[root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr -o 2 60 00 [root@pizero1 ~]# echo -en "\x60\x01" | i2c -a 48 -d w -c 2 -m tr -o 2 [root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr -o 2 60 01 [root@pizero1 ~]# echo -en "\x60\x00" | i2c -a 48 -d w -c 2 -m tr -o 2 [root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr -o 2 60 00 [root@pizero1 ~]#
Reading the datasheet for the TMP117
sensor I learned that the temperature readings are stored in the first register, as a 16-bit value, which means I use -c 2
for i2c(8)
so it reads two bytes (16 bits) from the sensor.
The datasheet for the TMP117
has this to say about the register where the temperature is stored: The data in the result register is in two's complement format, has a data width of 16 bits and a resolution of 7.8125m °C.
.
Two's complement is a widely used and convenient way to work with signed (negative) numbers in binary. Basically the most significant bit is used to indicate if what follows is a positive or a negative binary number.
The following table is from the TMP117
datasheet, it shows some sample temperatures in celcius and what those readings translate to in decimal, two's complement binary, and hex:
Temperature (°C decimal) | Sensor register value (0.0078125°C resolution) | ||
---|---|---|---|
Decimal | Binary | Hex | |
–256 | -32768 | 1000000000000000 | 8000 |
-25 | -3200 | 1111001110000000 | F380 |
–0.125 | -16 | 1111111111110000 | FFF0 |
–0.0078125 | -1 | 1111111111111111 | FFFF |
0 | 0 | 0000000000000000 | 0000 |
0.0078125 | 1 | 0000000000000001 | 0001 |
0.125 | 16 | 0000000000010000 | 0010 |
1 | 128 | 0000000010000000 | 0080 |
25 | 3200 | 0000110010000000 | 0C80 |
100 | 12800 | 0011001000000000 | 3200 |
255.9921 | 32767 | 0111111111111111 | 7FFF |
I haven't worked much with two's complement format, so I started out by familiarising myself with it in Python. I borrowed the following function from SO and started out by reproducing the values in the above table:
[root@pizero1 ~]# cat twos.py #!/usr/bin/env python3 import sys def twos(binary_str, bytecount): integer = int(binary_str, bytecount) bytes = integer.to_bytes(bytecount, byteorder="big", signed=False) return int.from_bytes(bytes, byteorder="big", signed=True) print(twos(sys.argv[1], int(sys.argv[2]))) [root@pizero1 ~]#
Trying it out with the values from the table:
[root@pizero1 ~]# echo "$(python twos.py 1000000000000000 2) * 0.0078125" | bc -256.0000000 [root@pizero1 ~]# echo "$(python twos.py 1111001110000000 2) * 0.0078125" | bc -25.0000000 [root@pizero1 ~]# echo "$(python twos.py 1111111111110000 2) * 0.0078125" | bc -.1250000 [root@pizero1 ~]# echo "$(python twos.py 1111111111111111 2) * 0.0078125" | bc -.0078125 [root@pizero1 ~]# echo "$(python twos.py 0000000000000000 2) * 0.0078125" | bc 0 [root@pizero1 ~]# echo "$(python twos.py 0000000000000001 2) * 0.0078125" | bc .0078125 [root@pizero1 ~]# echo "$(python twos.py 0000000000010000 2) * 0.0078125" | bc .1250000 [root@pizero1 ~]# echo "$(python twos.py 0000000010000000 2) * 0.0078125" | bc 1.0000000 [root@pizero1 ~]# echo "$(python twos.py 0000110010000000 2) * 0.0078125" | bc 25.0000000 [root@pizero1 ~]# echo "$(python twos.py 0011001000000000 2) * 0.0078125" | bc 100.0000000 [root@pizero1 ~]# echo "$(python twos.py 0111111111111111 2) * 0.0078125" | bc 255.9921875 [root@pizero1 ~]#
Looks like it works perfectly. Going from a reading to celcius is also easy, I tested with the values from the table again:
[root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("8000"), byteorder="big", signed=True) * 0.0078125, 8))' -256.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("F380"), byteorder="big", signed=True) * 0.0078125, 8))' -25.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("FFF0"), byteorder="big", signed=True) * 0.0078125, 8))' -0.125 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("FFFF"), byteorder="big", signed=True) * 0.0078125, 8))' -0.0078125 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("0000"), byteorder="big", signed=True) * 0.0078125, 8))' 0.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("0001"), byteorder="big", signed=True) * 0.0078125, 8))' 0.0078125 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("0010"), byteorder="big", signed=True) * 0.0078125, 8))' 0.125 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("0080"), byteorder="big", signed=True) * 0.0078125, 8))' 1.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("0C80"), byteorder="big", signed=True) * 0.0078125, 8))' 25.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("3200"), byteorder="big", signed=True) * 0.0078125, 8))' 100.0 [root@pizero1 ~]# python -c 'print(round(int.from_bytes(bytearray.fromhex("7FFF"), byteorder="big", signed=True) * 0.0078125, 8))' 255.9921875 [root@pizero1 ~]#
This means taking a reading from the sensor and converting it to celcius can be done like this:
[root@pizero1 ~]# i2c -a 48 -d r -c 2 -m tr 0c 81 [root@pizero1 ~]# python -c "print(round(int.from_bytes(bytearray.fromhex('0c 81'), byteorder='big', signed=True) * 0.0078125, 8))" 25.0078125 [root@pizero1 ~]#
Working with I2C
devices from FreeBSD is easy and works very well!
A PI Zero
is cheap, and along with a TMP117 you have a nice FreeBSD based high precision temperature measurement setup for like 160,- DKK (just over 20€).
Since the QWIIC
sensors can be daisychained such a setup can easily be expanded with more sensors to measure more stuff. Most of the sensors have existing high-quality Arduino libraries (the TMP117 is no different) if interpreting the data turns out to be tricky.
Next step is to take a look at getting FreeBSD support into SparkFuns Qwiic_Py Python package, which requires getting FreeBSD support in Qwiic_I2C_Py which in turn depends on getting FreeBSD support in smbus2.
I've recently signed up for Github Sponsors meaning it is now easy to sponsor me and my work. If this post or some of my other writing, software or services have helped you then you can consider becoming a sponsor.