element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Raspberry Pi Projects
  • Products
  • Raspberry Pi
  • Raspberry Pi Projects
  • More
  • Cancel
Raspberry Pi Projects
Blog Object Oriented example for the Data Acquisition Board for Pi Pico
  • Blog
  • Documents
  • Events
  • Polls
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Raspberry Pi Projects to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: Jan Cumps
  • Date Created: 18 Dec 2023 10:46 AM Date Created
  • Views 1574 views
  • Likes 9 likes
  • Comments 8 comments
  • ADC (Analog-to-Digital Converter)
  • pico
  • data acquisition
  • stl
  • rp2040
  • daq
  • pico-eurocard
  • raspberry_pi_projects
  • adc
  • test&measurement
Related
Recommended

Object Oriented example for the Data Acquisition Board for Pi Pico

Jan Cumps
Jan Cumps
18 Dec 2023
Object Oriented example for the Data Acquisition Board for Pi Pico

 shabaz designed a Data Acquisition Board for Pi Pico . In this post, I'm building a C++ class for the ADC. And I turn my continuous sample exercise into an OO one. 
Goal: investigate if  it's up to handling 1 second worth of samples, in 860 samples per second mode.

image

main()

The easiest way to show what an OO firmware looks like, is to show how the classes are used.

#include "ads1115.h"

#include <iterator>
#include <numeric>

// sample batch size in bulk mode
#define SAMPLES 860

// ************ global variables ****************
ads1115 ads = ads1115();

// ********** functions *************************
void rdy_callback(uint gpio, uint32_t events) {
  
ads.set_data_ready(true);
}

