"Complete" implementation of Teensy 3.0 ADC as a library

Status
Not open for further replies.

Pedvide

Senior Member+
Hello everybody,

IMPORTANT
SEE NEW POST WITH UPDATED INFORMATION HERE:
https://forum.pjrc.com/threads/25532-ADC-library-update-now-with-support-for-Teensy-3-1
SOME INFORMATION IN THIS THREAD IS OUTDATED, PLEASE REFER TO THE NEW POST ABOVE.

I've implemented most, if not all, functions of the Teensy 3.0 ADC.
Most of the code is not interrupt-proof, mostly because I'm very new to this microcontroller world and I don't know how things work.
Also, it is possible to further optimize the speed and power consumption of the ADC manually, maybe in the future it will be possible to select more speed/less power/more accuracy.

The library includes examples for all functions and at the end of every example you can find the speed of conversions for all different resolutions.
The file index.html in the doxygen/html folder has the documentation for the functions in the library.

What do you think of it?

View attachment ADC.zip
 
Last edited:
I've created a GitHub repository of the library: https://github.com/pedvide/ADC

This is a list with the methods of the library:

int ADC::analogRead ( uint8_t pin )
Returns the analog value of the pin.
It waits until the value is read and then returns the result. If a comparison has been set up and fails, it will return ADC_ERROR_VALUE.

int ADC::analogReadContinuous ( )
Reads the analog value of a continuous conversion.
Set the continuous conversion with with analogStartContinuous(pin) or startContinuousDifferential(pinP, pinN)
Returns
the last converted value.

int ADC::analogReadDifferential ( uint8_t pinP,
uint8_t pinN )

Reads the differential analog value of two pins (pinP - pinN).
It waits until the value is read and then returns the result. If a comparison has been set up and fails, it will return ADC_ERROR_DIFF_VALUE.

void ADC::disableCompare ( )
Disable the compare function.

void ADC::disableDMA ( )
Disable ADC DMA request.

void ADC::disableInterrupts ( )
Disable interrupts.

void ADC::enableCompare ( int16_t compValue,
int greaterThan )

Enable the compare function to a single value.
A conversion will be completed only when the ADC value is >= compValue (greaterThan=1) or < compValue (greaterThan=0) Call it after changing the resolution Use with interrupts or poll conversion completion with isADC_Complete()

void ADC::enableCompareRange ( int16_t lowerLimit,
int16_t upperLimit,
int insideRange,
int inclusive )

Enable the compare function to a range.
A conversion will be completed only when the ADC value is inside (insideRange=1) or outside (=0) the range given by (lowerLimit, upperLimit),including (inclusive=1) the limits or not (inclusive=0). See Table 31-78, p. 617 of the freescale manual. Call it after changing the resolution Use with interrupts or poll conversion completion with isComplete()

void ADC::enableDMA ( )
Enable DMA request.
An ADC DMA request will be raised when the conversion is completed (including hardware averages and if the comparison (if any) is true).

void ADC::enableInterrupts ( )
Enable interrupts.
An IRQ_ADC0 Interrupt will be raised when the conversion is completed (including hardware averages and if the comparison (if any) is true).

double ADC::getMaxValue ( )
Returns the maximum value for a measurement, that is: 2^resolution.

int ADC::getResolution ( )
Returns the resolution of the ADC.

int ADC::isComplete ( )
Is an ADC conversion ready?
Returns
1 if yes, 0 if not. When a value is read this function returns 0 until a new value exists So it only makes sense to call it before analogRead(), analogReadContinuous() or analogReadDifferential()

int ADC::isConverting ( )
Is the ADC converting at the moment?
Returns
1 if yes, 0 if not

void ADC::setAveraging ( unsigned int num )
Set the number of averages.
Parameters
num can be 0, 4, 8, 16 or 32.

void ADC::setReference ( uint8_t type )
Set the voltage reference you prefer, default is vcc.
Parameters
type can be DEFAULT, EXTERNAL or INTERNAL

