How to use Zephyr shell for interactive prototyping with I2C sensors
Zephyr has a powerful interactive shell that you need to have in your bag of tricks. Two weeks ago I showed how to use the Zephyr shell to set device keys for authenticating with the Golioth platform. Today I’ll dive into using the same interface for live-debugging of i2c sensors and devices.
i2c shell basics
The ability to type out i2c commands, rather than writing/compiling/flashing code to test your changes will speed up the prototyping process with new i2c parts. My favorite feature is the scan
command which lets me verify the part is connected correctly and at the address that I expected. Let’s begin by testing that out.
1. Starting from minimal sample
For this demo I’ll be using an ESP32 and the Zephyr basic/minimal sample. As the name suggests, this starts out with almost nothing running. We need to add a KConfig file that enables GPIO, I2C, and the related Zephyr shells:
CONFIG_GPIO=y CONFIG_I2C=y CONFIG_SHELL=y CONFIG_I2C_SHELL=y
I’m using an ESP32 board with the i2c0
pins. I also have a sensor connected which is shown as a node and will be used in the next section of this demo.
&i2c0 { status = "okay"; apds9960@39 { compatible = "avago,apds9960"; reg = <0x39>; label = "APDS9960"; int-gpios = <&gpio0 26 (GPIO_ACTIVE_LOW)>; }; };
west build -b esp32 samples/basic/minimal/ west flash
This can be built and flashed as normal:
west build -b esp32 samples/basic/minimal/ west flash
2. Opening a terminal connection
From there I drop into the shell by opening the device with a serial terminal program. I like to use minicom -D /dev/ttyUSB0 --color=on
. As a side note, I have noticed with the ESP32 I need to turn off hardware flow control or else keystrokes don’t make it to the device.
3. Basic i2c in the shell
Shell commands often include help menus that can be activated by adding -h
to your command. I use this to remind me of the syntax for the shell functions I’m using.
uart:~$ i2c -h i2c - I2C commands Subcommands: scan :Scan I2C devices recover :Recover I2C bus read :Read bytes from an I2C device read_byte :Read a byte from an I2C device write :Write bytes to an I2C device write_byte :Write a byte to an I2C device uart:~$ device list devices: - clock@5000 (READY) - gpio@842500 (READY) - CRYPTOCELL_SW (READY) - uart@8000 (READY) - nrf91_socket (READY) - i2c@9000 (READY) - flash-controller@39000 (READY) - bme280@76 (READY) requires: i2c@9000 - lis2dh@18 (READY) requires: gpio@842500 requires: i2c@9000 uart:~$
Here you can see that the help menu for the i2c keyword lists the basic syntax. I also called device list
which prints out the available devices, including the i2c bus. This means the actual command I want is:
uart:~$ i2c scan i2c@9000 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- 18 -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- 39 -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- 51 -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- 76 -- 4 devices found on i2c@9000
Voila! The grid that prints out shows that I have five devices that responded on the i2c bus. Note that I also get a number of error logs, which is the naturally result of trying to talk to i2c devices that are not present.
4. Direct communication with an i2c device
I know that the device at address 0x58 is an AW9523 port expander. There is no Zephyr driver for this part, so I need to control it with my own code. Before writing the functions in Zephyr, I can try out each command to verify the behavior.
uart:~$ i2c read_byte i2c@9000 0x58 0x12 Output: 0xff uart:~$ i2c write_byte i2c@9000 0x58 0x12 0x00 uart:~$ i2c read_byte i2c@9000 0x58 0x12 Output: 0x0 uart:~$
Here I’ve read the LED mode switch register on the device, set it to LED mode, and then verified that the new setting was received. The syntax places the device address (0x58) after the read/write command, then the register address (0x12), and for write commands you then add the value you want stored on that register (0x00).
Sensor shell
But wait, what about the i2c sensors with built in Zephyr support? There’s a shell for that too! Using it is an easy way to verify your sensors are working, and to confirm what sensor channels (the uniformed types of data used by the sensor subsystem) are available.
1. Turn on the sensor shell and sensor subsystem
You may remember that I already have an APDS9960 sensor declared as a subnode in my overlay file above. But to use it, we need to configure the sensor subsystem and the sensor shell. Here is my prj.conf file with three new entries:
CONFIG_GPIO=y CONFIG_I2C=y CONFIG_SHELL=y CONFIG_I2C_SHELL=y CONFIG_SENSOR_SHELL=y CONFIG_SENSOR=y CONFIG_APDS9960=y
With those changes in place, just rebuild and flash:
west build -b esp32 samples/basic/minimal/ west flash
2. Open the terminal connection
As above, I’m using minicom -D /dev/ttyUSB0 --color=on
to open a serial connection to the Zephyr shell on the device.
3. Read sensor values in the shell
I will again use the built-in help to find the right syntax:
uart:~$ sensor -h sensor - Sensor commands Subcommands: get :Get sensor data. Channel names are optional. All channels are read when no channels are provided. Syntax: <device_name> <channel name 0> .. <channel name N> uart:~$ sensor get -h get - Get sensor data. Channel names are optional. All channels are read when no channels are provided. Syntax: <device_name> <channel name 0> .. <channel name N> Subcommands: RTC GPIO_1 GPIO_0 UART_0 I2C_0 APDS9960 uart:~$ sensor get APDS9960 channel idx=15 prox = 16.000000 channel idx=17 light = 143.000000 channel idx=19 red = 77.000000 channel idx=20 green = 56.000000 channel idx=21 blue = 39.000000 uart:~$
Can you hear my evil laugh building to a crescendo?
The help message tells us that get
is the only available sensor command, and calling help on that lists our devices. The new entry on the list is the sensor we declared in our devicetree overlay file. Calling it without specifying a channel lists out all that are available. Now compare that to the sensor_channel_get() commands in code sample for this sensor:
sensor_channel_get(dev, SENSOR_CHAN_LIGHT, &intensity); sensor_channel_get(dev, SENSOR_CHAN_PROX, &pdata);
It’s worth mentioning that we’ve done all of this using KConfig values in Zephyr. The source code for this example is literally empty:
#include <zephyr/zephyr.h>; void main(void) { }
Zephyr has so many shells!
How many shells does Zephyr have? How many stars are there in the sky? There are surely ways to answer these questions but I don’t have them in front of me right now.
Popular among our team is the Network Shell for networking diagnostics, and the OpenThread Shell was used extensively in getting our Open Thread demo up and running. I have used the Kconfig Search page on the Zephyr docs to search for other shells and that yields a lot of really interesting info.
But mostly I just ask Marcin on the Golioth firmware team. He seems to already know about all of the cool ones. Like the Kernel Shell that lets you check on threads and stacks. Here’s a shell output that I’m using to tune up how I’m using RAM. Note that I’ve over-allocated stacks for my animation threads, and the system work queue probably needs a bit more stack space, just to be safe.
uart:~$ kernel stacks 0x3ffd3d60 (real size 2048): unused 1740 usage 308 / 2048 (15 %) 0x3ffd3cc0 weather_tid (real size 2560): unused 272 usage 2288 / 2560 (89 %) 0x3ffd3c20 hello_tid (real size 2048): unused 272 usage 1776 / 2048 (86 %) 0x3ffd4490 golioth_system (real size 3072): unused 768 usage 2304 / 3072 (75 %) 0x3ffd3b80 connection_tid (real size 2048): unused 304 usage 1744 / 2048 (85 %) 0x3ffd3a40 animate_sense_tid (real size 1024): unused 524 usage 500 / 1024 (48 %) 0x3ffd3ae0 animate_ping_tid (real size 1024): unused 524 usage 500 / 1024 (48 %) 0x3ffd47d0 rx_q[0] (real size 1504): unused 256 usage 1248 / 1504 (82 %) 0x3ffd4718 net_mgmt (real size 768): unused 288 usage 480 / 768 (62 %) 0x3ffd45e0 wifi (real size 3584): unused 1660 usage 1924 / 3584 (53 %) 0x3ffd4938 esp_event (real size 4096): unused 3388 usage 708 / 4096 (17 %) 0x3ffd4530 esp_timer (real size 4096): unused 3584 usage 512 / 4096 (12 %) 0x3ffd4b20 sysworkq (real size 1024): unused 32 usage 992 / 1024 (96 %) 0x3ffd3ef8 shell_uart (real size 2048): unused 588 usage 1460 / 2048 (71 %) 0x3ffd3e20 logging (real size 2048): unused 336 usage 1712 / 2048 (83 %) 0x3ffd49d8 idle (real size 1024): unused 828 usage 196 / 1024 (19 %) 0x3ffd4a78 main (real size 4096): unused 3040 usage 1056 / 4096 (25 %) 0x3ffed510 IRQ 00 (real size 2048): unused 1776 usage 272 / 2048 (13 %) uart:~$ kernel threads Scheduler: 2745 since last call Threads: 0x3ffd3d60 options: 0x0, priority: -1 timeout: 0 state: pending, entry: 0x4008d72c stack size 2048, unused 1740, usage 308 / 2048 (15 %) 0x3ffd3cc0 weather_tid options: 0x0, priority: 14 timeout: 10093 state: suspended, entry: 0x400d16b0 stack size 2560, unused 272, usage 2288 / 2560 (89 %) 0x3ffd3c20 hello_tid options: 0x0, priority: 14 timeout: 5843 state: suspended, entry: 0x400d1b60 stack size 2048, unused 272, usage 1776 / 2048 (86 %) 0x3ffd4490 golioth_system options: 0x0, priority: 14 timeout: 3785 state: pending, entry: 0x400dba6c stack size 3072, unused 768, usage 2304 / 3072 (75 %) 0x3ffd3b80 connection_tid options: 0x0, priority: 14 timeout: 1635823 state: suspended, entry: 0x400d1c28 stack size 2048, unused 304, usage 1744 / 2048 (85 %) 0x3ffd3a40 animate_sense_tid options: 0x0, priority: 14 timeout: 592 state: suspended, entry: 0x400d1bc8 stack size 1024, unused 524, usage 500 / 1024 (48 %) 0x3ffd3ae0 animate_ping_tid options: 0x0, priority: 14 timeout: 287 state: suspended, entry: 0x400d1b20 stack size 1024, unused 524, usage 500 / 1024 (48 %) 0x3ffd47d0 rx_q[0] options: 0x0, priority: -1 timeout: 0 state: pending, entry: 0x4008872c stack size 1504, unused 256, usage 1248 / 1504 (82 %) 0x3ffd4718 net_mgmt options: 0x0, priority: -1 timeout: 0 state: pending, entry: 0x40086ff0 stack size 768, unused 288, usage 480 / 768 (62 %) 0x3ffd45e0 wifi options: 0x8, priority: 2 timeout: 0 state: pending, entry: 0x400949c4 stack size 3584, unused 1660, usage 1924 / 3584 (53 %) 0x3ffd4938 esp_event options: 0x8, priority: 4 timeout: 0 state: pending, entry: 0x400ea860 stack size 4096, unused 3388, usage 708 / 4096 (17 %) 0x3ffd4530 esp_timer options: 0x8, priority: 3 timeout: 0 state: pending, entry: 0x400dcf88 stack size 4096, unused 3584, usage 512 / 4096 (12 %) 0x3ffd4b20 sysworkq options: 0x0, priority: -1 timeout: 0 state: pending, entry: 0x4008d72c stack size 1024, unused 32, usage 992 / 1024 (96 %) *0x3ffd3ef8 shell_uart options: 0x0, priority: 14 timeout: 0 state: queued, entry: 0x400d5570 stack size 2048, unused 588, usage 1460 / 2048 (71 %) 0x3ffd3e20 logging options: 0x0, priority: 14 timeout: 36 state: pending, entry: 0x4008f538 stack size 2048, unused 336, usage 1712 / 2048 (83 %) 0x3ffd49d8 idle options: 0x1, priority: 15 timeout: 0 state: , entry: 0x4008d1b0 stack size 1024, unused 828, usage 196 / 1024 (19 %) 0x3ffd4a78 main options: 0x1, priority: 0 timeout: 184000604 state: suspended, entry: 0x4008cbd4 stack size 4096, unused 3040, usage 1056 / 4096 (25 %) uart:~$
Until next time, go out and explore Zephyr shells. Just make sure to pop into our Discord channel and let us know which shells you find the most useful!