// ************ main function *******************
int main(void) {

 
  // 1: get samples using the ads1115 class

  uint16_t buf[SAMPLES];

  // check what ADC boards are installed, and initialize them
  ads.init(i2c_port);

  ads.build_data_rate(ads1115::data_rate::DR_860);
  ads.build_gain(ads1115::channel::CH_AIN1, ads1115::gain::GAIN_2_048); // +- 2.048V


  ads.build_cont_conversion();
  ads.adc_set_mux(ads1115::channel::CH_AIN1, false);

  ads.adc_enable_ready();
  ads.set_data_ready(false);
  ads.bulk_read(buf, sizeof buf);

  // 2: process data using STL constructs

  // buffer bytes will now get swapped,
  // and that's needed for the next actions
  // use stl iterator and lambda function
  std::for_each(std::begin(buf), std::end(buf), [](uint16_t& u){ u = u >> 8 | u << 8; });

  // statistics. Prereq: bytes are already swapped
  
// average
  uint16_t average = std::accumulate(std::begin(buf), std::end(buf), 0.0) / std::size(buf);
  printf("AVG = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, average));
  // min and max
  auto minmax = std::minmax_element(std::begin(buf), std::end(buf));
  printf("MIN = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.first));
  printf("MAX = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.second));

  // convert every value to voltage
  double volts[SAMPLES];
  std::transform(std::begin(buf), std::end(buf), std::begin(volts),
  [](uint16_t u){ return ads.to_volts(ads1115::channel::CH_AIN1, u); });

  // print voltage
  std::for_each(std::begin(volts), std::end(volts),
    {
printf("SMP = %.7f V\n", d);} );

  while (true) {
  }
}

This isn't the complete listing. I focused on the OO aspects. The ads1115 object is initialised, then used to get 860 samples in a buffer. The STL functionality is used to manipulate that result data.

ads1115 class

I've designed it as an OO API for the Data Acquisition Board for Pi Pico. It's able to handle one ADC, configured for differential input.

If you have one board mounted on the Pico, the class can find it by itself. If you stack multiple boards, you can create multiple objects, and pass them the address of the ADC they have to manage.

The class doesn't have a sample data buffer. I rely on the firmware to provide a buffer that it can use to collect a set of samples.

image

constructor

The constructor initialises data members and state. It doesn't do any other functionality. That's common for embedded classes. In embedded, objects (and other data) get instantiated staticly, to prevent using the heap. In that case, you don't have easy control when they are created. Definitely before you have the chance to initiate the i2c peripherals.

    ads1115::ads1115() : data_ready(false), i2c_port(nullptr), address(addr::A_LAST), dr(data_rate::DR_860) {}

The keen eye will spot that the data members are strongly typed. Read the "constants" subsection below for the explanation.

init

This is where the object learns what ADC IC it has to control.

There are two flavours. One that looks for an installed ADC IC and then initialises itself to talk to that one. If more than one daq board is mounted, the one with the lowest address is found. A second one initialises itself for a given IC address. Use that one if you have more than one daq board mounted. In that case, you'd create an object per board. Each object will know how to address the right ADC.

// configure for first found IC
bool ads1115::init(i2c_inst_t *i2c_port) { 
    bool found = false;
    uint8_t u;
    uint8_t buf[3] = {(uint8_t)reg::REG_CONFIG};
    // is the ADC chip installed?
    for (u = (uint8_t)addr::A_0; u < (uint8_t)addr::A_LAST; u++) { // test both board I2C addresses
        i2c_write_blocking(i2c_port, u, buf, 1, false);
        if (i2c_read_blocking(i2c_port, u, buf, 2, false) != PICO_ERROR_GENERIC) { 
            found = ads1115::init(i2c_port, (addr)u); // chip found!
            break;
        }
    }
    return found;
}

// configure for IC with provided address
bool ads1115::init(i2c_inst_t *i2c_port, addr address) { 
    this->i2c_port = i2c_port;
    this->address = address;
    return true;
}

prepare

These functions gather all info needed to configure the ADC, before actually starting the sample. They are the counterparts of Shabaz' build_***() functions. They prepare the register configuration values before they are actually written to the ADC.

// these build_xxx functions set up the local config register variables,
// for later programming into the ADC.

void ads1115::build_data_rate(data_rate dr) {
    this->dr = dr;
    adc_confreg[1] &= ~0xE0; // clear the DR bits
    adc_confreg[1] |= ((uint8_t)dr << 5); // set the DR bits
}

void ads1115::build_gain(channel chan, gain gain) {
    adc_range[(uint8_t)chan] = adc_range_value[(uint8_t)gain];
    adc_gain_bits[(uint8_t)chan] = gain;
    adc_confreg[0] &= ~0x0E; // clear the PGA bits
    adc_confreg[0] |= ((uint8_t)gain << 1); // set the PGA bits
}

void ads1115::build_cont_conversion() {
    adc_confreg[1] &= ~0x01; // clear the single-shot mode bit
}

void ads1115::build_single_conversion() {
    adc_confreg[1] |= 0x01; // set the single-shot mode bit
}

These members only change the object state. There's no communication with the ADC at all.

action, sample

These members talk to the IC. Registers are set, multiplexer configured, Ready pin behaviour set up, samples taken.

// selects either the first or second channel on the ADC board
// this ends up programming the entire config register
// if do_single_conversion is 1 then a single conversion will be immediately started.
void ads1115::adc_set_mux(channel chan, bool do_single_conversion) {
    uint8_t mux;
    uint8_t buf[3];
    switch(chan) {
        case channel::CH_AIN1:
            mux = (uint8_t)diff::DIFF_0_1;
            break;
        case channel::CH_AIN2:
            mux = (uint8_t)diff::DIFF_2_3;
            break;
    }
    adc_confreg[0] &= ~0x70; // clear the MUX bits
    adc_confreg[0] |= (mux << 4); // set the MUX bits
    buf[0] = (uint8_t)reg::REG_CONFIG;
    buf[1] = adc_confreg[0];
    buf[2] = adc_confreg[1];
    if (do_single_conversion) {
        buf[1] |= 0x01; // set MODE bit to single-shot mode
        buf[1] |= 0x80; // set START bit to start conversion
    }
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

void ads1115::adc_enable_ready() {
    uint8_t buf[3];

    buf[0] = (uint8_t)reg::REG_LO_THRESH;
    buf[1] = 0x00;
    buf[2] = 0x00; //Lo_thresh MS bit must be 0
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);

    buf[0] = (uint8_t)reg::REG_HI_THRESH;
    buf[1] = 0x80;
    buf[2] = 0x00; //Hi_thresh  MS bit must be 1
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

void ads1115::bulk_read(uint16_t* buf, size_t len) {

    uint8_t* begin = (uint8_t*)buf;
    uint8_t* end = ((uint8_t*)buf) + len;

    uint8_t reg = (uint8_t)reg::REG_CONVERSION;
    i2c_write_blocking(i2c_port, (uint8_t)address, &reg, 1, false);

    set_data_ready(false);
    while (begin < end) {
        while(!is_data_ready()) {/* wait */ }
        set_data_ready(false);
        i2c_read_blocking(i2c_port, (uint8_t)address, begin, 2, false);
        begin += 2;
    }
}

void ads1115::start_single_conversion() {
    uint8_t buf[3];
    build_single_conversion();
    buf[0] = (uint8_t)reg::REG_CONFIG;
    buf[1] = adc_confreg[0];
    buf[2] = adc_confreg[1];
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

// read the conversion register
uint16_t ads1115::adc_raw_diff_result() {
    uint16_t buf;
    uint8_t* buf8_ptr = (uint8_t*)&buf;
    *buf8_ptr = (uint8_t)reg::REG_CONVERSION;
    i2c_write_blocking(i2c_port, (*this)(address), buf8_ptr, 1, false);
    i2c_read_blocking(i2c_port, (*this)(address), buf8_ptr, 2, false);
    return(buf);
}

Although the i2c traffic delivers the bytes in a sample word in the wrong order for the RP2040 endian, the bytes of a sample aren't swapped in the class. It's a design choice, to spend as little time as possible in the sample blocks. Discuss if you 'd do that differently.

conversion ready

These function add support for the sample timing. Optional when running in single mode. In bulk mode, something has to tell our object that the next sample is ready. You can either use an interrupt from the ADC RDY pin (I use that in my example) or a timer interrupt, to inform the class that the next sample is ready to be read from the conversion register.

    inline bool is_data_ready() {return data_ready;}
    inline void set_data_ready(bool data_ready) {this->data_ready = data_ready;}

convert

This member knows how to translate a raw sample into a voltage. It expects that the sampled word has its bytes swapped.

    // convert the raw value to volts present at the ADC input
    // adjust for opamp gain
    inline double to_volts(channel chan, uint16_t raw) {
        return (double)(((int16_t)raw) * (adc_range[(uint8_t)chan] / 32768.0)) / opamp_gain[(uint8_t)chan];
    }

A theme in the design is that the samples aren't converted to signed raw values or floating point voltages. Unless needed. Most of the statistics can run on the raw 2-complement results. When needed, the STL can be used to transform these values into a different representation. In my example code, I show this. I transform the buffer of 2-compliment uint words to floating point voltages before printing them.

constants

class ads1115 {
public:
    enum class addr : uint8_t {
        A_0  = 0x48, 
        A_1,
        A_2,
        A_3,
        A_LAST
    };

    enum class data_rate : uint8_t {
        DR_8 = 0,
        DR_16,
        DR_32,
        DR_64,
        DR_128,
        DR_250,
        DR_475, 
        DR_860
    };

    enum class gain : uint8_t {
        GAIN_6_144 = 0,
        GAIN_4_096,
        GAIN_2_048,
        GAIN_1_024,
        GAIN_0_512,
        GAIN_0_256
    };

    enum class channel : uint8_t {
        CH_AIN1 = 0,
        CH_AIN2
    };

private:
    enum class reg : uint8_t {
        REG_CONVERSION = 0x00,
        REG_CONFIG,
        REG_LO_THRESH,
        REG_HI_THRESH
    };

    enum class diff: uint8_t {
        DIFF_0_1 = 0x00,
        DIFF_2_3 = 0x03
    };

I used C++ enumeration classes. That gives me strongly typed constants. A programmer can only assign a known set of values to an ADC address, register address, speed, .... That removes the need to validate the values a user passes to the class members. 

Only constants that should be available to the developer are public: address, gain, data rate, channel.

Example: 

ads.build_data_rate(ads1115::data_rate::DR_860);

The member function is typed, and that type only defines valid options. The code protects itself.

    enum class data_rate : uint8_t {
        DR_8 = 0,
        DR_16,
        DR_32,
        DR_64,
        DR_128,
        DR_250,
        DR_475,
        DR_860
    };

Developers can force-override this, but then it is a wanted jailbreak. It's fair that jailbreakers should do their own error handling.

other C++ constructs: STL to process samples

I use STL Iterators, Lambda functions and Callables to work with the raw data buffer. Each of these exercises have their separate blog post. When you use these in your own code, you 'll notice that they add virtually no overhead. STL is specialised in resolving the abstractions as much as possible at compile time. In my code below, the price is equal to using C style for loops

    // buffer bytes will now get swapped, 
    // and that's needed for the next actions
    // use stl iterator and lambda function
    std::for_each(std::begin(buf), std::end(buf), [](uint16_t& u){ u = u >> 8 | u << 8; }); // swap bytes

    // statistics. Prereq: bytes are already swapped

    // average
    uint16_t average = std::accumulate(std::begin(buf), std::end(buf), 0.0) / std::size(buf);
    printf("AVG = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, average));
    // min and max
    auto minmax = std::minmax_element(std::begin(buf), std::end(buf));
    printf("MIN = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.first));
    printf("MAX = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.second));

    // convert every value to voltage
    double volts[SAMPLES];
    std::transform(std::begin(buf), std::end(buf), std::begin(volts),
               [](uint16_t u){ return ads.to_volts(ads1115::channel::CH_AIN1, u); });

    // print voltage
    std::for_each(std::begin(volts), std::end(volts), [](double& d) { 
        printf("SMP = %.7f V\n", d);
    }); 

This code works with or without the ads1115 class. It deals with the raw buffer of samples. The only time it uses the object is to call it's voltage conversion functionality.

See:

  •  Average bulk samples from Data Acquisition Board for Pi Pico with C++ STL 
  •  Converting bulk samples from Data Acquisition Board for Pi Pico with C++ STL 
  •  Min and Max bulk samples from Data Acquisition Board for Pi Pico with C++ STL 

The Code

You'll find a link to the Github repository at the end of this post. The repo is a clone of Shabaz' code for the daq. I created a new branch for the OO version. That repo has a CMake file and can be built in the same way as the Pico examples from the Pico C/C++ SDK.

class header

#ifndef _ADS1115_H_
#define _ADS1115_H_

#include "hardware/gpio.h"
#include "hardware/i2c.h"

class ads1115 {
public:
    enum class addr : uint8_t {
        A_0  = 0x48, 
        A_1,
        A_2,
        A_3,
        A_LAST
    };

    enum class data_rate : uint8_t {
        DR_8 = 0,
        DR_16,
        DR_32,
        DR_64,
        DR_128,
        DR_250,
        DR_475, 
        DR_860
    };

    enum class gain : uint8_t {
        GAIN_6_144 = 0,
        GAIN_4_096,
        GAIN_2_048,
        GAIN_1_024,
        GAIN_0_512,
        GAIN_0_256
    };

    enum class channel : uint8_t {
        CH_AIN1 = 0,
        CH_AIN2
    };

private:
    enum class reg : uint8_t {
        REG_CONVERSION = 0x00,
        REG_CONFIG,
        REG_LO_THRESH,
        REG_HI_THRESH
    };

    enum class diff: uint8_t {
        DIFF_0_1 = 0x00,
        DIFF_2_3 = 0x03
    };
    
    volatile bool data_ready;
    i2c_inst_t *i2c_port;
    addr address;
    data_rate dr;
    uint8_t adc_confreg[2] = {0, 0};
    gain adc_gain_bits[2] = {gain::GAIN_2_048, gain::GAIN_2_048};
    double adc_range[2] = {2.048, 2.048};
    double adc_range_value[8] = {6.144, 4.096, 2.048, 1.024, 0.512, 0.256};
    double opamp_gain[2] = {0.38298, 0.38298};

public:
    ads1115() : data_ready(false), i2c_port(nullptr), address(addr::A_LAST), dr(data_rate::DR_860) {}

    bool init(i2c_inst_t *i2c_port); // configure for first found IC
    bool init(i2c_inst_t *i2c_port, addr address); // configure for IC with provided address

    inline bool is_data_ready() {return data_ready;}
    inline void set_data_ready(bool data_ready) {this->data_ready = data_ready;}

    void build_data_rate(data_rate dr);
    void build_gain(channel chan, gain gain);
    void build_cont_conversion();
    void build_single_conversion();

    void adc_set_mux(channel chan, bool do_single_conversion);
    void adc_enable_ready();

    void bulk_read(uint16_t* buf, size_t len);

    void start_single_conversion();
    uint16_t adc_raw_diff_result();

    // convert the raw value to volts present at the ADC input
    // adjust for opamp gain
    inline double to_volts(channel chan, uint16_t raw) {
        return (double)(((int16_t)raw) * (adc_range[(uint8_t)chan] / 32768.0)) / opamp_gain[(uint8_t)chan];
    }
};

#endif // _ADS1115_H_

class implementation

#include "ads1115.h"

// configure for first found IC
bool ads1115::init(i2c_inst_t *i2c_port) { 
    bool found = false;
    uint8_t u;
    uint8_t buf[3] = {(uint8_t)reg::REG_CONFIG};
    // is the ADC chip installed?
    for (u = (uint8_t)addr::A_0; u < (uint8_t)addr::A_LAST; u++) { // test both board I2C addresses
        i2c_write_blocking(i2c_port, u, buf, 1, false);
        if (i2c_read_blocking(i2c_port, u, buf, 2, false) != PICO_ERROR_GENERIC) { 
            found = ads1115::init(i2c_port, (addr)u); // chip found!
            break;
        }
    }
    return found;
}

// configure for IC with provided address
bool ads1115::init(i2c_inst_t *i2c_port, addr address) { 
    this->i2c_port = i2c_port;
    this->address = address;
    return true;
}

// these build_xxx functions set up the local config register variables,
// for later programming into the ADC.

void ads1115::build_data_rate(data_rate dr) {
    this->dr = dr;
    adc_confreg[1] &= ~0xE0; // clear the DR bits
    adc_confreg[1] |= ((uint8_t)dr << 5); // set the DR bits
}

void ads1115::build_gain(channel chan, gain gain) {
    adc_range[(uint8_t)chan] = adc_range_value[(uint8_t)gain];
    adc_gain_bits[(uint8_t)chan] = gain;
    adc_confreg[0] &= ~0x0E; // clear the PGA bits
    adc_confreg[0] |= ((uint8_t)gain << 1); // set the PGA bits
}

void ads1115::build_cont_conversion() {
    adc_confreg[1] &= ~0x01; // clear the single-shot mode bit
}

void ads1115::build_single_conversion() {
    adc_confreg[1] |= 0x01; // set the single-shot mode bit
}

// selects either the first or second channel on the ADC board
// this ends up programming the entire config register
// if do_single_conversion is 1 then a single conversion will be immediately started.
void ads1115::adc_set_mux(channel chan, bool do_single_conversion) {
    uint8_t mux;
    uint8_t buf[3];
    switch(chan) {
        case channel::CH_AIN1:
            mux = (uint8_t)diff::DIFF_0_1;
            break;
        case channel::CH_AIN2:
            mux = (uint8_t)diff::DIFF_2_3;
            break;
    }
    adc_confreg[0] &= ~0x70; // clear the MUX bits
    adc_confreg[0] |= (mux << 4); // set the MUX bits
    buf[0] = (uint8_t)reg::REG_CONFIG;
    buf[1] = adc_confreg[0];
    buf[2] = adc_confreg[1];
    if (do_single_conversion) {
        buf[1] |= 0x01; // set MODE bit to single-shot mode
        buf[1] |= 0x80; // set START bit to start conversion
    }
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

void ads1115::adc_enable_ready() {
    uint8_t buf[3];

    buf[0] = (uint8_t)reg::REG_LO_THRESH;
    buf[1] = 0x00;
    buf[2] = 0x00; //Lo_thresh MS bit must be 0
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);

    buf[0] = (uint8_t)reg::REG_HI_THRESH;
    buf[1] = 0x80;
    buf[2] = 0x00; //Hi_thresh  MS bit must be 1
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

void ads1115::bulk_read(uint16_t* buf, size_t len) {

    uint8_t* begin = (uint8_t*)buf;
    uint8_t* end = ((uint8_t*)buf) + len;

    uint8_t reg = (uint8_t)reg::REG_CONVERSION;
    i2c_write_blocking(i2c_port, (uint8_t)address, &reg, 1, false);

    set_data_ready(false);
    while (begin < end) {
        while(!is_data_ready()) {/* wait */ }
        set_data_ready(false);
        i2c_read_blocking(i2c_port, (uint8_t)address, begin, 2, false);
        begin += 2;
    }
}

void ads1115::start_single_conversion() {
    uint8_t buf[3];
    build_single_conversion();
    buf[0] = (uint8_t)reg::REG_CONFIG;
    buf[1] = adc_confreg[0];
    buf[2] = adc_confreg[1];
    i2c_write_blocking(i2c_port, (uint8_t)address, buf, 3, false);
}

// read the conversion register
uint16_t ads1115::adc_raw_diff_result() {
    uint16_t buf;
    uint8_t* buf8_ptr = (uint8_t*)&buf;
    *buf8_ptr = (uint8_t)reg::REG_CONVERSION;
    i2c_write_blocking(i2c_port, (*this)(address), buf8_ptr, 1, false);
    i2c_read_blocking(i2c_port, (*this)(address), buf8_ptr, 2, false);
    return(buf);
}

example

/************************************************************************
 * adc_board_test
 * main.cpp
 * rev 1.0 - shabaz - oct 2023
 * convert to bulk mode and cpp jc 20231218
 ************************************************************************/

// ************* header files ******************

#include <stdio.h>
#include <string.h>
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include "pico/stdio.h"
#include "hardware/i2c.h"
#include "hardware/irq.h"

#include "ads1115.h"

#include <iterator>
#include <numeric>

// I2C
// set this value to 0 or 1 depending on 0-ohm resistor positions on ADC board.
// set the value to 0 if the resistors are at the I2C0 labeled position,
// set the value to 1 if the resistors are at the unlabeled position (which will be I2C1)
// if the user did not set a selection, select I2C1 (shabaz solder instructions default)
#ifndef I2C_PORT_SELECTED
#define I2C_PORT_SELECTED 1
#endif // I2C_PORT_SELECTED

// these pins are supported by the ADC board:
#define I2C_SDA0_PIN 4
#define I2C_SCL0_PIN 5
#define I2C_SDA1_PIN 6
#define I2C_SCL1_PIN 7

#define ADC_RDY 13

// sample batch size in bulk mode
#define SAMPLES 860

// ************ global variables ****************

i2c_inst_t *i2c_port;
ads1115 ads = ads1115();

// ********** functions *************************

void rdy_callback(uint gpio, uint32_t events) {
    ads.set_data_ready(true);
}

// board initialisation
void board_init() {
    // I2C init
    if (I2C_PORT_SELECTED == 0) {
        i2c_port = &i2c0_inst;
    } else {
        i2c_port = &i2c1_inst;
    }
    i2c_init(i2c_port, 1000*1000);// 1 MHz I2C clock
    
    if (I2C_PORT_SELECTED == 0) {
        gpio_set_function(I2C_SDA0_PIN, GPIO_FUNC_I2C);
        gpio_set_function(I2C_SCL0_PIN, GPIO_FUNC_I2C);
        gpio_pull_up(I2C_SDA0_PIN); // weak pull-ups but enable them anyway
        gpio_pull_up(I2C_SCL0_PIN);
    } else {
        gpio_set_function(I2C_SDA1_PIN, GPIO_FUNC_I2C);
        gpio_set_function(I2C_SCL1_PIN, GPIO_FUNC_I2C);
        gpio_pull_up(I2C_SDA1_PIN); // weak pull-ups but enable them anyway
        gpio_pull_up(I2C_SCL1_PIN);
    }
 
    // trigger and listener for ADC RDY pin
    gpio_init(ADC_RDY);
    gpio_pull_up(ADC_RDY);
    gpio_set_dir(ADC_RDY, false);
    gpio_set_irq_enabled_with_callback(ADC_RDY, GPIO_IRQ_EDGE_RISE, true, &rdy_callback);

}

// ************ main function *******************
int main(void) {
    uint16_t buf[SAMPLES];

    stdio_init_all();
    board_init();

    // check what ADC boards are installed, and initialize them
    ads.init(i2c_port);
    sleep_ms(5 * 1000);

    ads.build_data_rate(ads1115::data_rate::DR_860);
    ads.build_gain(ads1115::channel::CH_AIN1, ads1115::gain::GAIN_2_048); // +- 2.048V

    
    ads.build_cont_conversion();
    ads.adc_set_mux(ads1115::channel::CH_AIN1, false);

    ads.adc_enable_ready();
    ads.set_data_ready(false);
    ads.bulk_read(buf, sizeof buf);

    // buffer bytes will now get swapped, 
    // and that's needed for the next actions
    // use stl iterator and lambda function
    std::for_each(std::begin(buf), std::end(buf), [](uint16_t& u){ u = u >> 8 | u << 8; }); // swap bytes

    // statistics. Prereq: bytes are already swapped

    // average
    uint16_t average = std::accumulate(std::begin(buf), std::end(buf), 0.0) / std::size(buf);
    printf("AVG = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, average));
    // min and max
    auto minmax = std::minmax_element(std::begin(buf), std::end(buf));
    printf("MIN = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.first));
    printf("MAX = %.7f V\n", ads.to_volts(ads1115::channel::CH_AIN1, *minmax.second));

    // convert every value to voltage
    double volts[SAMPLES];
    std::transform(std::begin(buf), std::end(buf), std::begin(volts),
               [](uint16_t u){ return ads.to_volts(ads1115::channel::CH_AIN1, u); });

    // print voltage
    std::for_each(std::begin(volts), std::end(volts), [](double& d) { 
        printf("SMP = %.7f V\n", d);
    }); 

    while (true) {        
    }
}

Enjoy!

Source: https://github.com/jancumps/adc_board_test/tree/object-oriented

Firmware (.uf2): adc_board_test.zip

  • Sign in to reply
  • Jan Cumps
    Jan Cumps over 1 year ago

    Follow-up:  OO example for the Data Acquisition Board for Pi Pico: deep dive into the sample data buffer 

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to Jan Cumps

    or, both options,

    • a flavour that takes iterators and
    • one that takes a container (can be a C plain array)

        // get bulk samples by passing iterators to buffer
        template<typename Iterator> void bulk_read(Iterator begin,  Iterator end) {
            // defined in header: https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file
            uint8_t reg = (uint8_t)reg::REG_CONVERSION;
            i2c_write_blocking(i2c_port, (*this)(address), &reg, 1, false);
            set_data_ready(false);
    
            std::for_each(begin, end, [this](uint16_t& u){ // capture "this", allows to call its methods
                while(!is_data_ready()) {/* wait */ }
                set_data_ready(false);
                // need to cast the uint16_t to a buffer of 2 uint8_t
                i2c_read_blocking(i2c_port, (*this)(address), reinterpret_cast<uint8_t *>(&u), 2, false);
            });
        }
    
        // get bulk samples by passing a container (can be a C array)
        void bulk_read(std::span<uint16_t> buf) {
            bulk_read(buf.begin(), buf.end());
        }

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to ggabe

     ggabe I'm testing this alternative. It 'll allow to use a buffer, or a container that can be used with iterators

        template<typename Iterator> void bulk_read_2(Iterator begin,  Iterator end) {
            // defined in header: https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file
            uint8_t reg = (uint8_t)reg::REG_CONVERSION;
            i2c_write_blocking(i2c_port, (*this)(address), &reg, 1, false);
            set_data_ready(false);
    
            std::for_each(begin, end, [this](uint16_t& u){ // capture "this", allows to call its methods
                while(!is_data_ready()) {/* wait */ }
                set_data_ready(false);
                // need to cast the uint16_t to a buffer of 2 uint8_t
                i2c_read_blocking(i2c_port, (*this)(address), reinterpret_cast<uint8_t *>(&u), 2, false);
            });
        }

    Here is an example of calling it with a traditional buffer. But it 'd also work for other sequence containers: array, vector, deque, list, forward_list.

    uint16_t buf[SAMPLES];
    // ...
    ads.bulk_read_2(std::begin(buf), std::end(buf));

    I have been thinking (again about wrapping the buffer. But I think that allowing the code to work with any STL compliant sequence container (includes a traditional C array) is more flexible.

    The developer can, if desired, develop a container that knows how to convert, print, do  statistics, ... . As long as it exposes the data with updatable iterators.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps over 1 year ago in reply to ggabe

    I have thought about that. I could either create a vector, and expose the internal buffer to the i2c api. Or wrap the buffer in one of the container adapters.
    Or use a container interface in the class, and let the user decide what container it passes to the class ...

    At this moment, I seem to get all I need from the raw C buffer, and the STL iterator adapters. STL can be used to transform it into what the programmer needs.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • shabaz
    shabaz over 1 year ago

    Hi Jan,

    This is a pretty detailed example of using STL and it's all microcontroller-friendly. This is a superb article that I'm going to make use of, since I mostly stick with the few STL features I'm familiar with (e.g. vector, map, pair) and need to learn more of them. Also some very neat features used in the coding, e.g. enum class, I've not seen before.

    Although the code here is useful for those using the ADS111x series of chips, it's a great resource for anyone to simply study the code and apply the techniques elsewhere.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
>
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube