triggered by recent discussions on the forum, I implemented a High-Speed data logger based on the audio library
Objective
-use audio library
-sample Teensy build in ADC at, say 196 kHz
-use queue object to provide acquisition buffer
-use Bill Greiman sdFS for logging on Teensy3.6 build in uSD card
So at the beginning there should be something like this
there are two tasks
-interfacing Queue object to sdFS
-changing ADC sampling rate from 44.1 kHz to 192 kHz
interfacing queue object to sdFS was easy
changing sampling frequency needed a little bit more understanding of the processor.
(also, as I wanted to use the audio library as is and not create another modified version)
The problem was that it was necessary to change the ADC setup (number of averaging) to have fast enough conversion for selected sampling frequency.
In the end I succeeded.
Needless to say, this program is a demonstration and modifications are possible and sometimes necessary but should be obvious
The statistics printout gives also a nice insight in the different delays (600 Audio blocks provide sufficient acquisition buffering for single channel at 192 kHz)
(caveat: programs compiles and runs, but I have NO external device to be hooked onto Teensy to verify, someone else can do that)
The final adc_logger demonstration program consists of three files
Main ino file
logger interface
adc modifications
Objective
-use audio library
-sample Teensy build in ADC at, say 196 kHz
-use queue object to provide acquisition buffer
-use Bill Greiman sdFS for logging on Teensy3.6 build in uSD card
So at the beginning there should be something like this
Code:
AudioInputAnalog adc1(A3);
AudioRecordQueue queue1;
AudioConnection patchCord1(adc1, queue1);
there are two tasks
-interfacing Queue object to sdFS
-changing ADC sampling rate from 44.1 kHz to 192 kHz
interfacing queue object to sdFS was easy
changing sampling frequency needed a little bit more understanding of the processor.
(also, as I wanted to use the audio library as is and not create another modified version)
The problem was that it was necessary to change the ADC setup (number of averaging) to have fast enough conversion for selected sampling frequency.
In the end I succeeded.
Needless to say, this program is a demonstration and modifications are possible and sometimes necessary but should be obvious
The statistics printout gives also a nice insight in the different delays (600 Audio blocks provide sufficient acquisition buffering for single channel at 192 kHz)
(caveat: programs compiles and runs, but I have NO external device to be hooked onto Teensy to verify, someone else can do that)
The final adc_logger demonstration program consists of three files
Main ino file
Code:
/*
* Simple High speed ADC logger
* using Bill Greimans's SdFs on Teensy 3.6
* uses Audio library for acquisition queuing
* build-in ADC is modified to sample at other than 44.1 kHz
*/
#define F_SAMP 192000 // desired sampling frequency
//==================== Audio interface ========================================
/*
* standard Audio Interface
* to avoid loading stock SD library
* NO Audio.h is called but required header files are called directly
*/
#include "input_adc.h"
#include "record_queue.h"
AudioInputAnalog adc1(A3);
AudioRecordQueue queue1;
AudioConnection patchCord1(adc1, queue1);
#include "adc_mods.h"
#include "adc_logger_if.h"
//__________________________General Arduino Routines_____________________________________
void setup() {
// put your setup code here, to run once:
AudioMemory (600);
while(!Serial);
pinMode(13,OUTPUT);
digitalWriteFast(13,HIGH);
uSD.init();
modifySampling(F_SAMP);
queue1.begin();
digitalWriteFast(13,LOW);
}
void loop() {
// put your main code here, to run repeatedly:
if(queue1.available())
{
// fetch data from queue
int16_t * data =queue1.readBuffer();
//
// copy to disk buffer
for(int ii=0;ii<128;ii++) outptr[ii] = data[ii];
queue1.freeBuffer();
//
// adjust buffer pointer
outptr+=128;
//
// if necessary reset buffer pointer and write to disk
if(outptr == diskBuffer+BUFFERSIZE)
{
outptr = diskBuffer;
// write to disk
uSD.write(diskBuffer,BUFFERSIZE); // this is blocking
}
}
// some statistics on progress
static uint32_t t0;
if(millis()>t0+1000)
{ Serial.printf("loop: %4d %5d;",uSD.getNbuf(),AudioMemoryUsageMax());
Serial.printf("%5d %5d\n",PDB0_CNT, PDB0_MOD);
AudioMemoryUsageMaxReset();
t0=millis();
}
}
Code:
#ifndef _ADC_LOGGER_IF_H
#define _ADC_LOGGER_IF_H
#include "kinetis.h"
#include "core_pins.h"
//==================== local uSD interface ========================================
#include "SdFs.h"
// Preallocate 40MB file.
const uint64_t PRE_ALLOCATE_SIZE = 40ULL << 20;
// Use FIFO SDIO or DMA_SDIO
#define SD_CONFIG SdioConfig(FIFO_SDIO)
//#define SD_CONFIG SdioConfig(DMA_SDIO)
#define MAXFILE 100
#define MAXBUF 1000
#define BUFFERSIZE (16*1024)
int16_t diskBuffer[BUFFERSIZE];
int16_t *outptr = diskBuffer;
char *header=0;
class c_uSD
{
private:
SdFs sd;
FsFile file;
public:
c_uSD(): state(-1) {;}
void init();
void write(int16_t * data, int32_t ndat);
uint16_t getNbuf(void) {return nbuf;}
private:
int16_t state; // 0 initialized; 1 file open; 2 data written; 3 to be closed
int16_t nbuf;
char *makeFilename(char * filename);
};
c_uSD uSD;
/*
* Logging interface support / implementation functions
*/
//_______________________________ For File Time settings _______________________
#include <time.h>
#define EPOCH_YEAR 2000 //T3 RTC
#define LEAP_YEAR(Y) (((EPOCH_YEAR+Y)>0) && !((EPOCH_YEAR+Y)%4) && ( ((EPOCH_YEAR+Y)%100) || !((EPOCH_YEAR+Y)%400) ) )
static const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31};
/* int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
*/
struct tm seconds2tm(uint32_t tt)
{ struct tm tx;
tx.tm_sec = tt % 60; tt /= 60; // now it is minutes
tx.tm_min = tt % 60; tt /= 60; // now it is hours
tx.tm_hour = tt % 24; tt /= 24; // now it is days
tx.tm_wday = ((tt + 4) % 7) + 1; // Sunday is day 1 (tbv)
// tt is now days since EPOCH_Year (1970)
uint32_t year = 0;
uint32_t days = 0;
while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= tt) year++;
tx.tm_year = 1970+year; // year is NOT offset from 1970
// correct for last (actual) year
days -= (LEAP_YEAR(year) ? 366 : 365);
tt -= days; // now tt is days in this year, starting at 0
uint32_t mm=0;
uint32_t monthLength=0;
for (mm=0; mm<12; mm++)
{ monthLength = monthDays[mm];
if ((mm==1) & LEAP_YEAR(year)) monthLength++;
if (tt<monthLength) break;
tt -= monthLength;
}
tx.tm_mon = mm + 1; // jan is month 1
tx.tm_mday = tt + 1; // day of month
return tx;
}
uint32_t tm2seconds (struct tm *tx)
{
uint32_t tt;
tt=tx->tm_sec+tx->tm_min*60+tx->tm_hour*3600;
// count days size epoch until previous midnight
uint32_t days=tx->tm_mday;
uint32_t mm=0;
uint32_t monthLength=0;
for (mm=0; mm<(tx->tm_mon-1); mm++) days+=monthDays[mm];
if(tx->tm_mon>2 && LEAP_YEAR(tx->tm_year-1970)) days++;
uint32_t years=0;
while(years++ < (tx->tm_year-1970)) days += (LEAP_YEAR(years) ? 366 : 365);
//
tt+=(days*24*3600);
return tt;
}
// Call back for file timestamps (used by FS). Only called for file create and sync().
void dateTime(uint16_t* date, uint16_t* time)
{
struct tm tx=seconds2tm(RTC_TSR);
// Return date using FS_DATE macro to format fields.
*date = FS_DATE(tx.tm_year, tx.tm_mon, tx.tm_mday);
// Return time using FS_TIME macro to format fields.
*time = FS_TIME(tx.tm_hour, tx.tm_min, tx.tm_sec);
}
//____________________________ FS Interface implementation______________________
void c_uSD::init()
{
if (!sd.begin(SD_CONFIG)) sd.errorHalt("sd.begin failed");
// Set Time callback
FsDateTime::callback = dateTime;
//
nbuf=0;
state=0;
}
char *c_uSD::makeFilename(char * filename)
{ static int ifl=0;
ifl++;
if (ifl>MAXFILE) return 0;
sprintf(filename,"File%04d.raw",ifl);
Serial.println(filename);
return filename;
}
void c_uSD::write(int16_t *data, int32_t ndat)
{
if(state == 0)
{ // open file
char filename[40];
if(!makeFilename(filename)) {state=-1; return;} // flag to do not anything
//
if (!file.open(filename, O_CREAT | O_TRUNC |O_RDWR))
{ sd.errorHalt("file.open failed");
}
if (!file.preAllocate(PRE_ALLOCATE_SIZE))
{ sd.errorHalt("file.preAllocate failed");
}
state=1; // flag that file is open
digitalWriteFast(13,LOW);
nbuf=0;
}
if(state == 1 || state == 2)
{ // write to disk
if(state==1)
{ // write header
state=2;
if(header)
{ if (512 != file.write((char *) header, 512)) sd.errorHalt("file.write header failed");
}
}
// write now data
if (2*ndat != file.write((char *) data, 2*ndat)) sd.errorHalt("file.write data failed");
nbuf++;
if(nbuf==MAXBUF) state=3; // flag to close file
}
if(state == 3)
{
digitalWriteFast(13,HIGH);
// close file
file.truncate();
file.close();
state=0; // flag to open new file
}
}
#endif
Code:
#ifndef _ADC_MODS_H
#define _ADC_MODS_H
/*
* -------------------mods for changing sampling frequency--------------------------
*
* following Mark Butcher: https://community.nxp.com/thread/434148
* and https://community.nxp.com/thread/310745
*
* the waitfor cal was adapted from cores/teensy3/analog.c
* as it is static declared in analog.c and not visible outside
*/
#include "kinetis.h"
#include "core_pins.h"
static void analogWaitForCal(void)
{ uint16_t sum;
#if defined(HAS_KINETIS_ADC0) && defined(HAS_KINETIS_ADC1)
while ((ADC0_SC3 & ADC_SC3_CAL) || (ADC1_SC3 & ADC_SC3_CAL)) { }
#elif defined(HAS_KINETIS_ADC0)
while (ADC0_SC3 & ADC_SC3_CAL) { }
#endif
__disable_irq();
sum = ADC0_CLPS + ADC0_CLP4 + ADC0_CLP3 + ADC0_CLP2 + ADC0_CLP1 + ADC0_CLP0;
sum = (sum / 2) | 0x8000;
ADC0_PG = sum;
sum = ADC0_CLMS + ADC0_CLM4 + ADC0_CLM3 + ADC0_CLM2 + ADC0_CLM1 + ADC0_CLM0;
sum = (sum / 2) | 0x8000;
ADC0_MG = sum;
#ifdef HAS_KINETIS_ADC1
sum = ADC1_CLPS + ADC1_CLP4 + ADC1_CLP3 + ADC1_CLP2 + ADC1_CLP1 + ADC1_CLP0;
sum = (sum / 2) | 0x8000;
ADC1_PG = sum;
sum = ADC1_CLMS + ADC1_CLM4 + ADC1_CLM3 + ADC1_CLM2 + ADC1_CLM1 + ADC1_CLM0;
sum = (sum / 2) | 0x8000;
ADC1_MG = sum;
#endif
__enable_irq();
}
//
void modifyADC(int16_t res, uint16_t avg)
{ // Mono only
// stop PDB
uint32_t ch0c1 = PDB0_CH0C1; // keep old value
PDB0_CH0C1 = 0; // disable ADC triggering
PDB0_SC &= ~PDB_SC_PDBEN;
//
analogReadRes(res);
analogReference(INTERNAL); // range 0 to 1.2 volts
analogReadAveraging(avg);
analogWaitForCal();
ADC0_SC2 |= ADC_SC2_ADTRG | ADC_SC2_DMAEN; // reassert HW trigger
// restart PDB
(void)ADC0_RA;
PDB0_CH0C1 = ch0c1;
PDB0_SC |= PDB_SC_PDBEN ;
PDB0_SC |= PDB_SC_SWTRIG ; // kick off the PDB* - just once
}
//modify Sampling rate
#define PDB_CONFIG (PDB_SC_TRGSEL(15) | PDB_SC_PDBEN | PDB_SC_CONT | PDB_SC_PDBIE | PDB_SC_DMAEN)
void modifySampling(uint32_t fsamp)
{
uint16_t n_bits=16, n_avg=1;
#define F_LIMIT (F_BUS/256) // this is found empirically for F_BUS == 60 MHz and 16 bits
if(fsamp<F_LIMIT/4) // assume that limit scales with n_avg
{ n_avg=4;
}
else if(fsamp<F_LIMIT) // assume that limit scales with n_avg
{ n_avg=1;
}
modifyADC(n_bits,n_avg);
// sampling rate can be modified on the fly
uint32_t PDB_period;
PDB_period = F_BUS/fsamp -1;
PDB0_MOD = PDB_period;
PDB0_SC = PDB_CONFIG | PDB_SC_LDOK;
}
#endif