void ADC::setResolution ( unsigned int bits )
Change the resolution of the measurement.
Parameters
bits is the number of bits of resolution. For single-ended measurements: 8, 10, 12 or 16 bits. For differential measurements: 9, 11, 13 or 16 bits. If you want something in between (11 bits single-ended for example) select the inmediate higher and shift the result one to the right.

void ADC::startContinuous ( uint8_t pin )
Starts continuous conversion on the pin.
It returns as soon as the ADC is set, use analogReadContinuous() to read the value.

void ADC::startContinuousDifferential ( uint8_t pinP,
uint8_t pinN )

Starts continuous conversion between the pins (pinP-pinN).
It returns as soon as the ADC is set, use analogReadContinuous() to read the value.

void ADC::stopContinuous ( )
Stops continuous conversion.


Now I'm working on using IntervalTimer to program timed measurements and I'm also looking into the whole interrupt safe thing, so that if you make a measurement and an interrupt starts an other one, when the interrupt finishes the original reading is continued.
 
Last edited:
Hey, that's awesome. For differential readings on the two built-in differential channels (A10-A11 and A12-A13) does your library require two sequential reads or can it take advantage of the single-read differential built into the Teensy 3 ADC?
 
The differential reading is only the built-in mode in the Teensy 3.0 ADC. You need to use it with pinP=A10(A12) and pinN=A11(A13), the other way around or if you try with other pins it will return a ADC_ERROR_DIFF_VALUE. Sorry I should have documented it better! Is there any other thing that isn't clear?
 
No, that looks pretty fantastic and might inspire Paul, who has been working on a ADC implementation of his own. One thing he's thinking of offering (and which I think would be really cool) is the ability to input the sequence of Analog inputs that the ADC is supposed to read (i.e. pin A1, pin A10, pin A7, etc.) to allow sequential reads to happen automatically. However, the devil is in the details, i.e. how and where the data coming out of a system like that would be stored in the meantime. That is, you'd have to declare a concurrent array or somesuch and then flag data ready on each read or when a sequence completed.

Anyhow, as much as I like the teensy 3 ADC in theory, I ultimately swore off the thing and went with a MCP3911 instead. Cheaper to implement with a energy-monitoring application than interfacing with the Teensy 3 on account of the BOP. (i.e. external voltage reference, level-shifting outputs to be positive-only, etc.). Meanwhile, the MCP3911 works great, can use CT's without electronics, simple bipolar input, PGA, etc. Oh, and the voltage and current channels are read simultaneously and allow phase compensation.
 
Does this library have functions to do the calibration sequence? Or do it happen automatically when you change the reference?
 
Paul,

It happens automatically when you change the reference. See that in you code (analog.c) it also recalibrates when you change the resolution, but p. 619 of the manual says it's not necessary.
I'll upload soon a new version of the library that allows regular sampling using the IntervalTimer library. In this new version it's possible to have a continuous measurement and add up to three periodic measurements (on different pins! even differential!!). The continuous reading is interrupted and the library automatically restarts it. I think it's very cool.
 
As promised. I've included a bunch of methods in the ADC library, plus a small implementation of a ring (circular) buffer.
The code is in the guthub repository https://github.com/pedvide/ADC or you can download it here View attachment ADC_v2.0.zip.
There are three new examples. A very simple example of the RingBuffer library, and two relating to interrupting a continuous measurement with an analogRead or a periodic reading.

The functions analogRead and analogReadDifferential have been updated and now they will restore the adc to its previous state before being called. That means that you can set up a continuous measurement and still call this functions, the continuous readings will continue after analogRead* returns.

The new functions implemented are:

