element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • About Us
  • 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 OO example for the Data Acquisition Board for Pi Pico: deep dive into the sample data buffer
  • 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: 20 Dec 2023 11:22 AM Date Created
  • Views 1315 views
  • Likes 11 likes
  • Comments 6 comments
  • ADC (Analog-to-Digital Converter)
  • pico
  • data acquisition
  • stl
  • rp2040
  • daq
  • pico-eurocard
  • raspberry_pi_projects
  • adc
  • test&measurement
Related
Recommended

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

Jan Cumps
Jan Cumps
20 Dec 2023

 shabaz designed a Data Acquisition Board for Pi Pico . In this post, I'm simplifying the C++ class for the ADC. Focus on working with a wide set of data containers.
Goal: investigate if I can let it fill a classical C array, or STL Sequence Containers, Ranges and Views. 1 second worth of samples, in 860 samples per second mode.

image
image is ai generated with nightcafe

How was the data buffer handled in post 1

In the 1st post, I passed a C style array of uint to the ADS class. And the class would fill that buffer with samples.

  uint16_t buf[SAMPLES];
  ads.bulk_read(buf, sizeof buf);

The class then used our traditional loops to fill the data:

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;
    }
}

This is traditional C, where I loop from one address to the next, until I reach the end of the buffer. I tell the code what the size of the buffer is. I manually update the pointer in the loop. I take care that the loop ends at the right data element. Works well, and has proven to be effective in millions of programs.

C++ style, with Iterators or a Span

The STL can perform a number of the steps for me. It has the powers to derive the size. Deals with incrementing the iterator. And can call code on each of the buffer elements. I use all of that here.

The core part is where the samples are read from the ADS1115 IC and stored in the buffer:

        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);
        });

I used a Lambda function here. An anonymous block of code that will be executed every time. In the square brackets, I pass it the variables it needs to perform the exercise.
Because it's a Lambda, it's not part of the class, but an isolated block of code. I have to tell it the object it's working on. That's why I capture this (the object) in the [] part.
The part between the () is the data value it gets out of the container from the for_each algorithm. I passed it by reference (& u), so that the Lambda can update it inside the container.
The code between the {} is the Lambda function.

What happens in real time:

  1. for_each will pass the first element to the Lambda
  2. Lambda:
    1. will wait for a sample to be ready,
    2. get the data via i2c,
    3. then writes the data to the data element
  3. if this was the last element, for_each stops and the buffer is filled.
  4. else, for_each gets the next element and we go back to step 1

Option 1: pass the begin and end iterators

The first implementation takes the start and (one past) end points of the data container. All elements within that range will get a sample.

In your code:

uint16_t buf[SAMPLES];
ads.bulk_read(std::begin(buf), std::end(buf)); // option 1: with iterators

I used a traditional C array of uint_16 here. You can try this out with the STL containers.

The ads1115 class method:

    // 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);
        });
    }

This one is the easiest if you want to fill a part of the buffer. If you use a round-robin pattern, you can use it to pass the first half of the buffer in one call, then the 2nd half in the next call, etc.... The method will nicely stay within those boundaries. This will allow you to pass the half data set to the other Pico core for processing, and fill the other half in parallel with fresh data.

Option 2: just pass the buffer (uses std::span)

This 2nd option allows you to just pass the data container. As long as it is sequentially addressable and updatable, it will work. The container can be a plain C array, a STL Vector, Array, (Forward) List, Double-ended Queue, or a Span.

Daily bit(e) of C++ | std::span by ŠIMON TÓTH:

C++20 introduced std::span, a view that can wrap a contiguous memory block and provides a contiguous range interface. We can use a std::span to bridge the C and C++ code and to access the underlying byte representation.

The method bulk_read(std::span<uint16_t> buf) will accept any of those, derive the begin and end iterator, and pass it to the option 1 function.

In real life:

