High-Speed ADC Logger with Audio Library

Status
Not open for further replies.

WMXZ

Well-known member
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
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();
 }
}
logger interface
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
adc modifications
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
 
Status
Not open for further replies.
Back
Top