int startAnalogTimer( uint8_t pin, uint32_t period): Starts a periodic measurement on the pin. That is, every period microseconds a reading will take places and be stored in a ring buffer. Read the oldest value with getTimerValue(pin), check if it's the last value with isLastValue(pin). When the buffer is full, new data will overwrite the oldest values.
ANALOG_TIMER_ERROR will be returned if the timer could not be started. Stop other analog timer and retry. This function is interrupt safe. The ADC interrupt will restore the adc to its previous settings and restart the readings if it stopped a measurement. If you modify the adc_isr then this won't happen.

int startAnalogTimerDifferential(uint8_t pinP, uint8_t pinN, uint32_t period): Same as above but with differential readings. pinP must be A10 or A12, pinN must be A11 (if pinP=A10) or A13 (if pinP=A12). Use pinP as argument in getTimerValue, isLastValue, etc.

void stopAnalogTimer(uint8_t pin): Stops the timer, you won't be able to access the buffer any more. It disables the adc interrupt if there are no more analog timers.

int getTimerValue(uint8_t pin): Returns the oldest value of the ring buffer corresponding to the pin.

bool isTimerLastValue(uint8_t pin): Is the oldest value also the last one?


The following methods are used by the analog timers, but maybe they are useful so they are public:

int startSingleRead(uint8_t pin): Starts an analog measurement on the pin and enables interrupts. It returns inmediately, get value with readSingle(). If the pin is incorrect it returns ADC_ERROR_VALUE This function is interrupt safe. The ADC interrupt will restore the adc to its previous settings and restart the adc if it stopped a measurement. If you modify the adc_isr then this won't happen.

int startSingleDifferential(uint8_t pinP, uint8_t pinN): Same as above but with differential measurements.

int readSingle(): Reads the analog value of a single conversion. Set the conversion with with startSingleRead(pin) or startSingleDifferential(pinP, pinN).


The methods in the RingBuffer library are self explanatory. The implementation is from wikipedia. The buffer size is constant (equal 1024 now) and must be a power of 2.

As I mentioned in the last post, this functions allow to have a continuous measurement running with up to 3 periodic readings at the same time. And also a continuous reading with "random" calls to analogRead or analogReadDifferential.

As a final note, see that there are some bugs in the code. The 16 bit mode presents some problems because it actually is 15 bit + 1 sign bit for the differential mode, so the comparison values are actually twice what you would expect. Please check that everything works.
The analog timers also have problems at 16 bits, this time I don't even know why, but the values go from 0 to +1.65 and then jumps to -1.65 to 0.

Paul, now I realize that the answer I gave you was wrong. It's necessary in fact to recalibrate the ADC when you change the resolution because you also change the speed of the measurements. My library does this automatically, but it means that it takes some time to change the resolution (manual says, I think, up to 15000 cycles!)
 
Pedvide,
Your library appears to have a rich set of functions for ADC. Thanks for your effort. I'm gearing up to use ADC on Teensy 3.1 and I was cramming on the subject when I came across this post. I have a few questions:

Does your library work with Teensy 3.1?
What's the status of the library, i.e. is actively supported and bugs being fixed,etc?
I've been using analogRead() to sample ADC. Are there any conflicts with the PJRC provided operations (tweensyduino)?
Would there be any performance difference between different libraries?

Thanks.
 
Hello everybody,

I'm glad you like the library, I'm interested in maintaining it and expand it (using DMA with the ring buffer and also implementing both ADC in Teensy 3.1), so if you have more suggestions or you find any bugs, please tell me.
On the other hand I moved to a new country to get a new job, so at the moment I don't even have internet at home! This will change soon and I hope that next weekend I'll finally have time to play with the Teensy 3.1.
 
Hello Pedvide,
Thanks for your great work!
I have a suggestion for a function I was seeking, and seems very easy to implement in your library imho..

analogRead() now waits for the adc result, and you've lost typically 10uS of coding time per sample, it is to much for my application, I need more time.
I'd like to have a function like throwAnalogRead() that just starts the conversion,
And a function catchAnalogRead(), that waits for the adc to complete (if it wasnt allready finished), and returns the value.
(The user has to check the throw and catch pairs himself, you shouldnt check that in your library..)

