ADC library, with support for Teensy 4, 3.x, and LC

I actually get this error whenever I load any sketch that tries to include the library (all of the examples included). If I open a blank sketch and type this one line:

#include <ADC.h>


and compile, the same error pops up...
 
hmm, ADC0_PGA and the rest ADC0_PGA_x should be defined in mk20dx128.h, do you have the latest Teensyduino version? have you modified the file and not updated it?
The correct definitions are:
Code:
#define ADC0_PGA		*(volatile uint32_t *)0x4003B050 // ADC Programmable Gain Amplifier
#define ADC0_PGA_PGAEN			(uint32_t)0x00800000		// Enable
#define ADC0_PGA_PGALPB			(uint32_t)0x00100000		// Low-Power Mode Control, 0=low power, 1=normal
#define ADC0_PGA_PGAG(n)		(uint32_t)(((n) & 15) << 16)	// Gain, 0=1X, 1=2X, 2=4X, 3=8X, 4=16X, 5=32X, 6=64X
 
Great addition! The examples are very clear; I'm sure even I can make it work! One question: what is the function of this last line in your examples?

GPIOC_PTOR = 1<<5;
 
That instruction toggles the on-board led, I should probably use the better (more portable and as fast): digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN) );
 
Thanks! I just learned three new things from your reply. I agree, that syntax would make the function clearer.
 
Just noticed with the the new teensyduino 1.19 RC_2, you have to change the adc_module.cpp to reflect the new definitions. So ADC0_PGA has to be changed to ADC_PGA. I just did a search and replace using notepad++ and my sketch now compiles.
On a side note, has anybody got the timer examples to work? I have had no luck with it yet with the quick testing I did.
 
Hi,

I'm working on a big update, but it will take time, as I moved again for work reasons (and I'll move again in a month).
About the timers:
The code uses IntervalTimer to set up periodic calls to singleRead(...) and stores the result in a circular buffer. It all comes at the cost of many lines of code and complexity (interrupts) so that the user doesn't have to see all that. I'm thinking of actually deleting all that and let the user do it. I have some examples ready. What do you think? I can post the example code so you can see how it works.
I'm playing with the PDB and DMA to substitute the timers, but it's still preliminary.
 
In fact it's been a great help!
I have a working DMA circular buffer with a different design than yours (it uses the modulo feature), but your code was the starting point. The PDB code I'm working is very similar to yours too.
The main problem is that PDB only allows two timers I think, one using the trigger and the second using the pretrigger, while IntervalTimer allows 3 timers.

What I want is that the user can have many periodic measurements at different frequencies (for a sensors of different priorities for example).
Right now the user can start a continuous measurement and use analogRead(...) to get a value in an other pin without having to worry about restarting the continuous measurement (plus the new code will use atomic instructions, so it's even more interrupt safe than now). The idea is that the user can have this continuous conversion and periodic ones of different frequencies at the same time with minimum effort, both single-ended and differential and using both ADC. Of course this means that the library is huge now, but it's also scalable (I think), so when you release the Teensy 3.5 with 10 ADCs it will work ;-)
 
I would love to see some examples. I have already been using my own timer, but not sure if doing it the best way. I only need a collection rate of 200sps, however the signal is pretty noisy, so want to average as many samples as possible. I am using differential continuous, filling a buffer and every 5000us I average all those values. Seems to be ok, but have not had a chance to test with a function generator yet. I will post my code below, mostly taken from your examples (take it easy on me, all self taught).

Code:
#include "ADC.h"
#include <SdFat.h>
#include <SdFatUtil.h>
#include <Bounce.h>
#include <Time.h> 