uint16_t buf[SAMPLES];
ads.bulk_read(buf); // option 2: with span view

The ads1115 class method:

    // 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());
    }

This one is easier if you just want to fill the full buffer. The class will look up the start and end. Can also be used to pass a subset of the buffer (by passing it you own span view of  a range of the buffer container). But in that case option 1 is usually simpler.

Enjoy!

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

I created a mock program, that tests the constructs on any platform that has a C++20 capable toolchain. Can be used to validate what containers are supported, and if the choices change size or overhead.

#include <algorithm>
#include <span>
#include <iterator>
#include <numeric>

#include <stdint.h>
#include <stdio.h>

#define SAMPLES (10)

 #define BT_C_ARRAY
// #define BT_STD_VECTOR
// #define BT_STD_ARRAY
// #define BT_STD_LIST

#ifdef BT_C_ARRAY
// no include
uint16_t buf[SAMPLES];
#endif

#ifdef BT_STD_VECTOR
#include <vector>
std::vector<uint16_t> buf = std::vector<uint16_t>(SAMPLES);
#endif

#ifdef BT_STD_ARRAY
#include <array>
std::array<uint16_t, SAMPLES> buf = std::array<uint16_t, SAMPLES>();
#endif

#ifdef BT_STD_LIST
#include <list>
std::list<uint16_t> buf = std::list<uint16_t>(SAMPLES);
#endif


class sensor {
public:
	// constructor
	sensor() : voltconversionfactor(1.1) {} // mock conversion factor

    // mock get bulk data by passing iterators to buffer
    template<typename Iterator>
    void fill(Iterator begin,  Iterator end) {
    	// capture "this" between the [], allows to call this object's methods
    	// not used in this example, but used in real code to check data readiness status
        std::for_each(begin, end, [this](uint16_t& u){
        	u = 0b0000000100000000; // in reality, a sensor would get i2c/spi, ... data here
        	// I used 256, because after byte swapping later, it 'll become 1
        	// obviously, just mocking sensor data here
        });
    }

    // get bulk data by passing a container (can be a C array)
    void fill(std::span<uint16_t> buf) {
        fill(buf.begin(), buf.end());
    }

    // mock convert the raw value to volts present at the ADC input
    // obviously, just mocking conversion here
    inline double to_volts(uint16_t raw) {
        return (double)(raw * voltconversionfactor);
    }

private:
	double voltconversionfactor; // mock conversion factor
};


sensor s = sensor();

int main() {
	setbuf(stdout, NULL); // print output while debugging

	// select the option to get the data: via iterators or by passing the buffer
	// uncomment the one you want to use, comment the other
    // s.fill(std::begin(buf), std::end(buf));  // option 1: with iterators
    s.fill(buf);                             // option 2: with span view

    // 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", s.to_volts(average));
    // min and max
    auto minmax = std::minmax_element(std::begin(buf), std::end(buf));
    printf("MIN = %.7f V\n", s.to_volts(*minmax.first));
    printf("MAX = %.7f V\n", s.to_volts(*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 s.to_volts(u); });

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

    return 0;
}

https://gist.github.com/jancumps/36a1d41e2ff566f07332c7db5757a6b6

Spoiler:

This fill method (option 1) supports all sequence containers:
s.fill(std::begin(buf), std::end(buf)); // option 1: with iterators

The second fill method (option 2) works with classic C array, std::vector and std::array.
s.fill(buf); // option 2: with span view

  • Sign in to reply

