mborgerson
Well-known member
I converted my earlier sample to be pseudo transient logger with the following characteristics:
* Storage is started by a low-going pulse on Pin 1.
* ADC conversions are done in continuous mode with an interrupt at the end of conversion.
* The interrupt handler records the number CPU cycles since the last interrupt and the ADC value.
* The data is stored in the T4.1 PSRAM chip. I record 4 bytes per sample, for a total of about 4MB per file.
* After the data is collected, it is written to the SD card.
* During collection, the ADC is assigned a high interrupt priority (32) so that it is not disturbed by things like
USB or serial port interrupts---but systick interrupts still happen.
* The recorded 12-bit data has about +/- 20 LSBs of noise. I suspect that most of it is in the 3.3V reference,
since my test input is a 3.3K resistor from a digital pin to the ADC pin with a 10uF cap from the ADC pin to
ground.
I found that I got pretty impressive sampling speeds with the T4.1 clock at 600MHz:
12-bit conversion, High Speed ADC clock, Medium Speed Sampling: 987,354 samples/second
12-bit conversion, Very High Speed ADC clock, High Speed Sampling: 1,172,903 samples/second
12-bit conversion, Very High Speed ADC clock, Very High Speed Sampling: 1,339,177 samples/second
8-bit conversion, Very High Speed ADC clock, Very High Speed Sampling: 1,875,806 samples/second
As you can see, all except the slowest meet the MegaSample criterion.
Another thing I finally realized is although there is about 30 to 40 cycles timing jitter in the collection of the data
by the interrupt handler, THIS DOES NOT MEAN THERE IS JITTER IN THE TIMING OF THE ADC COLLECTION!
The ADC sampling and conversion timing is under the control of the ADC clock system. So long as the interrupt
handler reads the data before the end of the next conversion, there are no ADC timing errors. Since the maximum
disturbance to the interrupt timing is about 40 CPU cycles, I don't think ADC timing errors are likely.
The ADC IRQ handler executes in about 80nSec, so there are plenty of CPU cycles for other things, even while
collection is in progress.
Some things I plan to work on over the next few days:
1. Put the data in a circular buffer. After arming, the data goes into the queue. At trigger time, the
queue position is marked. After a full circle in the queue, the logger starts at some position before
the trigger an saves a full queue's worth of data. This allows the file to have a 'pre-trigger' front
porch.
2. Add a second ADC channel on ADC1. Save that data instead of all the timing intervals. The IRQ
handler will just record the maximum and minimum intervals between interrupts.
3. See whether continuous megasample logging is possible using the PSRAM for two large ping-pong buffers.
Here's the code:
* Storage is started by a low-going pulse on Pin 1.
* ADC conversions are done in continuous mode with an interrupt at the end of conversion.
* The interrupt handler records the number CPU cycles since the last interrupt and the ADC value.
* The data is stored in the T4.1 PSRAM chip. I record 4 bytes per sample, for a total of about 4MB per file.
* After the data is collected, it is written to the SD card.
* During collection, the ADC is assigned a high interrupt priority (32) so that it is not disturbed by things like
USB or serial port interrupts---but systick interrupts still happen.
* The recorded 12-bit data has about +/- 20 LSBs of noise. I suspect that most of it is in the 3.3V reference,
since my test input is a 3.3K resistor from a digital pin to the ADC pin with a 10uF cap from the ADC pin to
ground.
I found that I got pretty impressive sampling speeds with the T4.1 clock at 600MHz:
12-bit conversion, High Speed ADC clock, Medium Speed Sampling: 987,354 samples/second
12-bit conversion, Very High Speed ADC clock, High Speed Sampling: 1,172,903 samples/second
12-bit conversion, Very High Speed ADC clock, Very High Speed Sampling: 1,339,177 samples/second
8-bit conversion, Very High Speed ADC clock, Very High Speed Sampling: 1,875,806 samples/second
As you can see, all except the slowest meet the MegaSample criterion.
Another thing I finally realized is although there is about 30 to 40 cycles timing jitter in the collection of the data
by the interrupt handler, THIS DOES NOT MEAN THERE IS JITTER IN THE TIMING OF THE ADC COLLECTION!
The ADC sampling and conversion timing is under the control of the ADC clock system. So long as the interrupt
handler reads the data before the end of the next conversion, there are no ADC timing errors. Since the maximum
disturbance to the interrupt timing is about 40 CPU cycles, I don't think ADC timing errors are likely.
The ADC IRQ handler executes in about 80nSec, so there are plenty of CPU cycles for other things, even while
collection is in progress.
Some things I plan to work on over the next few days:
1. Put the data in a circular buffer. After arming, the data goes into the queue. At trigger time, the
queue position is marked. After a full circle in the queue, the logger starts at some position before
the trigger an saves a full queue's worth of data. This allows the file to have a 'pre-trigger' front
porch.
2. Add a second ADC channel on ADC1. Save that data instead of all the timing intervals. The IRQ
handler will just record the maximum and minimum intervals between interrupts.
3. See whether continuous megasample logging is possible using the PSRAM for two large ping-pong buffers.
Here's the code:
Code:
/*******************************************************
One MegaSample T4.0 ADC 12-bit
MJB 4/2/20
updated 8/9/2020
NOTE:
this test runs the ADC as fast as possible and saves the
data in an IRQ handler
The program uses the ADC library from Pedvide, which
is one of the libraries installed by Teensyduino.
If you plan to do high-speed ADC acquisition, learning
how this library works should be high on your TO-DO
list.
***************************************************************/
#include "SdFat.h"
#include "sdios.h"
#include <TimeLib.h>
#include <ADC.h>
/*******************************************************/
// when USEMTP is defined, you can upload file with MTP,
// but you have to have the MTP library and modified USB files
#define USEMTP
#ifdef USEMTP
#include "MTP.h"
#include <Storage.h>
#include <usb1_mtp.h>
MTPStorage_SD storage;
MTPD mtpd(&storage);
#endif
/*******************************************************/
// instantiate a new ADC object
ADC *adc = new ADC(); // adc object;
// This version uses SdFs, which can be either FAT32 or EXFat
SdFs sdf;
SdioCard sdc;
FsFile logFile;
#define SD_CONFIG SdioConfig(FIFO_SDIO)
const char compileTime [] = "T4.1 MegaSample 12-bit transient logger Compiled on " __DATE__ " " __TIME__;
const int tranpin = 31;
const int admarkpin = 32; // Changed to end pins on T4.1
const int wrmarkpin = 33;
const int ledpin = 13;
const int trigpin = 0;
// ADMARKHI and ADMARKLO are used to observe ADC timing on oscilloscope
#define ADMARKHI digitalWriteFast(admarkpin, HIGH);
#define ADMARKLO digitalWriteFast(admarkpin, LOW);
// WRMARKHI and WRMARKLO are used to observe SDC Write timing on oscilloscope
#define WRMARKHI digitalWriteFast(wrmarkpin, HIGH);
#define WRMARKLO digitalWriteFast(wrmarkpin, LOW);
// tranpin is the signal we record as it brings A) high through RC network
#define TRANHI digitalWriteFast(tranpin, HIGH);
#define TRANLO digitalWriteFast(tranpin, LOW);
#define LEDON digitalWriteFast(ledpin, HIGH);
#define LEDOFF digitalWriteFast(ledpin, LOW);
struct datrec {
uint16_t deltacount;
uint16_t adval;
};
volatile struct datrec *baseptr = (datrec *) 0x70000000;
volatile struct datrec *bufptr = baseptr;
volatile bool logging = false;
volatile uint32_t bufcount;
uint16_t adspeed = 1;
#define ADBLOCKSIZE 1048576 // 4MBytes of samples at ~1MHz
/*****************************************************
This is the ADC Completion interrupt handler
It executes in about 80nSec when saving data
and about 40nSec when not saving
******************************************************/
volatile uint32_t dwtlast;
void ADCChore(void) {
uint16_t value;
uint32_t dwt;
dwt = ARM_DWT_CYCCNT;
ADMARKHI
value = adc->adc0->readSingle();
if (logging) { // Save result to buffer
bufptr->deltacount = dwt - dwtlast;
bufptr->adval = value;
bufptr++;
bufcount++;
// stop logging when enough samples collected
if (bufcount >= ADBLOCKSIZE) logging = false;
} // end of if(logging)
// ADMARHI to ADMARKLO takes about 180nS at 600MHz
// So some oversampling may be possible
dwtlast = dwt;
ADMARKLO
}
void SetupPins(void) {
pinMode(trigpin, INPUT_PULLUP); // low going signals triggers recording
pinMode(tranpin, OUTPUT); // Generates hi-going transient to record
TRANLO
pinMode(admarkpin, OUTPUT); // for measuring interrupt time with O-Scope
pinMode(wrmarkpin, OUTPUT); // Signals SD Write Interval for O-Scope
pinMode(ledpin, OUTPUT); // LED is on during transient recording
pinMode(A0, INPUT_DISABLE); // disable digital keeper resistors
}
// Set ADC Speed: 1: slower 2: medium 3:fastest possible 12-bit 4 fastest 8-bit
// Speeds are relative---after all this is a high-speed transient recorder!
void SetADCSpeed(uint16_t spd) {
adc->adc0->disableInterrupts();
adc->adc0->stopContinuous();
switch (spd) {
case 1:
Serial.println("ADC Speed set to 12-bit low range.");
adc->adc0->setResolution(12); // set bits of resolution
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::MED_SPEED);
adspeed = 1;
break;
case 2:
Serial.println("ADC Speed set to 12-bit medium range.");
adc->adc0->setResolution(12); // set bits of resolution
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
adspeed = 2;
break;
case 3:
Serial.println("ADC Speed set to 12-bit high range.");
adc->adc0->setResolution(12); // set bits of resolution
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
adspeed = 3;
break;
case 4:
Serial.println("ADC Speed set to 8-bit high range.");
adc->adc0->setResolution(8); // set bits of resolution
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
adspeed = 4;
break;
}
adc->adc0->enableInterrupts(ADCChore);
adc->adc0->startContinuous(A0);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
delay(500); // wait for Serial to open
Serial.println();
Serial.println(compileTime);
Serial.printf("CPU Frequency: %lu\n", F_CPU_ACTUAL);
Serial.println("Requires installed PSRAM chip.");
SetupPins();
// enable the CPU cycle counter for timing interrupt routine
ARM_DEMCR |= ARM_DEMCR_TRCENA;
ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
logging = false;
adc->adc0->setAveraging(1 ); // set number of averages
adc->adc0->setResolution(12); // set bits of resolution
if (!StartSDCard()) {
// do fast blink forever
do { // hang with blinking LED
LEDON
delay(100);
LEDOFF
delay(100);
} while (1);
}// end of if (!StartSDCard())
SetADCSpeed(1); // start at slowest speed
setSyncProvider(getTeensy3Time); // helps put time into file directory data
#ifdef USEMTP
StartMTP();
#endif
}
void loop() {
// put your main code here, to run repeatedly:
char ch;
if (Serial.available()) {
ch = Serial.read();
if (ch == '1') SetADCSpeed(1);
if (ch == '2') SetADCSpeed(2);
if (ch == '3') SetADCSpeed(3);
if (ch == '4') SetADCSpeed(4);
if (ch == 'l') LogTran();
if (ch == 's') ShowADC();
if (ch == 't') ShowTiming();
if (ch == 'd') sdf.ls(LS_SIZE | LS_DATE | LS_R);
}
#ifdef USEMTP
mtpd.loop();
#endif
}
/******************************************************
Read one milllion samples and store in PSRAM
During collection, ADC Interrupt priority is raised
to a high level so that it is higher than everything
except the system tick
*****************************************************/
void LogTran(void) {
uint32_t startmillis, dmillis, drate;
Serial.print("Waiting for low-going trigger. . . .");
Serial.send_now();// wait for USB serial to finish up.
delay(500);
// if trigger pin still low, wait until it goes high
while(!digitalReadFast(trigpin)) delay(5);
// trigger pin is high now
while(digitalReadFast(trigpin))delay(5);
// Trigger pin has gone low, so trigger tranpin and start logging
LEDON
bufcount = 0;
bufptr = (datrec *) baseptr;
NVIC_SET_PRIORITY(IRQ_ADC1, 32); // Very high priority
// NVIC_DISABLE_IRQ(IRQ_USB1); // didn't really help only about 6 clocks less
startmillis = millis();
logging = true;
delay(2); // put a 2msec leader before transient
TRANHI
delay(200);
TRANLO
// Now wait until logging is done
while (logging); // wait until sampling finished
//NVIC_ENABLE_IRQ(IRQ_USB1);
dmillis = millis()-startmillis;
NVIC_SET_PRIORITY(IRQ_ADC1, 128); // Normal priority
TRANLO
LEDOFF
drate = (1000 * bufcount)/dmillis;
Serial.printf("\nCollected %lu samples in %lu milliseconds or %u samples/second\n", bufcount, dmillis, drate);
Serial.printf("Last Sample stored at %p\n", bufptr - 1);
// save log file by writing as one large chunk
if ( OpenLogFile()) {
logFile.write((void *)baseptr, ADBLOCKSIZE * sizeof(datrec));
logFile.close();
Serial.printf("Wrote %lu bytes to log file.\n",ADBLOCKSIZE * sizeof(datrec));
} else Serial.println("Could not open log file!");
}
/******************************************************
Transient logger SD Card File Name Creator
output has appended datetime
in _MMDDHHmm format
for example: Tran_08152353.TRn where n is conversion speed
*****************************************************/
const char* FNString(void) {
static char fname[64];
time_t nn;
nn = now();
int mo = month(nn);
int dd = day(nn);
int hh = hour(nn);
int mn = minute(nn);
sprintf(fname, "Tran_%02d%02d%02d%02d.TR%d",mo,dd,hh,mn, adspeed);
return fname;
}
bool OpenLogFile(void) {
if (!logFile.open(FNString(), O_RDWR | O_CREAT | O_TRUNC)) {
return false;
}
return true;
}
void ShowTiming(void) {
uint32_t tidx,maxidx, minidx, spikecnt;
uint16_t val, mind, maxd;
float dsum, meand;
uint16_t psize;
volatile struct datrec *bptr = baseptr;
maxd = 0; maxidx = 0;
mind = 65535; minidx = 0;
dsum = 0.0;
for (tidx = 0; tidx < ADBLOCKSIZE; tidx++) {
val = bptr->deltacount;
dsum += val;
if (val > maxd) {
maxd = val;
maxidx = tidx;
}
if (val < mind) {
mind = val;
minidx = tidx;
}
bptr++;
}
Serial.printf("Timing Sum: %8.1f\n", dsum);
meand = dsum/ADBLOCKSIZE;
Serial.println("\nSample Timing intervals in CPU Clock Cycles");
Serial.printf("Minimum: %u at %u Maximum: %u at %u ", mind, minidx, maxd, maxidx);
Serial.printf(" Mean: %8.3f\n", meand);
psize = (maxd - mind) / 8;
// Determine hom many times there was a significant increase in write interval
spikecnt = 0;
bptr = baseptr;
for (tidx = 0; tidx < ADBLOCKSIZE - 1; tidx++) {
val = bptr->deltacount;
if (val > meand + psize) {
spikecnt++;
}
bptr++;
}
Serial.printf("Total disruptions: %lu\n", spikecnt);
}
/******************************************************
Display data from adcbuffer0 in lines of 20 values
Only the first NUMTOSHOW values are displayed
*****************************************************/
#define NUMTOSHOW 100 // change to alter numbers output
void ShowADC(void) {
uint32_t sendidx;
uint16_t val;
volatile struct datrec *bptr = baseptr;
Serial.println("Delta Counts");
// Sending the full 128K samples would a long time!
for (sendidx = 0; sendidx < NUMTOSHOW; sendidx++) {
if ((sendidx % 16) == 0) {
Serial.printf("\n%p: ", bptr);
}
val = bptr->deltacount;
bptr++;
Serial.printf("% 5u", val);
}
bptr = baseptr;
Serial.println("\nADC Values");
for (sendidx = 0; sendidx < NUMTOSHOW; sendidx++) {
if ((sendidx % 16) == 0) {
Serial.printf("\n%p: ", bptr);
}
val = bptr->adval;
bptr++;
Serial.printf("% 5u", val);
}
Serial.println();
}
bool StartSDCard() {
if (!sdf.cardBegin(SD_CONFIG)) {
Serial.println("cardBegin failed");
}
if (!sdf.volumeBegin()) {
Serial.println("volumeBegin failed");
}
if (!sdf.begin(SdioConfig(FIFO_SDIO))) {
Serial.println("\nSD File initialization failed.\n");
return false;
} else Serial.println("initialization done.");
if (sdf.fatType() == FAT_TYPE_EXFAT) {
Serial.println("Type is exFAT");
} else {
Serial.printf("Type is FAT%d\n", int16_t(sdf.fatType()));
}
// set date time callback function
SdFile::dateTimeCallback(dateTime);
return true;
}
/*****************************************************************************
Read the Teensy RTC and return a time_t (Unix Seconds) value
******************************************************************************/
time_t getTeensy3Time() {
return Teensy3Clock.get();
}
//------------------------------------------------------------------------------
/*
User provided date time callback function.
See SdFile::dateTimeCallback() for usage.
*/
void dateTime(uint16_t* date, uint16_t* time) {
// use the year(), month() day() etc. functions from timelib
// 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());
}
#ifdef USEMTP
void StartMTP(void) {
Serial.println("Starting MTP Responder");
usb_mtp_configure();
if (!Storage_init(&sdf)) {
Serial.println("Could not initialize MTP Storage!");
}
}
#endif