//sdFat Setup
SdFat sd;
SdFile file;
#define SD_CHIP_SELECT  10  // SD chip select pin
#define error(s) sd.errorHalt_P(PSTR(s))
#define FILE_BASE_NAME "LOG" //Can be max size of 6 character
#define TMP_FILE_NAME "TMP_LOG.BIN"
const uint8_t BASE_NAME_SIZE = sizeof(FILE_BASE_NAME) - 1;
const uint32_t FILE_BLOCK_COUNT = 10000UL; //500=250kb
const uint32_t ERASE_SIZE = 262144L;
uint32_t bgnBlock, endBlock;
uint8_t*  pCache;
char binName[13] = FILE_BASE_NAME "00.BIN";

//Pins
#define LED 6  //recording signal
#define RECORDBUTTON 7
#define FULL 8  //used to signal a full file and turn off recording
const int battery = A3; // ADC1
const int ledPin = 13;

//Interval Timer
IntervalTimer timer0;

// Variables
const int BUFF = 500;
float average = 0.000000;
int i=0;
float sum = 0.000000;
float offset = 0.000000;
int buffer [BUFF];
float SDBuffer [128];
int count = 0;
uint32_t bn = 1; //block number
int batteryVoltage = 0;

//boolean setup
boolean recordValue = false;
int ledValue = LOW;

//Button Setup
Bounce bouncer = Bounce( RECORDBUTTON,5 ); //5ms delay

//ADC object
int value = ADC_ERROR_VALUE;
ADC *adc = new ADC(); 

//Flash LED error setup
#define LONG_INTERVAL 500   // number of mills to wait, for a longish time
#define SHORT_INTERVAL 100  // number of mills to wait, short time.  

//------------------------------------------------------------------------------
/*
 * User provided date time callback function.
 * See SdFile::dateTimeCallback() for usage.
 */
void dateTime(uint16_t* date, uint16_t* time) {
  // User gets date and time from GPS or real-time
  // clock in real callback function
  time_t now();
  // return date using FAT_DATE macro to format fields
  *date = FAT_DATE(year(), month(), day());

  // return time using FAT_TIME macro to format fields
  *time = FAT_TIME(hour(), minute(), second());
}

time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}


void createFile(){

  bn=1; //reset block count
  count=0;
  // Find unused file name.
  if (BASE_NAME_SIZE > 6) {
    sendError(3,1);
  }
  while (sd.exists(binName)) {
    if (binName[BASE_NAME_SIZE + 1] != '9') {
      binName[BASE_NAME_SIZE + 1]++;
    } 
    else {
      binName[BASE_NAME_SIZE + 1] = '0';
      if (binName[BASE_NAME_SIZE] == '9') sendError(3,1);
      binName[BASE_NAME_SIZE]++;
    }
  }
  // delete old log file
  if (sd.exists(TMP_FILE_NAME)) {
    // Serial.println(F("Deleting tmp file"));
    if (!sd.remove(TMP_FILE_NAME)) sendError(3,1);
  }
  // create new file
  file.close();
  if (!file.createContiguous(sd.vwd(),
  TMP_FILE_NAME, 512 * FILE_BLOCK_COUNT)) sendError(3,1);
  // get address of file on SD
  if (!file.contiguousRange(&bgnBlock, &endBlock)) sendError(3,1);
  // use SdFat's internal buffer
  pCache = (uint8_t*)sd.vol()->cacheClear();
  if (pCache == 0) sendError(3,1); 
  memset(pCache, '0', 512); 
  // flash erase all data in file
  uint32_t bgnErase = bgnBlock;
  uint32_t endErase;
  while (bgnErase < endBlock) {
    endErase = bgnErase + ERASE_SIZE;
    if (endErase > endBlock) endErase = endBlock;
    if (!sd.card()->erase(bgnErase, endErase)) sendError(3,1);
    bgnErase = endErase + 1;
  }
}

