Hello CircuitDojo Community,

My name is Ted, and I am coming up to speed on Zephyr device driver development. I’m working with device tree overlays, and an accelerometer which does not appear in either Zephyr nor Nordic’s Connect SDK drivers.

My general question is, when crafting or extending a new Zephyr device driver, are we allowed to call the Zephyr I2C API directly? If yes, and in early driver development stage, would a fair place to do this be the project root dir, in ‘drivers/new_driver.c’?

My question applies to Zephyr’s SPI peripheral API as well.

A little context and info about where I’m working: colleagues recently shared Marti Bolivar’s Zephyr device tree talk,

I also recently found and read – and found very helpful! – Jared Wolff’s instructional blog post:

My first device driver work is in a modified copy of ncsv1.6.1 Zephyr branch samples, a copy of <code>samples/basic/blinky</code>. Following Marti’s on-line talk and class, I was able to create and successfully compile an overlay file. I haven’t written any custom driver routines yet, but I am able to call Zephyr’s macro pairing to assign a device type pointer to the overlaid device:

# Project:  blinky-modified
# File:  app.overlay
# Target board:  Sparkfun nRF9160 Thing Plus
# Description:  Zephyr device tree overlay for Kionix KX123 sensor

&i2c1 {
        compatible = "nordic,nrf-twim";
        status = "okay";
        sda-pin = <26>;
        scl-pin = <27>;
        kionix_sensor: kx132-1211@1f {
                compatible = "kionix,kx132-1211";
                reg = <0x1F>;
                label = "KX132-1211";
        };
};

Alongside app.overly, in <code>src/main.c</code> these lines declare and assign a pointer to a Zephyr type device to the device expressed in the above overlay file:

    const struct device *dev_accelerometer;

    dev_accelerometer = DEVICE_DT_GET(DT_NODELABEL(kionix_sensor));
    if (dev_accelerometer == NULL) { . . . }

At run time I see this call succeed.

With a valid device pointer I am ready to start writing (or use existing Zephyr) API code. In other, non-Zephyr RTOS environs I am used to calling an I2C API more or less directly. But here in Zephyr, this layer of function calls is obscured to me. I’ve followed the ncs example code for the LIS2DH accelerometer, and there is no obvious place where I see driver code calling I2C API functions. (This sensor also supports SPI bus connection.)

Jared Wolff’s blog post describes the purpose and use of DEVICE_AND_API_INIT, what looks like a Zephyr macro. Marti Bolivar’s overlay example shows a sensor added to device tree using two nodes, one for I2C and one for SPI. He goes on to show that no C code changes are needed to switch between the two bus types, which is pretty slick. But what happens when I need to configure a new sensor, or take a reading from it, in a manner that’s not enumerated in <$ZEPHYR_BASE>/include/drivers/sensor.h? There’s a long enumeration there which I see LIS2DH driver use some of its elements:

/**
 * @brief Sensor channels.
 */
enum sensor_channel {
        /** Acceleration on the X axis, in m/s^2. */
        SENSOR_CHAN_ACCEL_X,
        /** Acceleration on the Y axis, in m/s^2. */
        SENSOR_CHAN_ACCEL_Y,
        /** Acceleration on the Z axis, in m/s^2. */
        SENSOR_CHAN_ACCEL_Z,
        /** Acceleration on the X, Y and Z axes. */
        SENSOR_CHAN_ACCEL_XYZ,
        /** Angular velocity around the X axis, in radians/s. */
        SENSOR_CHAN_GYRO_X,
 .
 .
 .

How and where does our new driver code connect with Zephyr’s I2C and SPI bus APIs? I’ll be starting out as Jared does in his blog post, with driver work in a ‘drivers’ directory alongside src/main.c and app.overlay.

Thanks ahead of time for any help on this question!

  • Ted

    tedhavelka66

    tedhavelka66 My general question is, when crafting or extending a new Zephyr device driver, are we allowed to call the Zephyr I2C API directly? If yes, and in early driver development stage, would a fair place to do this be the project root dir, in ‘drivers/new_driver.c’?

    Not necessarily. I’m literally working on some new drivers right now in this repo (yet to be pushed) https://github.com/circuitdojo/air-quality-wing-zephyr-drivers

    Generally, I’d work on getting the drivers working and if you want to share them with the community (as I recommend you do!) then you can work on integrating them into the Zephyr tree in order to facilitate a PR.

    tedhavelka66 With a valid device pointer I am ready to start writing (or use existing Zephyr) API code. In other, non-Zephyr RTOS environs I am used to calling an I2C API more or less directly. But here in Zephyr, this layer of function calls is obscured to me. I’ve followed the ncs example code for the LIS2DH accelerometer, and there is no obvious place where I see driver code calling I2C API functions. (This sensor also supports SPI bus connection.)

    Oh they’re in there. I’ve spent lots of time in this particular driver. You’re going to want to look at lis2dh_i2c.c where the I2C action happens. LIS2DH may be a bad example to start with since it’s extremely complex due to the dual interface capabilities. I would recommend trying to use a single interface first (like I2C b/c it’s easier) and then expand to SPI, etc.

