Maker.io main logo

ADS1115 in Zephyr - Using a new I2C device in Zephyr without drivers

2024-12-27 | By Yalu Ouyang

ADCs

ADS1115 in Zephyr - Using a new I2C device in Zephyr without drivers

This article assumes that you are already familiar with using ADS1115 in other simpler platforms such as Arduino, but you are still confused about how to use ADS1115 in Zephyr. If you are not familiar with ADS1115, please help yourself to Adafruit’s guide. Or if you just want to look at the code to make this possible, here it is.

Devicetree

First, we will start with the devicetree overlay. If you are not familiar with a devicetree, it is a configuration file for the compiler to know what hardware definitions to prepare for you in your C code, so that you can reference them.

Copy Code
&lpi2c1 { 
‎    status = "okay"; 
‎    clock-frequency = <I2C_BITRATE_FAST>; 
‎ 
‎    ADS1115name:ADS1115@48 { 
‎        compatible = "ti,ADS1115"; 
‎        status = "okay"; 
‎        reg = <0x48>; 
‎        label = "ADS1115 addr48 @ i2c1"; 
‎        #io-channel-cells = <1>; #address-cells = <1>; #size-cells = <0>; 
‎    }; 
‎};‎
 

Let’s go through the things you probably would need to change.

  • The first thing that needs to change is lpi2c1 on line 1. This should be the name of the I2C of your board. I am using teensy 4.1 as my board. For your board, you can find it either through documentation you search through Google, or through your board’s devicetree definitions in the Zephyr folder: zephyrproject/zephyr/boards. The file you are looking for should have a file extension of “.dts”
  • Second thing is to change ADS1115name on line 5. This is the name of this instance of ADS1115. You cannot have capital letters, underscores, or symbols in this name. This name is global in all devicetrees.
  • Last thing you might need to change is the number 48 on both line 5 and line 8. This is the address of ADS1115 you are using. If you are using 0x48, then leave it as is.

Now let’s understand what this devicetree entry is saying. On the first line, the “&” symbol means we are adding to an existing device, if you look at the .dts file of your board, it will reveal that there are most configurations for the same device. On the second line, status = “okay” means that this device is enabled as opposed to status = “disabled.” On line 6, compatible = “ti,ADS1115”; means that it is compatible with this driver. But this driver doesn’t allow you to read from rdy interrupts nor cycle through the different channels of input through the mux; therefore, we will not be using it here. And for the number of channel cells and address cells, it is only 1 instead of 4 because the drivers don’t support it, but we can manually reference each channel without the drivers as we can see later. All the other lines are pretty self-explanatory.

Using I2C in the C code

Copy Code
#include <zephyr/kernel.h> #include <zephyr/device.h> #include <zephyr/drivers/i2c.h> 
‎ 
‎ 
‎#include "ads1x15.h" #include "ads1115_defs.h" 
‎ 
‎ 
‎// i2c adc devices 
struct i2c_dt_spec ads_i2c = I2C_DT_SPEC_GET(DT_ALIAS(adcchamberpressure)); 
‎ 
‎ 
int main_thread(void) 
‎{ 
‎   int ret; 
‎ 
‎ 
‎   if (!device_is_ready(ads_i2c->bus)) 
‎   { 
‎       printk("the i2c bus for the ADS1115 is not ready\n"); 
‎       return -1; 
‎   } 
‎ 
‎ 
‎   while (true) 
‎   { 
‎       // send configuration to ADS1115 
‎       ret = ADS1x15_set_config( 
‎           ads_i2c, 
‎           (ADS1115_REG_CONFIG_MUX_DIFF_0_1 | 
‎            ADS1115_REG_CONFIG_OS_SINGLE | 
‎            ADS1115_REG_CONFIG_PGA_6_144V | 
‎            ADS1115_REG_CONFIG_DR_860SPS | 
‎            ADS1115_REG_CONFIG_MODE_CONTIN | 
‎            ADS1115_REG_CONFIG_CMODE_TRAD | 
‎            ADS1115_REG_CONFIG_CPOL_ACTVLOW | 
‎            ADS1115_REG_CONFIG_CLAT_NONLAT | 
‎            ADS1115_REG_CONFIG_CQUE_1CONV)); 
‎       if (ret != 0) 
‎           return 1; 
‎ 
‎ 
‎       // wait for data 
‎       k_sleep(K_MSEC(100)); 
‎ 
‎ 
‎       int16_t 16bit_number = ADS1x15_read(ads_i2c); 
‎       double voltage = 16bit_number * (6.144 / (32768)); 
‎       printk("channel 1: %lf", bufferVolts); 
‎   } 
‎ 
‎ 
‎   return 0; 
‎}‎
 

K_THREAD_DEFINE(thread_main, 40960, main_thread, NULL, NULL, NULL, 1, 0, 0);

This code demonstrates how to configure and read data from the ADS1115 using the I2C protocol in Zephyr. The main_thread function first checks if the I2C bus is ready, then enters a loop where it configures the ADS1115 and in the while loop reads voltages from ADS1115 continuously.