void flashLED(byte count, int interval) {
    // Just what it says: it flashes the LED 'count' times for 
    // the given on/off interval each time.
    byte j;
    for (j=0; j<count; j++) {
        digitalWrite(LED, HIGH);
        delay(interval);
        digitalWrite(LED, LOW);
        delay(interval);
    }
}
void sendError(byte errorGroup, byte errorItem) {
    // Flashes the LED count times, then pauses, then repeats. 
    // Used to tell the user what the problem is.
    while (true) {
        flashLED(errorGroup, SHORT_INTERVAL);
        delay(LONG_INTERVAL/2);
        flashLED(errorItem, SHORT_INTERVAL);
        delay(LONG_INTERVAL);
    }
}

void checkBattery(){
  while (i<1000){
    sum += adc->analogRead(battery, ADC_1);
    i++;
    delay(1);
  }
  batteryVoltage=sum/1000;
  if (batteryVoltage < 1500) //1497=3.7V
  {
    sendError(3,1);
  }
  sum=0;
  i=0;
} 

void timerCallback0(){

  for (int b=0; b<i; b++)
  {
    sum += buffer [b]; //Sum samples
  }  
  average = sum/i;
  //Serial.println(i);
  i=0;
  sum=0;


  if (count < 128){ //since using float values, 128x4=512bytes
    SDBuffer[count++] = average;
  }
  if (count==128){
    noInterrupts();
    memcpy(pCache, &SDBuffer, 512);
    if (!sd.card()->writeData((uint8_t*)pCache)) ;
    bn++;
    count = 0; 
    memset(pCache, '0', 512); 
    interrupts();
  }
   Serial.print('V');
   Serial.println(average,6);
  //stop recording if file is full
  if (bn == FILE_BLOCK_COUNT) digitalWrite(FULL,HIGH); //pulse digital pin to mimic buttonpush

}