    tedhavelka66 But what happens when I need to configure a new sensor, or take a reading from it, in a manner that’s not enumerated in <$ZEPHYR_BASE>/include/drivers/sensor.h?

    I would be surprised if your sensor does not match that long enum. What type of sensor are you working with? There’s also ways to read or configure custom registers/sensors along with the sensor API. Which is handy since not every sensor is the same!

    tedhavelka66 How and where does our new driver code connect with Zephyr’s I2C and SPI bus APIs? I’ll be starting out as Jared does in his blog post, with driver work in a ‘drivers’ directory alongside src/main.c and app.overlay.

    I would definitely do that. Make sure that your drivers directory does not live within your application. You’ll want to follow my directory structure strictly.

    Good luck!

      jaredwolff
      Hello Jared,

      Thanks much for the fast reply, this is a big help! Thank you also for the pointer to file lis2dh_i2c.c. The I2C API routines here look similar to what I’ve worked with before, and it looks like the register read and write functions (for configuring sensors) are generic and will support any I2C compliant sensor.

      To clarify an early point in my initial past, I meant to say that my driver work for now I will place in a sandbox area. I would not attempt to develop a Zephyr driver in Zephyr’s official drivers path. In addition I’m sure there’s a formal review and vetting process drivers must pass before being added to Zephyr RTOS. I must get a driver working before thinking a lot about that!

      Regarding given sensor, I’m trying to bring up a Kionix KX132 accelerometer. There is good documentation on KX132 at https://www.kionix.com/product/KX132-1211.

      As for Zephyr’s pre-defined readings types, on review it is likely they cover all readings the KX132 can provide. I have not yet compared the config register maps of LIS2DH and KX132 side by side, nor carefully compared the readings they can output. Before seeing lis2dh_i2c.c it was less clear to me how I could sensibly talk to the new sensor only through the higher level, more abstracted pieces of Zephyr device and bus code.

      In terms of Zephyr driver architecture, there appear to be two important code blocks at end of lis2dh_i2c.c, a struct and an initializer:

       static const struct lis2dh_transfer_function lis2dh_i2c_transfer_fn = { 
              .read_data = lis2dh_i2c_read_data,
              .write_data = lis2dh_i2c_write_data,
              .read_reg  = lis2dh_i2c_read_reg,
              .write_reg  = lis2dh_i2c_write_reg,
              .update_reg = lis2dh_i2c_update_reg,
       };
      
       int lis2dh_i2c_init(const struct device *dev)
       {
              struct lis2dh_data *data = dev->data;
      
              data->hw_tf = &lis2dh_i2c_transfer_fn;
      
              return 0;
       }
       #endif /* DT_ANY_INST_ON_BUS_STATUS_OKAY(i2c) */

      Grepping for lis2dh_i2c_init in <$ZEPHYR_BASE> shows there’s no place this init routine is called directly by its name. However, I see there is an assignment of this routine to .bus_init in a structure in lis2dh.c:

       463 /*
       464  * Instantiation macros used when a device is on an I2C bus.
       465  */
       466
       467 #define LIS2DH_CONFIG_I2C(inst)                                         \
       468         {                                                               \
       469                 .bus_name = DT_INST_BUS_LABEL(inst),                    \
       470                 .bus_init = lis2dh_i2c_init,                            \  <-- key assignment of init routine
       471                 .bus_cfg = { .i2c_slv_addr = DT_INST_REG_ADDR(inst), }, \
       472                 .is_lsm303agr_dev = IS_LSM303AGR_DEV(inst),             \
       473                 .disc_pull_up = DISC_PULL_UP(inst),                     \
       474                 LIS2DH_CFG_INT(inst)                                    \
       475         }

      A narrow search for ‘bus_init’ yields:

       ted@localhost:~/projects/embedded/ncs/zephyr$ grep -nr bus_init ./* | grep lis2dh | grep bus_init
       ./drivers/sensor/lis2dh/lis2dh.h:193:  int (*bus_init)(const struct device *dev);
       ./drivers/sensor/lis2dh/lis2dh.c:278:  cfg->bus_init(dev);
       ./drivers/sensor/lis2dh/lis2dh.c:449:          .bus_init = lis2dh_spi_init,                            \
       ./drivers/sensor/lis2dh/lis2dh.c:470:          .bus_init = lis2dh_i2c_init,                            \
       ./samples/sensor/lis2dh/build/zephyr/zephyr.lst:7930:  cfg->bus_init(dev);

      It looks like the code on lis2dh.c line 278 is where each discovered lis2dh sensor gets initialized. I am trying to understand the sequence of code execution to initialize a driver for a device instance.

      While I too prefer to work from a simple starting example, fair to say that lis2dh.c and lis2dh_i2c.c are analogous to the files I’ll want to create for the alternate accelerometer?

      • Ted

        tedhavelka66 While I too prefer to work from a simple starting example, fair to say that lis2dh.c and lis2dh_i2c.c are analogous to the files I’ll want to create for the alternate accelerometer?

        Very likely yes. You’ll learn a lot by looking at those files. Have fun. 😀

        Terms and Conditions | Privacy Policy