Within the while loop, we send the configuration to ADS1115 on line 25, which triggers the ADS1115 to start sampling the analog voltage. Here, there is a lot of configurations that we are sending to the ADS1115. Let's go through them one by one:

  • Multiplexer Setting (ADS1115_REG_CONFIG_MUX_DIFF_0_1): The device will measure the differential voltage between channels AIN0 and AIN1, which is useful for measuring small differential signals.
  • Operational Status (ADS1115_REG_CONFIG_OS_SINGLE): Initiates a single conversion when writing to the register. This is important for triggering the measurement process.
  • Programmable Gain Amplifier (ADS1115_REG_CONFIG_PGA_6_144V): The ADC can measure a wide input voltage range of ±6.144V. This is important for applications where the signal amplitude can vary significantly.
  • Data Rate (ADS1115_REG_CONFIG_DR_860SPS): The ADC will sample at the fastest rate of 860 samples per second. This is useful for high-speed data acquisition applications.
  • Mode Selection (ADS1115_REG_CONFIG_MODE_CONTIN): The ADC will continuously perform conversions, which is suitable for continuous monitoring applications.
  • Comparator Mode (ADS1115_REG_CONFIG_CMODE_TRAD): Uses a traditional comparator with hysteresis to prevent false triggering due to noise.
  • Comparator Polarity (ADS1115_REG_CONFIG_CPOL_ACTVLOW): The ALERT/RDY pin will be low when active, which can be useful for signaling conditions to a microcontroller.
  • Comparator Latching (ADS1115_REG_CONFIG_CLAT_NONLAT): The comparator does not latch, meaning the ALERT/RDY pin will go back to its non-active state once the condition is no longer true.
  • Comparator Queue (ADS1115_REG_CONFIG_CQUE_1CONV): The comparator will trigger an alert after one conversion. This is useful for quickly responding to threshold conditions.

If you want to alter these configurations, please substitute each one according to the macros defined in this file.

And after we set the configuration, we then read the data from ADS1115 and scale it from the range of -32768 to 32768 to the range of -6.144 to 6.144. This will change a bit if you choose a different configuration for the Programmable Gain Amplifier in the configurations. With the correct voltage value now parsed, we print it out and finish our task of getting voltage from the ADS1115 ADC into Zephyr and ready to be used in your program.

But before we wrap up, we did use 2 unexplained functions (ADS1x15_set_config(ads_i2c, config_bitmask) and ADS1x15_read(ads_i2c)). So, let’s also look into what is going on in these functions.

Copy Code
static int ADS1x15_set_config(const struct i2c_dt_spec *ads_i2c, uint16_t config_bitmask) 
‎{ 
‎   int err; 
‎   uint8_t sendConfigBuff[3]; 
‎ 
‎ 
‎   sendConfigBuff[0] = ADS1115_REG_POINTER_CONFIG; 
‎   *((int16_t *)&sendConfigBuff[1]) = sys_cpu_to_be16(config_bitmask); 
‎ 
‎ 
‎   err = i2c_write_dt(ads_i2c, sendConfigBuff, 3); 
‎   if (err != 0) 
‎   { 
‎       printk("write i2c error\n"); 
‎       return -1; 
‎   } 
‎   return 0; 
‎}‎
 

Here, we send this configuration to ADS1115 by first reordering the bytes to be in big-endian as my microcontroller on Teensy 4.1 is in little-endian. This is done with the sys_cpu_to_be16 function. Then, we can write these bits to the ADS1115 with the i2c_write_dt function, completing the task.

Copy Code
static int16_t ADS1x15_read(const struct i2c_dt_spec *ads_i2c) 
‎{ 
‎   int err; 
‎   static const uint8_t sendDataReqBuff = ADS1115_REG_POINTER_CONVERT; 
‎   err = i2c_write_dt(ads_i2c, &sendDataReqBuff, 1); 
‎ 
‎ 
‎   uint8_t rx_bytes[2]; 
‎   err = i2c_read_dt(ads_i2c, rx_bytes, 2); 
‎   int16_t value = ((rx_bytes[0] << 8) | rx_bytes[1]); 
‎   return value; 
‎}‎
 

Here, we get data back from ADS1115 by telling the address of the data that we want to retrieve. In this case, we stored this in the ADS1115_REG_POINTER_CONVERT macro. We write this request to the ADS1115 with i2c_write_dt just like when we set the configuration. We follow this request to the operating system with a request to read from the I2C bus into the byte array of rx_bytes with the call i2c_read_dt(ads_i2c, rx_bytes, 2).

Integrating the ADS1115 into a Zephyr-based project involves several crucial steps, from configuring the devicetree overlay to writing the appropriate C code for I2C communication. This guide walked through setting up the devicetree for the ADS1115 and writing C code to configure and read data from the ADC. Understanding the specific configuration settings for the ADS1115, such as the multiplexer, gain, and data rate, is essential for tailoring the ADC to your application's needs. Hopefully, you find this article helpful, you can now leverage the ADS1115's capabilities within the Zephyr environment, enabling robust and precise analog-to-digital conversion in your projects.

制造商零件编号 DEV-16771
TEENSY 4.1
SparkFun Electronics
制造商零件编号 1085
ADS1115 16BIT ADC 4CH PROG GAIN
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.