void setup() {
  Serial.begin(115200);
  setSyncProvider(getTeensy3Time);
  SdFile::dateTimeCallback(dateTime);
  
  pinMode(A10, INPUT); //Diff Channel 0 Positive
  pinMode(A11, INPUT); //Diff Channel 0 Negative
  pinMode(battery, INPUT); //single channel to read battery voltage
  pinMode(RECORDBUTTON, INPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(LED, OUTPUT);
  pinMode(FULL, OUTPUT);
  digitalWrite(FULL, LOW);

  // initialize the SD card at SPI_HALF_SPEED to avoid bus errors with
  if (!sd.begin(SD_CHIP_SELECT, SPI_FULL_SPEED)) {
    sd.initErrorHalt();
  }


  //load cell voltage is ~2.3V...so maybe can't use internal
  //maybe not affected using differential?
  // adc->setReference(ADC_REF_INTERNAL, ADC_0); //change all 3.3 to 1.2 if you change the reference
  //adc->setReference(ADC_REF_EXTERNAL, ADC_0); //change all 3.3 to 1.2 if you change the reference

  adc->enablePGA(6, ADC_0);
  adc->setAveraging(32, ADC_0); // set number of averages
  adc->setResolution(13, ADC_0); // set bits of resolution
  //Battery voltage reading on A3
  adc->setAveraging(32, ADC_1); // set number of averages
  adc->setResolution(12, ADC_1); // set bits of resolution

  // always call the compare functions after changing the resolution!
  //adc->enableCompare(1.0/3.3*adc->getMaxValue(ADC_1), 0, ADC_1); // measurement will be ready if value < 1.0V
  //adc->enableCompareRange(1.0*adc->getMaxValue(ADC_1)/3.3, 2.0*adc->getMaxValue(ADC_1)/3.3, 0, 1, ADC_1); // ready if value lies out of [1.0,2.0] V

  delay(500);

  while (i<1000){
    sum += adc->analogRead(battery, ADC_1);
    i++;
    delay(1);
  }
  batteryVoltage=sum/1000;
  delay(2000);
  Serial.println(batteryVoltage);
  if (batteryVoltage < 1500) //1497=3.7V
  {
    sendError(3,1);
  }
  sum=0; //reset to calulate offset
  i=0;

  createFile(); //create first file, since after this only make file after collection stops

 //adc->enableInterrupts(ADC_0); // enable interrupts BEFORE calling a reading function!

  adc->startContinuousDifferential(A10, A11, ADC_0);
  digitalWrite(LED,HIGH); //indicate calibrating of loadcell

  //Calibration/Offset Routine
  while (i<2000){
    sum += adc->analogReadContinuous(ADC_0);
    i++;
    delay(1);
  }
  digitalWrite(LED,LOW);
  offset = sum/2000;
  delay(1000);
  Serial.println(offset,6);
  i=0;
  sum=0;
 
}




void loop() {

  if ( bouncer.update() ) {
    if ( bouncer.read() == HIGH) {
      if ( ledValue == LOW ) {          
        if (!sd.card()->writeStart(bgnBlock, FILE_BLOCK_COUNT)) sendError(3,1);
        recordValue = true;
        ledValue = HIGH;
        timer0.begin(timerCallback0, 5000); //start timer 5000=200sps
      }
      else{
        timer0.end(); //Turn off timer to stop SD card write
        recordValue = false;
        digitalWrite(FULL,LOW);
        if (!sd.card()->writeStop())sendError(3,1);
        //file.close();
        ledValue = LOW;
        // Truncate file if recording stopped early.
        if (bn != FILE_BLOCK_COUNT) {    
            if (!file.truncate(512L * bn)) sendError(3,1);
        }
        if (!file.rename(sd.vwd(), binName)) sendError(3,1);
        
        //checkBattery(); //dbl check to make sure working
        createFile();
      }

      digitalWrite(LED,ledValue); //to indicate file recording
    }
  }  
 
  if (i<BUFF & recordValue == true){
    buffer[i++] = adc->analogReadContinuous(ADC_0) - offset;
  }

  delayMicroseconds(100);

}

void adc0_isr(void) {//raised on each ADC conversion, need to be enabled?
  // Low-level code
  ADC0_RA;
 //GPIOC_PTOR = 1<<5; //toggles LED on/off

  // High-level code
  //adc->analogReadContinuous(ADC_0); // read to clear the COCO flag
  //digitalWrite(ledPin, !digitalRead(ledPin)); // toggle led

}
#if defined(__MK20DX256__)
void adc1_isr(void) {
  // Low-level code
  ADC1_RA;
  GPIOC_PTOR = 1<<5;

  // High-level code
  //adc->analogReadContinuous(ADC_1); // read to clear the COCO flag
  //digitalWrite(ledPin, !digitalRead(ledPin)); // toggle led

}
#endif
 
This is an example. It sets up two timers and reads values at different frequencies.: View attachment analogReadIntervalTimer.ino
If one timer starts a measurement and the second starts an other one before the first finishes, then the 1st will be canceled and started again when the 2nd one is done.
This works because startSingleRead checks whether the ADC was already working and stores all the config registers, then you can restore them back in the adc_isr.

Again, I'm still not sure whether I will delete the analogTimer support, and the user will have to do something like in the example, or I'll keep the current functionality.

PS: the examples has some debug code, you can check pins 13 and 14 with an oscilloscope to see the isr functions.
 
Thanks for that example, however couple of questions. I am confused about when you say //read the values in the buffer, the values are placed every 10 ms, but we read them every 50 ms. But at the bottom of the loop the delay is 10000us, which is 100ms, so how is it 50ms. Also, the timer is running at 100us, which is .1ms. I am also wondering, if we are placing all these values in the buffer, can we read all the values, or does it just read the last value placed in the buffer. If this is the case, then why sample so quickly if you ignore the other samples. As posted in a few posts above, I am doing an average of so many samples, and then reading that every 5ms. I am not in the lab until tomorrow, so once there I will try your code and prob it with the oscilloscope. Thanks again for your examples and if I wanted to read differential pins, I assume I would just do single differential reads.
 
Code:
// read the values in the buffer, the values are placed every 10 ms, but we read them every 50 ms
That comment is wrong! We read with readPeriod and convert with periodX, which have the values you said.

The buffer we use is a circular (or ring) buffer. We add values and always read the latest value (LIFO), when the buffer is full this means that we read the latest measurement. It's useful because you can decouple the reading and writing timings. If you start reading faster than periodX then you will empty it.

For differential measurements you use the corresponding differential function. This is why I'm thinking about having the user work like in the example, one you understand how it works you can modify it for you exact needs.
 
Finally got a chance to test your program and I get this error
analogReadIntervalTimer.ino: In function 'void adc0_isr()':
analogReadIntervalTimer:137: error: 'sc1a2channelADC0' was not declared in this scope
I am using 1.19, maybe there is a change I need to fix.

Update- Got it working, I just called the variable at the top since I think it is only called in the callback function, which is now not called. The other issue was I had to comment out the include intervaltimer.h. At first it did not work since it was not figuring out the right pin, so I just debugged it by printing out the pin value. The first time it was 64 when using both pin A8 and A9. I then got rid of pin A8 and then the pin value was 1. It worked pretty well, but was getting some spikes in the data (was reading in a 1kz sine wave).
 
Last edited:
An other derp on my part!
You can call it using ADC_Module::channel2sc1aADC0[...].

I have a version on github and a different version on my pc, so sometimes it's confusing.
 
Great work!

I was trying to use both ADCs for some time now, but the mem addresses in the manual did never worked for me...
It is really helpfull for one of my projects!
 
I was wondering, what is the difference between continuous read and single read. Does it make more sense to do a single read every 100us and average those samples to get a smoother sample, or should I use continuous read like I do in the code I posted in a previous post above. My method is sort of working, however there is an issue where when the SD card writes, the number of samples that are averaged is 2x as many, even though the SD write only takes 200us. Pretty sure I am not really doing it the exact correct method, but also not sure how I would utilize your ring buffer method. Anyways, thanks again for your library and support.
 
The good thing about continuous read is that it's converting all the time and you read the values whenever you want. This is very fast.
The problem is that it uses more power (it's always working after all!).

Using a ring buffer you can store the readings and use them later, all of them. With continuous read you only have the last one.
 
I will have to try the single read method and see the difference in power use. I will be using batteries for my project and currently it is using between 45-55mA and then spikes a bit when starting and stopping recording to the SD card. I was also thinking I could turn on conversion when I want to record and then turn it off when done recording. Do you see any issue with that?
 
I'm really interested in having the absolute maximum sample speed I can get for my MicLoc project
http://ruralhacker.blogspot.pt/p/micloc.html

Right now, I only need 8bit precision but I need to sample 4 channels, so I coded a highspeed 8bit reading macro and init code:

Code:
Code:
//FAST 8 BIT SETUP AND READING FOR 2 ADCs
//THIS IS TEENSY VERSION 3.1 SPECIFIC

#ifndef _teensy31adc_h_
#define _teensy31adc_h_

#include <ADC.h>


//defined as a macro its faster
#define highSpeed8bitAnalogReadMacro(channel1, channel2, value1, value2) ADC0_SC1A = channel1;ADC1_SC1A = channel2;while (!(ADC0_SC1A & ADC1_SC1A & ADC_SC1_COCO)) {} value1 = ADC0_RA;value2 = ADC1_RA;

/*
FUNCTION PSEUDOCODE FOR MACRO, of course we could not pass value1 and value2 like this to a function (should be pointers or return a struct)
int highSpeed8bitAnalogRead(uint8_t channel1, uint8_t channel2, int value1, int value2){
ADC0_SC1A = channel1;
ADC1_SC1A = channel2;
while (!(ADC0_SC1A & ADC1_SC1A & ADC_SC1_COCO)) {}
value1 = ADC0_RA;
value2 = ADC1_RA;
}
*/


void highSpeed8bitADCSetup(){
  
  /*
0 ADLPC (Low-Power Configuration)
0 ADIV (Clock Divide Select)
0
0 ADLSMP (Sample time configuration)
0 MODE (Conversion mode selection) (00=8/9, 01=12/13, 10=10/11, 11=16/16 bit; diff=0/1)
0
0 ADICLK (Input Clock Select)
0
*/
  ADC0_CFG1 = 0b00000000;
  ADC1_CFG1 = 0b00000000;

   /*
0 MUXSEL (ADC Mux Select)
0 ADACKEN (Asynchrononous Clock Output Enable)
0 ADHSC (High-Speed Configuration)
0 ADLSTS (Long Sample Time Select) (00=+20 cycles, 01=+12, 10=+6, 11=+2)
0
*/
  ADC0_CFG2 = 0b00010100;
  ADC1_CFG2 = 0b00010100;
  
  /*
0 ADTRG (Conversion Trigger Select)
0 ACFE (Compare Function Enable)
0 ACFGT (Compare Function Greater than Enable)
0 ACREN (Compare Function Range Enable)
0 ACREN (COmpare Function Range Enable)
0 DMAEN (DMA Enable)
0 REFSEL (Voltage Reference Selection) (00=default,01=alternate,10=reserved,11=reserved)
*/
  ADC0_SC2 = 0b00000000;
  ADC1_SC2 = 0b00000000;
 
  /*
1 CAL (Calibration)
0 CALF (read only)
0 (Reserved)
0
0 ADCO (Continuous Conversion Enable)
1 AVGS (Hardware Average Enable)
1 AVGS (Hardware Average Select) (00=4 times, 01=8, 10=16, 11=32)
1
*/
  
  ADC0_SC3 = 0b10000000;
  ADC1_SC3 = 0b10000000;

  
  // Waiting for calibration to finish. The documentation is confused as to what flag to be waiting for (SC3[CAL] on page 663 and SC1n[COCO] on page 687+688).
  while (ADC0_SC3 & ADC_SC3_CAL) {} ;
  while (ADC1_SC3 & ADC_SC3_CAL) {} ;

}

#endif

Which I then use directly:

Code:
const int channelA2 = ADC_Module::channel2sc1aADC0[2];
const int channelA3 = ADC_Module::channel2sc1aADC1[3];
const int channelA11 = ADC_Module::channel2sc1aADC0[11];
const int channelA10 = ADC_Module::channel2sc1aADC1[10];
...

void loop() {
  startTime = micros();
     //START SAMPLING
     //Strange init in this for, but the compiler seems to optimize this code better, so we get faster sampling
  for(i=0,k=0,samples=SAMPLES,event=0;i<samples;i++) {
    [B]//TAKE THE READINGS
    highSpeed8bitAnalogReadMacro(channelA2,channelA3,value1,value2);
    //SHOULD ADJUST THIS 2nd READING
    highSpeed8bitAnalogReadMacro(channelA11, channelA10,value3,value4);[/B]
   
 ...
   
  }
  stopTime = micros();

Essentialy I stripped down anything that could slow down the ADC readings.
So I'm sampling as fast as I can in 4 channels, 2 at a time. With the code overhead I need I can get around 454k samples/sec, which is really great.
Do you think it's possible to get even more? Is there any caveat I'm not aware of sampling like this using the macro as is?
Also, I can't understand why if I swap the channelsA11 and channelA10 the code no longer works. I have no problem with A2 and A3.
Maybe its an unsupported combination?

Thanks,
kripthor
 
I think you're doing everything fine.

The problem with the channels A10-A13 is that not all of them can be used in single-ended mode (see the picture of Teensy 3.1 in my first post). A11 can only be accessed by ADC0 in single-ended mode, A13 only by ADC1, while A10 and A12 can be used by both ADC in single-ended mode.

I strongly recommend you that instead of writing "ADC0_SC3 = 0b10000000;" you write "ADC0_SC3 = ADC_SC3_CAL;", it's way more readable. Think that you'll go on to work on the next part of your project and when you go back at this you won't know what "0b10000000" means!
 
Back
Top