Top Comments

  • shabaz
    shabaz over 1 year ago +1
    I followed that substack link and discovered a new tool I'd not used before; Compiler Explorer. I enjoyed experimenting a little with the std::span example.
  • DAB
    DAB over 1 year ago

    Nice update Jan.

    • 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

    found why the std::list doesn't work with the option 2:

    A std::span can be created from [a plain C-array, a pointer with a size, a std::array, a std::vector, or a std::string]

    • 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

    to test it with a vector and other containers, add this under the includes:

    // #define BT_C_ARRAY
    #define BT_STD_VECTOR
    // #define BT_STD_ARRAY
    // #define BT_STD_LIST
    
    #ifdef BT_C_ARRAY
    // no include
    uint16_t buf[SAMPLES];
    #endif
    
    #ifdef BT_STD_VECTOR
    #include <vector>
    std::vector<uint16_t> buf = std::vector<uint16_t>(SAMPLES);
    #endif
    
    #ifdef BT_STD_ARRAY
    #include <array>
    std::array<uint16_t, SAMPLES> buf = std::array<uint16_t, SAMPLES>();
    #endif
    
    #ifdef BT_STD_LIST
    #include <list>
    std::list<uint16_t> buf = std::list<uint16_t>(SAMPLES);
    #endif
    

    ... and remove the buf definition just before the main() function.

    You can then switch between buffer types by commenting / uncommenting the desired #define

    The std::span call (option 2) doesn't work with a std::list. I have to figure out why ...
    The list woks with the iterators call (option 1) 

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

     shabaz , here is a mock example of the ads1115 OO code. It would run in the explorer above, or any c++ compiler that runs C++20.

    #include <algorithm>
    #include <span>
    #include <iterator>
    #include <numeric>
    
    #include <stdint.h>
    #include <stdio.h>
    
    
    #define SAMPLES (10)
    
    class sensor {
    public:
    	// constructor
    	sensor() : voltconversionfactor(1.1) {} // mock conversion factor
    
        // mock get bulk data by passing iterators to buffer
        template<typename Iterator>
        void fill(Iterator begin,  Iterator end) {
        	// capture "this" between the [], allows to call this object's methods
        	// not used in this example, but used in real code to check data readiness status
            std::for_each(begin, end, [this](uint16_t& u){
            	u = 0b0000000100000000; // in reality, a sensor would get i2c/spi, ... data here
            	// I used 256, because after byte swapping later, it 'll become 1
            	// obviously, just mocking sensor data here
            });
        }
    
        // get bulk data by passing a container (can be a C array)
        void fill(std::span<uint16_t> buf) {
            fill(buf.begin(), buf.end());
        }
    
        // mock convert the raw value to volts present at the ADC input
        // obviously, just mocking conversion here
        inline double to_volts(uint16_t raw) {
            return (double)(raw * voltconversionfactor);
        }
    
    private:
    	double voltconversionfactor; // mock conversion factor
    };
    
    
    // change to any sequence container of uint16_t
    uint16_t buf[SAMPLES];
    sensor s = sensor();
    
    int main() {
    	setbuf(stdout, NULL); // print output while debugging
    
    	// select the option to get the data: via iterators or by passing the buffer
    	// uncomment the one you want to use, comment the other
        // s.fill(std::begin(buf), std::end(buf));  // option 1: with iterators
        s.fill(buf);                             // option 2: with span view
    
        // 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", s.to_volts(average));
        // min and max
        auto minmax = std::minmax_element(std::begin(buf), std::end(buf));
        printf("MIN = %.7f V\n", s.to_volts(*minmax.first));
        printf("MAX = %.7f V\n", s.to_volts(*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 s.to_volts(u); });
    
        // print voltage
        std::for_each(std::begin(volts), std::end(volts), [](double& d) {
            printf("SMP = %.7f V\n", d);
        });
    
        return 0;
    }
    

    I reduced the code to just the interaction with the containers.

    This will allow to test if it also works with a std::vector, std::array, ... with element type uint16_t, by redefining the buf definition.

    Violations should flag at compile time. Your code should not build if the buffer passed is not a sequence of uint16_t elements 

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

    I followed that substack link and discovered a new tool I'd not used before; Compiler Explorer. I enjoyed experimenting a little with the std::span example.

    image

    • Cancel
    • Vote Up +1 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