Gas station without pumps

2016 September 27

Teensy 3.5 & 3.6 Kickstarter

Filed under: Uncategorized — gasstationwithoutpumps @ 16:59
Tags: , , ,

As many of the followers of my blog know, the Teensy 3.1 and Teensy LC have been my favorite microcontroller boards for the past couple of years.  The Teensy 3.1 has since been replaced by the slightly better Teensy 3.2, which has a better voltage regulator but is otherwise pretty much the same as the 3.1.  I’ve been using the Teensy LC with PteroDAQ software for my electronics course.

I’ve just noticed that PJRC has a Kickstarter campaign for a new set of boards the Teensy 3.5 and 3.6.  These will be much more powerful ARM processors (120MHz and 180MHz Cortex M4 processors with floating-point units, so at least 2.5 times faster than the Teensy 3.2, more if floating-point is used much).  The form factor is similar to before, but the boards are longer, taking up 24 rows of a breadboard, instead of just 14.  The extra board space is mainly to provide more I/O, but there is also a MicroSD card slot.

The designer is still dedicated to making the Teensy boards run in the Arduino environment, and the breadboard-friendly layout is very good for experimenting.

PJRC is positioning the new boards between the old Teensy boards and the Linux-based boards like the Raspberry Pi boards. The new Teensy boards will have a lot of raw power, but not an operating system, though I suspect that people outside PJRC will try porting one of the small real-time operating systems to the board.

The new boards are a bit pricey compared to the Teensy LC ($23–28 instead of under $12 for the Teensy LC), but still reasonable for what they provide.  PJRC also has a history of providing good software for their boards.

I probably need to get both a Teensy 3.5 and a 3.6 to port PteroDAQ to them—that looks like a $50 purchase. If the boards and the software are available in time for me do development on PteroDAQ by December, I might get it done—any later than that and I’ll have no time, as I have a very heavy teaching and service load for Winter quarter.

I suspect that the new Teensyduino software will need a newer version of the Arduino development environment, which in turn would require a newer version of the Mac operating system (my laptop is still running 10.6.8), which in turn probably means a new laptop.

I’m waiting to see if Apple releases a new, usable MacBook Pro in October, so there is a bit of built-in delay in the whole process. I’m not impressed with their recent design choices for iPhones and MacBook Air—I need connections to my laptop—so there is a strong possibility that I may be having to leave the Macintosh family of products after having been a loyal user since 1984 (that’s 32 years now).

Update 2016 Sep 27: I just watched the Kickstarter video.  They used the Karplus-Strong algorithm as a demo.  Of course, that demo could have been done on a Z80 chip from 1976 (though not easily—the sampling rate suffered a bit to run 4 strings on the Z80, and only 8-bit DACs were affordable in those days).  It would have been more instructive to do an FM synthesis algorithm, which takes a lot more processor power than Karplus-Strong.

Advertisements

2016 July 13

Autoranging capacitance meter using TeensyLC

Filed under: Circuits course — gasstationwithoutpumps @ 09:59
Tags: , , , ,

 

My earlier post, Capacitance meter using touchRead(), showed the beginnings of a capacitance meter, using the touchRead() routine in the Teensyduino environment.

Today I’ll share a slightly more complicated program that uses the TSI (touch sensing input) peripheral more directly to make an autoranging capacitance meter that can measure capacitors down to 1pF and up to 3µF.  The repeatability of the measurements is not great (about ±3%), but the linearity seems pretty good.

The program includes the ability to zero out the test fixture (important for measuring small capacitances) and to calibrate the meter to a known capacitor.  I don’t have a capacitor with a tight tolerance, so I had to do my calibration against a DT-9205A multimeter, which is not a very reliable standard.  Still, it seemed more consistent than the labeling on the cheap ceramic capacitors I have, some of which seem to be off by a factor of two!

The multimeter could not measure capacitances at the low end of the range, so only nominal values are used there.

The multimeter could not measure capacitances at the low end of the range, so only nominal values are used there.

A number of the capacitors in the 100pF–10nF range seemed to drift consistently upward for quite a while, both with the multimeter and my homebrew capacitance meter. Touching them with my finger brought them sharply back down again, but touching them with stainless steel tweezers did not. I suspect that the effect is a thermal one, with the capacitance dropping when my fingers warmed the capacitors and going up again as the capacitors cooled to room temperature. The change was several percent, which is consistent with a low-quality “Y” dielectric.