I'm building a 2 samples latency audio module that runs synchronous: the codeblock is limited to the time between 2 samples. At every start of this code block, I want to start the conversion, do all computing stuff, at the end of the code I will read it out, to use at the next start. That way the audio sampling is technically a hardware thread that doesn't cost time whatsoever..
Since I'm doing this on the Teensy3.1, I'll actually have audio output with 2 samples latency, via A14..
 
Hello,

This is already implemented!!
int startSingleRead(uint8_t pin): Starts an analog measurement on the pin. It returns inmediately, get value with readSingle(). If the pin is incorrect it returns ADC_ERROR_VALUE This function is interrupt safe. The ADC interrupt will restore the adc to its previous settings and restart the adc if it stopped a measurement. If you modify the adc_isr then this won't happen.

int startSingleDifferential(uint8_t pinP, uint8_t pinN): Same as above but with differential measurements.

int readSingle(): Reads the analog value of a single conversion. Set the conversion with with startSingleRead(pin) or startSingleDifferential(pinP, pinN).

The idea is that you call startSingleRead(pin), do your stuff and use readSingle() to read the value. If you want to read the value as soon as is converted, you can enable the interrupts (call enableInterrupt()) and have an isr like this:

volatile int result;
void adc0_isr(void) {
result = ADC0_RA;
// do more stuff
}