// Preliminary program for a capacitance meter
// Kevin Karplus
// 2016 Jul 13


// To use:
//	* Connect a serial monitor (like the Arduino IDE) to the USB port
//	* Connect capacitor to measure between pin 0 and ground
//	* Press "a" and <return> to make an autorange measurement.

// Capacitances in the range 1pF to 3uF can be measured, but large
// capacitors take a long time to measure, up to 9s for 3uF.
// The measurement time can be reduced by reducing NUM_READS, perhaps
//	replacing the constant by an array dependent on range.
//	A factor of about 10 reduction in measurement time is available, as
//	NUM_READS is currrently set to 20.

// Note: readings may drift by +- 5%.  I've not yet determined the
// cause of this drift.  It may be thermal (cooling from finger
// temperature to room temperature may raise the capacitance by 2%,
// depending on the additives to the ceramic used as the dielectric).

// To calibrate:
//	* Remove capacitors from pin 0
//	* Type 'z' to measure the counts for the empty test fixture
//	* Pick a known capacitor (around 1nF) and connect it between
//		pin 0 and ground.  The capacitor must be small enough
//		to be measurable on the highest-resolution range (<=3nF).
//		To calibrate with a larger capacitor, first calibrate with
//		a small one to get the highest-resolution range set, then
//		repeat the calibration with the larger capacitor, which
//		will only reset the larger ranges.
//	* Type 'k', followed by an integer known capacitance in pF,
//		followed by a separator (like ';')
//	* Type 'c', to measure the pF per count

// Note: the calibrations are reported to the USB serial port, so they
// can be saved and used to edit the source code, changing count_for_0pF
// and pf_per_count arrays.

// The measurement process can be followed in more detail by turning
// on debugging with the 'D' command.
// Debugging can be turned off with the 'd' command.

// Capacitance is measured on pin 0 (one of several pins with TSI),
// because pin 0 is immediately next to ground, making it easy to connect
// small capacitors between pin 0 and ground.
#define TOUCH_PIN (0)

// forward references to later routines
int32_t capRead(uint8_t pin, 
		uint8_t I_ext=3, uint8_t I_ref=4, 
		uint8_t Prescale=2, uint8_t N_scan=9);
float autoCapRead(uint8_t pin);
int readInt(void);
void print_calib(int count, int n_cycles=1);
float read_counts_per_cycle(uint8_t i_ext, uint8_t i_ref,float count_for_zero);

volatile uint8_t debug;

// The current for the external oscillator and the reference
// oscillator of the TSI interface determine the capacitance range, resolution,
// and speed of the measurement.
// The program uses the highest-resolution range that does not cause
// the 16-bit counter to overflow.

// Settings of the current paramters for the different ranges
// The highest-resolution range is first, the widest range is last.
#define NUM_RANGES (6)
const int8_t i_ext_choices[NUM_RANGES] = {2,3,4,5,6,7};
const int8_t i_ref_choices[NUM_RANGES] = {5,4,3,2,1,0};

float pF_per_count[NUM_RANGES]=
	{0.2126, 0.772, 2.897, 11.08, 47., 195.3};
float count_for_0pF[NUM_RANGES] =
	{48.7612, 13.82, 3.813, 1.0605, 0.273, 0.0517};

// parameters for calibration
int C_known=0;	// known capacitance value 

void setup() 
{
    pinMode(TOUCH_PIN, INPUT);
    Serial.begin(115200);
    debug=0;
}