When you start the measurement you can also check if the ADC is done converting using isComplete() (see that if you read the value this function will return 0, so don't use both the isr and this).

Good luck.
 
Ah super, Pedvide!
It was just missing in the examples, that is why I couldnt find this function..
really great, Thanks!

Hello,

This is already implemented!!
int startSingleRead(uint8_t pin): Starts an analog measurement on the pin. It returns inmediately, get value with readSingle(). If the pin is incorrect it returns ADC_ERROR_VALUE This function is interrupt safe. The ADC interrupt will restore the adc to its previous settings and restart the adc if it stopped a measurement. If you modify the adc_isr then this won't happen.

int startSingleDifferential(uint8_t pinP, uint8_t pinN): Same as above but with differential measurements.

int readSingle(): Reads the analog value of a single conversion. Set the conversion with with startSingleRead(pin) or startSingleDifferential(pinP, pinN).

The idea is that you call startSingleRead(pin), do your stuff and use readSingle() to read the value. If you want to read the value as soon as is converted, you can enable the interrupts (call enableInterrupt()) and have an isr like this:

volatile int result;
void adc0_isr(void) {
result = ADC0_RA;
// do more stuff
}

When you start the measurement you can also check if the ADC is done converting using isComplete() (see that if you read the value this function will return 0, so don't use both the isr and this).

Good luck.
 
I'm looking into the best solution, I'm comparing the startSingleRead() construction vs. startContinuous ()
I've got the startSingleRead() running very smoothly, but ik takes about 2/3uS time, so I'm investigating on continuous mode to get it even better. Now I've got the system running @12bits with 22uS between samples (45k5 samplerate), and that includes the audiooutput, makes a 18% idle load. Actually very good!
Continous mode should be even better, but the problem is syncing with it.
You mention .isComplete as function to check if the conversion is ready, but I think it doesn't work once the continuous adc is running.
Should I connect a interrupt to facilitate syncing via a custom flag, or can it be done by your library/hardware allready?
 
Hello,

Yeah, continuous mode is great if you don't mind about power consumption.
When in continuous mode the ADC is always converting so isComplete always returns 0 (I think, I'm not sure). The best way is to use a custom flag inside the adc_isr. See that analogReadContinuous() gives the last converted value.
 
I sure dont mind power consumption, it will be a wallsocket powered applciation in the end.
I've temporarily fixed it with continous and a custom flag in adc_isr, plus mux switching in the adc_isr.. not documented, but seems to work fine..

Hello,

Yeah, continuous mode is great if you don't mind about power consumption.
When in continuous mode the ADC is always converting so isComplete always returns 0 (I think, I'm not sure). The best way is to use a custom flag inside the adc_isr. See that analogReadContinuous() gives the last converted value.


Heres my code, it is a complete little performance testenvironment for multichannel realtime audio/controls.

Code:
/*

TESTING Teensy3.1 6 Channel ADC with interupt for low latency applications 
Paul Driessen

*/

#include <ADC.h>

ADC adc; // adc object

int rawAudioIn; 
int dcAudio;
int audioIn;
int audioOut;
int sampleRate;
int microsPerSample;
elapsedMicros microCount;
elapsedMillis updateDisplay;
int accu=0;


void setup()
{                
  Serial.begin(38400);
  delay(1000);
  adc.setAveraging(1);                   // where not into astrophysics here, no averaging required
  adc.setResolution(10);                 // valid resolutions for singelended adc like this are 8, 10, 12 or 16 bit.
                                         // each resolution comes with it own max speed.  
                                         // Note: 16 bit will distort the output easy, I've added no conversion between bit depth buildin ( but it's easy: code it yourself!)
  sampleRate=40000 ;                     // requested samplerate, will result however in 1 uS steppable frequencies!
  microsPerSample= 1000000/sampleRate;   // thats how we keep pace
  
  analogWriteResolution(12);             // max resolution on the output
  sei();
  for(int t=0; t<40000; t++)
  {
    accu  += adc.analogRead(A0);          // get the dc level.. 
  }
  dcAudio = accu / 40000;
  
  adc.enableInterrupts();
}

// Stuff for the input sequenser..
byte channel2sc1a[]= {
    5, 14, 8, 9, 13, 12, 6, 7, 15, 4,
    0, 19, 3, 21, 26, 22};
byte adcChannel[6]={channel2sc1a[A0],
                    channel2sc1a[A1],
                    channel2sc1a[A2],
                    channel2sc1a[A3],
                    channel2sc1a[A4],
                    channel2sc1a[A5]};
int  adcOutput[6];
byte currentAdcChannel;
volatile byte readySampling;
int adcUsage;
void loop()                     
{
  microCount=0;                           // hard reset here, compensating wont work, overflow will reoccur repetitively.. 
  currentAdcChannel=0; 
  readySampling=0;
  adc.startContinuous (A0);
  //while(!readySampling){};                //  adc test, uncomment if needed
  int adcMC= microCount;
 
  analogWrite(A14, (audioOut+2048));             // 1uS: write the value from previous calculation
  audioIn += (rawAudioIn - dcAudio);             // get rid of DC values the easiest way.. 
  
  
  //  
  // E N D   O F  C O D I N G   A R E A  
  //
  //  while(microCount<microsPerSample){} 
  // Display Performance in %
  int MC= microCount;
  int Performance= (MC*100)/(microsPerSample);
  int adcPerformance= (adcMC*100)/(microsPerSample); 
  if(updateDisplay>1000)
  {
    Serial.print("Timing frame = ");
    Serial.print(microsPerSample);
    Serial.println(" uS"); 
    Serial.print("Code usage:");
    Serial.print(Performance);
    Serial.println("%"); 
     Serial.print("ADC usage:");
    Serial.print((adcUsage*100)/(microsPerSample));
    Serial.println("%");   
    Serial.print(adcOutput[0]);Serial.print(" ");
    Serial.print(adcOutput[1]);Serial.print(" ");
    Serial.print(adcOutput[2]);Serial.print(" ");
    Serial.print(adcOutput[3]);Serial.print(" ");
    Serial.print(adcOutput[4]);Serial.print(" ");
    Serial.print(adcOutput[5]);Serial.println(" "); 
    Serial.println(" ");
    //Serial.println( currentAdcChannel);
    //Serial.println( readySampling);
    
    updateDisplay=0;
  }  
  while(microCount<microsPerSample){}     // fill our timing budget

  
}

void adc0_isr(void) {

    // ADC0_RA; // ?????????
    GPIOC_PTOR = 1<<5;
     __disable_irq();
    adcOutput[currentAdcChannel]=ADC0_RA;
    currentAdcChannel++;
    if(currentAdcChannel>5)
    { 
        currentAdcChannel=0; 
        adc.stopContinuous(); 
        readySampling=1; 
        adcUsage=microCount; 
      }else{
       ADC0_SC1A = ADC_SC1_AIEN + adcChannel[currentAdcChannel]; // convert next channel..
     }
      
     __enable_irq();
}

/*
A single-ended input is selected for conversion through the ADCH channel select bits when the DIFF bit in the
SC1n register is low.
ADC status and control registers 1 (ADC0_SC1A) 
ADC0_SC1A
0-4 ADC channel
5 DIFF   diff enable
6 AIEN   interrutp enable
7 COCO  The COCO bit is cleared when the respective SC1n register is written or when the respective Rn register is read.

ADC0_CFG1
7 ADLPC  lowpower
6–5 ADIV  CLOCKDIVIDER
4 ADLSMP  sampletime 0 SHORT / 1 long
3-2 MODE BITDEPTH: 00=8BIT 01=12BIT 10=10BIT 11=BIT
1–0 ADICLK
  00 Bus clock.
  01 Bus clock divided by 2.
  10 Alternate clock (ALTCLK).
  11 Asynchronous clock (ADACK).  <-- klok start bij opstarten! langzamer

ADC0_CFG1
4 MUXSEL   select mux set???
3 ADACKEN  Asynchronous clock output enable   <-- op 1, start sneller op
2 ADHSC
1-0  long speed sample (when ADSLMP=1)

ADC0_RA
dataresult register

ADC0_SC2
7 ADACT   ->verschil met COCO? omgedraaid..
6 ADTRG   0 soft 1 hard
5 ACFE    1 comparator on
4 ACFGT    comp
3 ACREN      comp
2 DMAEN   dma enable
1-0      analog reference select 0 default 1 alt

*/
 
Hi,

At the beginning of the code you set the resolution at 10 bits
adc.setResolution(10);
but then you call
analogWriteResolution(12);
I don't think it's a good idea to mix analog.c functions with ADC library ones! I really don't know what the resolution for you measurements will be, I think the ADC saves the 10 bits, so when you call other functions it will use it, but you should do something like:
Code:
adc.setResolution(12);
// get the DC level
adc.setResolution(10);


Seeing your code, you don't need the continuous measurement at all! If your plan is to measure in different channels one after the other you can call adc.startSingleRead(pin0) and in the adc_isr store the result and call the next measurement as you do.
You can substitute
Code:
ADC0_SC1A = ADC_SC1_AIEN + adcChannel[currentAdcChannel]; // convert next channel..
by adc.startSingleRead(currentAdcChannel); So that the adc_isr will be called again when the conversion is done.
 
DAC output doesn't need to be same bitdepth as adc input. its another device.
yes, I also tested it without continuous, also works. I know that this isnt clean coded, sorry for that.. works anyhow.
But ill switch to DMA as soon as I've hacked that code..
 
I've checked it again: startContinuous with switching is somewhat faster than startSingleread , and that is the reason I use it. But could not find documentation for it. Just swap the call in loop(), you can see the difference in the serial output. (rounding to uS occurs, take the average of 20 readings or so, then you'll have a indicator of the speed difference, my statistics arent that hightech sofar..)
//adc.startContinuous (A0);
adc.startSingleRead(A0);
My project is starting out very well, thanks to your good work, Pedvide, and this code I've come up with now is really good enough for the early development stages, at some point i will switch to full DMA as described in this article:
http://cache.freescale.com/files/32bit/doc/app_note/AN4590.pdf?&Parent_nodeId=&Parent_pageType=
 
Status
Not open for further replies.
Back
Top