void loop()
{
     if (Serial.available())
     {   char c=Serial.read();
         if (c=='D') {debug=1;}
	 else if (c=='d') {debug=0;}
	 else if (c=='a')
         {   // do one reading and print it
             Serial.print(autoCapRead(TOUCH_PIN));
             Serial.println(" pF");
        }
        else if (c=='k')
        {   // set known capacitance for calibration checks
	   C_known=readInt();
        }
	else if (c=='z')
	{   // Set the count_for_0pF array for each range.
	    // Print the current parameters and zero count for each range.
	    // Only issue this command if there is no capacitor connected
	    // to TOUCH_PIN
	    for (int range=0; range<NUM_RANGES; range++)
	    {   uint8_t i_ext=i_ext_choices[range];
	        uint8_t i_ref=i_ref_choices[range];
		count_for_0pF[range] =read_counts_per_cycle(i_ext,i_ref,0.0);
		Serial.print("# zero for ");
		Serial.print(i_ext); Serial.print("\t");
		Serial.print(i_ref); Serial.print("\t");
		Serial.println(count_for_0pF[range],4);
	    }	
	}
	else if (c=='c')
	{   // do autocalibration, setting pF_per_count
	    // printing known capacitance, 
	    //		external current setting,
	    //		reference current setting,
	    //		average pF_per_count for a single cycle
	    
	    if (C_known==0)
	    {   Serial.println("# use 'k<known C in pF>;' first");
	    	return;
	    }
	    for (int range=0; range<NUM_RANGES; range++) { uint8_t i_ext=i_ext_choices[range]; uint8_t i_ref=i_ref_choices[range]; float calib=read_counts_per_cycle(i_ext,i_ref,count_for_0pF[range]); if (!isnan(calib)) { calib = C_known/calib; pF_per_count[range]= calib; } else { Serial.print("# "); // comment out overflows } Serial.print(C_known); Serial.print("\t"); Serial.print(i_ext); Serial.print("\t"); Serial.print(i_ref); Serial.print("\t"); Serial.println(calib,4); } } } } // print a calibration line void print_calib(int count, uint8_t i_ext, uint8_t i_ref, int n_cycles) { float avg_count = (count+0.0)/n_cycles; Serial.print(C_known); Serial.print("\t"); Serial.print(i_ext); Serial.print("\t"); Serial.print(i_ref); Serial.print("\t"); Serial.print(count); Serial.print("\t"); Serial.print(n_cycles); Serial.print("\t"); Serial.println(C_known/avg_count,4); } float read_counts_per_cycle(uint8_t i_ext, uint8_t i_ref, float count_for_zero) { // Do one measurement and return average counts/cycle - count_for_zero. // If debug set, print C_known, count, i_ext, i_ref, n_cycles, cap/count // // Actually does NUM_READS+1 measurements: one with a single cycle, // then again NUM_READS times with as many cycles as can be fit without // overflowing counter. if (debug) { Serial.println("# C\ti_ext\ti_ref\tcount\tn_cycle\tC/avg_count"); } int count=capRead(TOUCH_PIN, i_ext, i_ref, 0, 0); if (count>=0xFFFF)
    {   if (debug)
    	{   Serial.print("# "); // comment out overflows
	    print_calib(count,i_ext,i_ref,1);
	}
	return NAN;	// abort rest of calibration check
    }

    // Determine max number of cycles that can fit
    int cycles= 0xFFFE/count; // how many cycles to use
    int prescale,n_scan;
    for (prescale=0; cycles>32 && prescale<7; prescale++) {cycles/=2;} if (cycles==0) {n_scan=0;} else if (cycles>32) {n_scan=31;}
    else {n_scan=cycles-1;}
    cycles = (n_scan+1)<<prescale;

#define NUM_READS	(20)
    // To do: replace NUM_READS with a const uint8_t num_reads[NUM_RANGES]
    // array, to speed up measurement at high ranges.

    int sum_count=0;
    for (int i=0; i<NUM_READS; i++) { count=capRead(TOUCH_PIN, i_ext, i_ref, prescale, n_scan); if (debug) { print_calib(count, i_ext, i_ref, cycles); } sum_count+=count; } return (sum_count+0.0)/ (NUM_READS*cycles) -count_for_zero; } // Read a non-negative integer from Serial as a series of digits, terminated // by any non-digit (recommend using something obvious like ';'). // The terminating character is discarded. int readInt(void) { int value=0; while (!Serial.available()) {} // wait for next char char c=Serial.read(); while (c>='0' && c<='9')
    {   value= 10*value + (c-'0');
	while (!Serial.available()) {}	// wait for next char
       	c=Serial.read();
    }
    return value;
}
   


float autoCapRead(uint8_t pin)
{
    // Return the capcitance in pF at the pin, using the highest-resolution
    // range that doesn't overflow the counter.
    
    // pick the lowest (highest resolution) range that doesn't overflow
    int32_t count;	// number of counts of ref oscillator
    int range;
    for (range=0, count=0xFFFF;  range<NUM_RANGES && count>=0xFFFF; range++)
    {	count=capRead(pin, i_ext_choices[range],i_ref_choices[range],0,0);
    }
    range--;
    
    if (count>=0xFFFF)
    {    return NAN;	// capacitance too big to measure with TSI
    }
    
    float count_per_cycle=read_counts_per_cycle(i_ext_choices[range],
		i_ref_choices[range],
		count_for_0pF[range]);
    return pF_per_count[range]*count_per_cycle;
}


/* Raw reading is based on the
 * Teensyduino Core Library touch.c (which implements touchRead)
 * http://www.pjrc.com/teensy/
 * Copyright (c) 2013 PJRC.COM, LLC.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * 1. The above copyright notice and this permission notice shall be 
 * included in all copies or substantial portions of the Software.
 *
 * 2. If the Software is incorporated into a build system that allows 
 * selection among a list of target devices, then similar target
 * devices manufactured by PJRC.COM must be included in the list of
 * target devices and selectable in the same manner.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// #include "core_pins.h"

// capacitance is supposed to be
//	Cref * (1<<I_ext) / ( (1<<I_ref) * (1<<Prescale) * (N_scan+1) ) * COUNT
// but use 
// Cref * ext_current[I_ext] / ( ref_current[I_ref] * (1<<Prescale) * (N_scan+1) ) * COUNT // because the current ratios are not a constant factor of 2 // with DVOLT==0 (slowest, but least noise sensitive), // I_ext = I_ref-1, // Prescale=2, // N_scan=9, // Capacitance is approx 0.01846 pF * COUNT, // so Cref approx 1.47694pF #if defined(__MK20DX128__) || defined(__MK20DX256__) // These settings give approx 0.02 pF sensitivity and 1200 pF range // Lower current, higher number of scans, and higher prescaler // increase sensitivity, but the trade-off is longer measurement // time and decreased range. static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 12, 255, 255, 255, 255, 255, 255, 11, 5 }; #elif defined(__MK66FX1M0__) static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 255, 255, 255, 255, 11, 12, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; #elif defined(__MKL26Z64__) static const uint8_t pin2tsi[] = { //0 1 2 3 4 5 6 7 8 9 9, 10, 255, 2, 3, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13, 0, 6, 8, 7, 255, 255, 14, 15, 255, 255, 255 }; #endif // for I_ref=I_ext+1, N_scan=9, Prescale=2, output is approx pF * 50 // time to measure 33 pF is approx 0.25 ms // time to measure 1000 pF is approx 4.5 ms int32_t capRead(uint8_t pin, uint8_t I_ext, uint8_t I_ref, uint8_t Prescale, uint8_t N_scan) { uint32_t ch; int32_t count; if (pin >= NUM_DIGITAL_PINS) return 0;
    ch = pin2tsi[pin];
    if (ch == 255) return 0;

    *portConfigRegister(pin) = PORT_PCR_MUX(0);
    SIM_SCGC5 |= SIM_SCGC5_TSI;
#if defined(KINETISK)
    TSI0_GENCS = 0;
    TSI0_PEN = (1 << ch);
    TSI0_SCANC = TSI_SCANC_REFCHRG(I_ref) | TSI_SCANC_EXTCHRG(I_ext);
    TSI0_GENCS = TSI_GENCS_NSCN(N_scan) | TSI_GENCS_PS(Prescale) | TSI_GENCS_TSIEN | TSI_GENCS_SWTS;
    delayMicroseconds(10);
    while (TSI0_GENCS & TSI_GENCS_SCNIP) ; // wait
    delayMicroseconds(1);
    count= *((volatile uint16_t *)(&TSI0_CNTR1) + ch);
#elif defined(KINETISL)
    TSI0_GENCS = TSI_GENCS_REFCHRG(I_ref) | TSI_GENCS_EXTCHRG(I_ext) | TSI_GENCS_PS(Prescale)
	    | TSI_GENCS_NSCN(N_scan) | TSI_GENCS_TSIEN | TSI_GENCS_EOSF;
    TSI0_DATA = TSI_DATA_TSICH(ch) | TSI_DATA_SWTS;
    delayMicroseconds(10);
    while (TSI0_GENCS & TSI_GENCS_SCNIP) ; // wait
    delayMicroseconds(1);
    count= TSI0_DATA & 0xFFFF;
#endif
    if (debug)
    {
	Serial.print("  I_ext= ");  Serial.print(I_ext);
	Serial.print("  I_ref= ");  Serial.print(I_ref);
	Serial.print("  Prescale= ");  Serial.print(Prescale);
	Serial.print("  N_scan= ");  Serial.print(N_scan);
	Serial.print("  count= ");  Serial.print(count);
	Serial.println();
    }
    return count;
}

2015 September 25

Teensy 3.2 available

Filed under: Circuits course,Data acquisition — gasstationwithoutpumps @ 08:22
Tags: , ,

A new board in the Teensy series, the Teensy 3.2, has just been released.  It is almost identical to the Teensy 3.1, but has a better 3.3V regulator, so that more 3.3V power can be used by peripherals.  We’ll have to make some tiny changes to the PteroDAQ data acquisition system so that it will recognize the Teensy 3.2, but nothing major, as it will use exactly the same code as the Teensy 3.1.  I’ll have to reinstall the Teensyduino development system and find out how (or whether) it distinguishes between the boards.

(Update 2015-Sept-19:00:  The Teensyduino software treats the Teensy 3.1 and Teensy 3.2 identically, so there is nothing that needs to be done to PteroDAQ but to change some info items to be “3.1/3.2” instead of “3.1”.)

The addition of the voltage regulator is a substantial improvement to the board, allowing about 500mA of current on the 3.3V line, rather than the 100mA limit of the Teensy 3.1.

I still think I’ll recommend the Teensy LC for the electronics class, as a somewhat better price/performance ratio for the needs of the class, but the Teensy 3.2 is a good choice if you need a little more processing speed or the more complex DMA capabilities.

2015 September 2

Calibrating PteroDAQ frequency measurements

Filed under: Circuits course,Data acquisition — gasstationwithoutpumps @ 22:35
Tags: , , , ,

In PteroDAQ frequency channels, I wrote

One of the clocks is out of spec.  When the FG085 was putting out a nominal 500kHz signal, the PteroDAQ system measured it at 499958.5 Hz, which is –83ppm.  The two crystals should each be about ±20ppm, so an 83ppm discrepancy is larger than expected. With a nominal 5kHZ signal, the PteroDAQ system measured it at 4999.524 Hz (–95ppm).  I’ll have to take the function generator and PteroDAQ board into the circuits lab and use the frequency counters and function generators there to see what the actual accuracies are.  For anything that we do in the applied electronics class, 100ppm is good enough, but it would be nice to know how far off my instruments really are.

Today I took all three of my PteroDAQ boards (Teensy 3.1, Teensy LC, and FRDM-KL25Z) and my JYEtech FG085 function generator into the circuits lab, to compare the measurements made with the PteroDAQ to measurements made with the bench equipment in the lab.  I also used the function generator in the lab to get higher frequencies with less jitter, to see if that made a difference in the measurement.

The bench instruments I was using were

  • Agilent 34401A multimeter
  • Leader LDC822 frequency counter
  • Tektronix MSO 2024 digital storage oscilloscope
  • Agilent 33120A arbitrary function generator

Some  general observations:

  • The Agilent 33120A multimeter provided the highest precision on the measurements, but failed at 1MHz.
  • The multimeter also had some trouble with pulse trains with a lot of jitter, such as those produced by the FG085.
  • The oscilloscope routine reports the frequency for the inputs, but also has a “frequency” option on the measurement menu. The routine report seems to be stable and precise, while the “measurement” option fluctuated a lot—perhaps tracking the jitter in the signal.

The Leader frequency counter and the Agilent function generator agreed pretty well on frequencies, with the Leader counter reporting that the function generator was about 0.3–0.5ppm lower than it claimed to be.   The oscilloscope had less precision, and reported the Agilent function generator as 3–5ppm high.  The multimeter reported the function generator as 1.4–1.6ppm high.

I’m interested in the PteroDAQ measurements relative to any of the bench instruments.  Here is a table of the ppm error for several frequencies (f_s is the sampling frequency for PteroDAQ looking at the frequency channel):

33120A setting Teensy 3.1 FRDM-KL25Z Teensy LC
MHz /33120A /Leader f_s /33120A /Leader f_s /33120A /Leader f_s
0.5 11.2 11.6 20 Hz  27.4 27.8 1 Hz  -24  -21.6 1 Hz
0.9 11.556 11.889 30 Hz  27.444  27.778 1 Hz  -23.889  -23.556 1 Hz
1.0 9. 9.3 31 Hz  27  27.5 1 Hz  -23.7  -23.5 1 Hz
2.0 -23. -22.5 62 Hz  26.5  27 2 Hz  -25  -25 2 Hz
3.0 -59.667 -59 100 Hz  25.333  26 3 Hz  -25.333  -25 3 Hz
3.9  24.359  24.872 4 Hz  -27.179  -26.923 4 Hz
4.0 -87.75 -87.25 125 Hz  -1660.8  -1660.3 4 Hz  -2059.5  -2059.3 4 Hz
4.5 -1777.555 -1774.667 140 Hz
avg -23.11 -22.66  26.339  26.825  -24.85  -23.263

The entries in red show the frequency counter failing by not counting all the edges—when the DMA channel can’t keep up, it silently ignores edges (not a great failure mode, since the numbers may look plausible while being several percent off). I did not include the red numbers in the average.

The KL25 and KL26 processors (FRDM-KL25Z and Teensy LC) both have fairly sharp transitions between counting every edge (at 3.9MHz) and missing many (at 4MHz). The K20 (Teensy 3.1) has stranger behavior, where it overestimates low frequencies and underestimates high frequencies.  There error a 3MHz is already fairly large.  I suspect that the problem may result from the shorter counter in the K20, which necessitated counting for shorter periods at high frequencies to avoid overflow.  There is a brief dead time at each period while the DMA counter is reset.

If there is a dead time of duration \tau, during which the first pulse is counted (captured by the port circuitry), but subsequent pulses in the dead time are missed, then the relative errors should be f_{s} \min(0, 1- \tau f) /f where f_{s} is the sampling frequency and f is the frequency being measured. Note that \tau f is an estimate of how many pulses arrive during one dead time.  If this is 1 or less, then no pulses are missed, otherwise only one of the arriving pulses is counted.

If the dead time for the Teensy 3.1 is about 900ns, then the errors should be about 0,0,0, -25, -57, and -81 for 0.5MHz, 0.9MHz, 1MHz, 2MHz, 3MHz, and 4MHz, consistent with the observed pattern.  With the longer counters  and so lower sampling frequencies on the other two boards, I expect errors of about 0,0,0,-1,-2,-3 ppm for a 1µs dead time, which is also consistent with the pattern of errors observed.

I could compensate for the error in PteroDAQ (on the host), by adding f_{s}\min(0, \tau f -1) to f.  The small initial error in f makes an insignificant change to \tau f. I will have to get a better estimate of the dead time than 900ns and 1µs, though, perhaps by counting instruction cycles for the compiled code between disabling and re-enabling the DMAMUX.  (Incidentally, do you have any idea how hard it is for a bioinformatician to type “DMA” instead of “DNA”?)

The ±27ppm error for the FRDM-KL25Z and Teensy LC boards seems fairly typical for a cheap crystal oscillator (which is usually ±30ppm).  Those errors are much larger than the <2ppm differences between the bench instruments, so it really doesn’t matter which of the bench instruments we treat as the “standard”.

What about the FG085 function generator? What is the ppm error on it?

FG085 setting 34401A
DMM
Leader LDC822
counter
30Hz  -23.667 to -121  0
3kHz  -88.333  -100
30kHz  -58.667  -63.333
327kHz  -55.352  -58.716
600kHz  -103.33  -58.500
999kHz  -58.158
avg  -76.4  -56.45

The multimeter had some difficult producing consistent reading when there was a lot of jitter in the signal. The frequency counter was more consistent, indicating that the JYEtech FG085 function generator was running about 57ppm slow, which is well outside the normal crystal frequency range. The cheapest crystals are ±30ppm, and ±20ppm often doesn’t cost any more. (There are ±50ppm crystals, but you have to go out of your way to find them, and they aren’t any cheaper.)

There are several possibilities:

  •  an error in the way the firmware for the FG085 sets up the frequencies,
  • I picked particularly bad frequencies to test at, or
  • the crystals are unusually bad.

The FG085 uses a 24-bit phase oscillator with a 2.5MHz clock (except for frequencies below 40Hz, where it uses a 10kHz clock), so the worst case rounding should be for 3kHz, with a phase increment of 20133 instead of 20132.659, making the frequency 17ppm too high.  If they truncated instead of rounding, then the frequency would be 32.7ppm low.  At 30kHz, truncation error would be only 3ppm, though, so this does not explain the consistently high errors.  I think that bad crystals are the simplest explanation—it is certainly consistent with the super-cheap (and bad) design of their DAC (see FG085 function generator bugs).

I’m a little disappointed with FG085 function generator, since for about 50¢ more, they could have made a much better instrument (using a resistor-ladder chip instead of discrete, unmatched resistors; using a ±20ppm crystal; drilling large enough holes in the top so that the buttons didn’t stick).

2015 August 28

DMA counter for event counting on Freescale Kinetis processors

Filed under: Circuits course,Data acquisition — gasstationwithoutpumps @ 18:25
Tags: , , , , , ,

The application note mentioned in PteroDAQ frequency channels is Freescale’s AN5083Using DMA for pulse counting on Kinetis, which I finally got around to looking up and reading today, after implementing frequency channels on the KL26, KL25, and K20 processors (Teensy LC, FRDM-KL25Z, and Teensy 3.1 boards, respectively).  The reason I looked it up finally was to look for a workaround for the biggest problem—namely that the CITER counter on the K20 has a maximum of 32767.  If I’m counting at the highest frequency (supposedly 8MHz or 6.26MHz, depending which documentation I believe, but I’ve not been able to get measurements much above 4MHz), that requires sampling at 125–245Hz or faster. But I often want to sample at factors of 60Hz, to alias out any frequency modulation from line interference.  The cruder DMA systems of the KL25 and KL26 use a 20-bit counter, and should be able to count to 4-5MHz (though they are likely to top out at 3MHz), so I could sample as slowly as once every 3.8s, even at the highest measurable input frequency.

I had no problem getting the counting to work with the 15-bit DMA_TCD_CITER counter. I can live with the 32767 count limit (at 60Hz sampling, that would be a 1.966MHz limit on the counting), but I wanted to see if I could use the optional interrupt that can be set up when the CITER counter ends to increment a software counter and reset the DMA counting.  That should give me an arbitrarily wide counter, at the cost of perhaps missing a count at the wraparound (an error of 30.5ppm, about the same as the clock error).

Unfortunately, AN5083 merely explains what I had already figured out for myself—without even the essential warning that you need a NOP after disabling the DMAMUX before looking at the counter to avoid a race condition on the KL25 and KL26:

    (&DMAMUX0_CHCFG0)[0] &= ~(DMAMUX_ENABLE);       // turn off at DMAMUX
    __asm__ volatile( "nop" );
    uint32_t count = 0xfffff - (DMA_DSR_BCR0 & 0xfffff);

They only say

The major loop counter of the DMA register DMA_TCDn_CITER_ELINKNO is limited to 15 bits, in the case where the channel linking feature of the DMA is not in use (refer to DMA chapter in the Reference Manual). Therefore, there is a limitation of 32K major loops count. So, user must check CITER register and make sure it will not reach to zero. If, CITER reaches zero, BITER will reload the value from BITER register.

I tried for a good chunk of yesterday to get the code working with the interrupt.  I can get the interrupt to occur (detected by turning off an LED in the interrupt return), I can read the longer frequency counter (detected by turning off an LED if the count to return is >32767), and the count is returned to be pushed on the queue, but something gets wedged and that longer count is never output to the host.

The processor needs to be power-cycled, the same as if the processor had gone into an infinite loop (which is what the Teensyduino unimplemented_isr routine does). I did disable the DMA error interrupt routine (with DMA_CEEI), so that isn’t the problem.  I even confirmed this by enabling the error interrupt and providing a dma_error_isr routine that turns off the LED, but the LED did not turn off.  I also checked the DMA_ES register at the beginning and end of the dma_ch0_isr routine and in the routine that reads the counter, and DMA_ES was always 0.  So if there is an error, it is not being reported by the hardware.

I suppose that an SWD debugging interface (not available on the Teensy 3.1 board) might be useful here, as I could at least check my conjecture that the processor is looping in the fault_isr routine.  It may be wedged somewhere else.  And there is always the possibility that there is an undocumented hardware bug (like the race condition between DMAMUX disabling and reading DMA_DSR_BCR on the KL26) that requires some workaround that I haven’t thought of yet.

I’ve given up on the higher resolution counters for Teensy 3.1 for now and just pushed the 15-bit implementation (with the interrupt code mostly commented out) to the BitBucket PteroDAQ repository.  If anyone reading this blog is familiar with the DMA interrupts on the K20 processor, please look at the code in kinetis_frequency.c and tell me what I’m doing wrong!

Next Page »

Blog at WordPress.com.

%d bloggers like this: