Difference between revisions of "Frequency meter on Arduino"

From ShawnReevesWiki
Jump to navigationJump to search
(Created page with "==Arduino-based frequency meter== The following program directs an Arduino or compatible micro-controller to time a signal (falling voltage) on an input pin and report the inv...")
 
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
 
==Arduino-based frequency meter==
 
==Arduino-based frequency meter==
 
The following program directs an Arduino or compatible micro-controller to time a signal (falling voltage) on an input pin and report the inverse of the time between previous signals as a frequency in Hz, in a range of 100.0Hz to 0.10Hz. Used with the circuit diagrammed in [[File:Frequency-meter-Arduino-on-a-board.pdf]].
 
The following program directs an Arduino or compatible micro-controller to time a signal (falling voltage) on an input pin and report the inverse of the time between previous signals as a frequency in Hz, in a range of 100.0Hz to 0.10Hz. Used with the circuit diagrammed in [[File:Frequency-meter-Arduino-on-a-board.pdf]].
 +
 +
For a discussion of the circuit and its function, please see [[frequency meter]].
 
===Code===
 
===Code===
/*
+
<source lang="c">
On timing:
+
/*
micros() returns the number of microseconds since the Arduino board began running the current program.
+
On timing:
micros() returns an unsigned long integer, so it overflows to zero after 4,294,967,295 µs.
+
micros() returns the number of microseconds since the Arduino board began running the current program.
millis() is similar except it counts milliseconds.
+
micros() returns an unsigned long integer, so it overflows to zero after 4,294,967,295 µs.
NOTA BENE:Both millis() and micros() clear interrupts while they read the timer, so use them sparingly!
+
millis() is similar except it counts milliseconds.
+
NOTA BENE:Both millis() and micros() clear interrupts while they read the timer, so use them sparingly!
On LCD quirks:
+
 
Clearing the display with the clear/home command takes a lot of time.
+
On LCD quirks:
We might consider leaving characters in memory and just overwriting them.
+
Clearing the display with the clear/home command takes a lot of time.
--We'd just need to be sure to write spaces at the end if one printout might be shorter than the previous printout.
+
We might consider leaving characters in memory and just overwriting them.
+
--We'd just need to be sure to write spaces at the end if one printout might be shorter than the previous printout.
On interrupt routine:
+
 
attachInterrupt(interrupt, ISR, mode)
+
On interrupt routine:
  interrupt: 0 means digital pin 2 and 1 means digital pin 3. (Mega2560, Leonardo, and Due boards offer it on more pins)
+
attachInterrupt(interrupt, ISR, mode)
  ISR: routine to call when interrupt happens.
+
  interrupt: 0 means digital pin 2 and 1 means digital pin 3. (Mega2560, Leonardo, and Due boards offer it on more pins)
  mode: LOW, CHANGE, RISING, FALLING (DUE boards also allow HIGH)
+
  ISR: routine to call when interrupt happens.
*/
+
  mode: LOW, CHANGE, RISING, FALLING (DUE boards also allow HIGH)
//Uncomment the following to allow serial debug messages. May interfere with timing?
+
*/
//#define serialDebug 1
+
//Uncomment the following to allow serial debug messages. May interfere with timing?
//Define NOP as an assembly language macro to delay one cycle. See http://forum.arduino.cc/index.php?topic=43333.0
+
//#define serialDebug 1
#define NOP __asm__ __volatile__ ("nop\n\t")
+
//Define NOP as an assembly language macro to delay one cycle. See http://forum.arduino.cc/index.php?topic=43333.0
// and then use it in code as follows
+
#define NOP __asm__ __volatile__ ("nop\n\t")
//NOP; // delay 62.5ns on a 16MHz AtMega
+
// and then use it in code as follows
+
//NOP; // delay 62.5ns on a 16MHz AtMega
//HARDWARE PIN SELECTION:
+
 
//The pins are selected to minimize spaghetti-crossing on the breadboard, and so are dependent on the microcontroller's physical config.
+
//HARDWARE PIN SELECTION:
const int switchPin = 3;//must be pin 2 or 3
+
//The pins are selected to minimize spaghetti-crossing on the breadboard, and so are dependent on the microcontroller's physical config.
const int switchPinInt = 1;//for 328-based arduinos, 0 means digital I/O pin 2, and 1 means digital I/O pin 3
+
const int switchPin = 3;//must be pin 2 or 3
//We're using home-made SPI instead of the standard library since our LCD (EA DOGM081) uses its own flavor of SPI
+
const int switchPinInt = 1;//for 328-based arduinos, 0 means digital I/O pin 2, and 1 means digital I/O pin 3
//The following pins depend on how the DOGM081 and the mircro-controller are connected
+
//We're using home-made SPI instead of the standard library since our LCD (EA DOGM081) uses its own flavor of SPI
const int DATAP = 9;  //to SI on the DOGM081
+
//The following pins depend on how the DOGM081 and the mircro-controller are connected
const int CLKP = 10;  //to CLK on the DOGM081
+
const int DATAP = 9;  //to SI on the DOGM081
const int chipSelectPin = 11;  //to CSB on the DOGM081
+
const int CLKP = 10;  //to CLK on the DOGM081
const int RSPin = 12;  //to RS on the DOGM081
+
const int chipSelectPin = 11;  //to CSB on the DOGM081
+
const int RSPin = 12;  //to RS on the DOGM081
//CONDITIONAL, CONSTANT PARAMETERS:
+
 
const unsigned long debounceMicroSeconds = 100;//Our routine will ignore signals that come closer together than this amount of microseconds
+
//CONDITIONAL, CONSTANT PARAMETERS:
const unsigned long maxTime = 11000000;//The amount of microseconds before going into waiting mode.
+
const unsigned long debounceMicroSeconds = 100;//Our routine will ignore signals that come closer together than this amount of microseconds
+
const unsigned long maxTime = 11000000;//The amount of microseconds before going into waiting mode.
//
+
 
//It's recommended all variables used in the interrupt service routine are considered volatile,
+
//
// so not kept in multi-purpose registers but in dedicated memory slots:
+
//It's recommended all variables used in the interrupt service routine are considered volatile,
volatile unsigned long hitTime;
+
// so not kept in multi-purpose registers but in dedicated memory slots:
unsigned long lastTime;//Track the time of the previous signal so we can calculate time elapsed since.
+
volatile unsigned long hitTime;
unsigned long microsSince;//for determining idleness. 0 <= ul <= 4,294,967,295
+
unsigned long lastTime;//Track the time of the previous signal so we can calculate time elapsed since.
unsigned long periodMicroSeconds;
+
unsigned long microsSince;//for determining idleness. 0 <= ul <= 4,294,967,295
float frequencyHz;//we use floating point math for maximum precision.
+
unsigned long periodMicroSeconds;
word frequencycHz;//this we will convert to a string to display.
+
float frequencyHz;//we use floating point math for maximum precision.
byte thisDigit;
+
word frequencycHz;//this we will convert to a string to display.
byte precision;  //to keep track of roughly how precise the frequency will be.
+
byte thisDigit;
byte updateDisplay;  //True this byte when display should be updated.
+
byte precision;  //to keep track of roughly how precise the frequency will be.
byte trackDigits;  //track whether display of significant digits has started.
+
byte updateDisplay;  //True this byte when display should be updated.
word delayCounter;//can count 0 to 65535
+
byte trackDigits;  //track whether display of significant digits has started.
+
word delayCounter;//can count 0 to 65535
void recordTime(){//this is the interrupt service routine to be called whenever switchPin's voltage falls
+
 
//we want as few instructions and variables inside this routine as possible, so we just gather the time and go.
+
void recordTime(){//this is the interrupt service routine to be called whenever switchPin's voltage falls
  hitTime = micros();//reads a clock in microseconds
+
//we want as few instructions and variables inside this routine as possible, so we just gather the time and go.
}
+
  hitTime = micros();//reads a clock in microseconds
void sendToDisplay(byte byteToSend, word microsecondsToDelay ){
+
}
  for (int shiftCounter = 0; shiftCounter < 8; shiftCounter++) {//repeat 8 times
+
void sendToDisplay(byte byteToSend, word microsecondsToDelay ){
    if (byteToSend > 127) {//get most significant bit. If a byte is over 127, then it must start with a one.
+
  for (int shiftCounter = 0; shiftCounter < 8; shiftCounter++) {//repeat 8 times
      digitalWrite(DATAP,HIGH);//Represent one, or on
+
    if (byteToSend > 127) {//get most significant bit. If a byte is over 127, then it must start with a one.
    } else {
+
      digitalWrite(DATAP,HIGH);//Represent one, or on
      digitalWrite(DATAP,LOW);//Represent zero, or off
+
    } else {
    }
+
      digitalWrite(DATAP,LOW);//Represent zero, or off
    //data setup time, the minimum time the display chip needs the data pin to be stable before the clock rises, is 10ns
+
    }
    NOP; // Do nothing for one cycle, delaying 62.5ns on a 16MHz AtMega
+
    //data setup time, the minimum time the display chip needs the data pin to be stable before the clock rises, is 10ns
    digitalWrite(CLKP,HIGH);//raise voltage on clock pin, forcing display chip to read from the data pin.
+
    NOP; // Do nothing for one cycle, delaying 62.5ns on a 16MHz AtMega
    //data hold time, the maximum time the display chip might need to read the data after the clock rises, is 20ns
+
    digitalWrite(CLKP,HIGH);//raise voltage on clock pin, forcing display chip to read from the data pin.
    NOP; // delay 62.5ns on a 16MHz AtMega
+
    //data hold time, the maximum time the display chip might need to read the data after the clock rises, is 20ns
    digitalWrite(CLKP,LOW);//get ready for next time
+
    NOP; // delay 62.5ns on a 16MHz AtMega
    for (int clockDelay=16;clockDelay>0;clockDelay--){//repeat 16 times
+
    digitalWrite(CLKP,LOW);//get ready for next time
      NOP; // delay 62.5ns on a 16MHz AtMega
+
    for (int clockDelay=16;clockDelay>0;clockDelay--){//repeat 16 times
    }
+
      NOP; // delay 62.5ns on a 16MHz AtMega
    byteToSend *=2;//line up second-most significant bit as the most significant bit
+
    }
  }
+
    byteToSend *=2;//line up second-most significant bit as the most significant bit
//  shiftOut(DATAP, CLKP, MSBFIRST, byteToSend);//this built-in library was working extremely slowly (~1Hz).
+
  }
  while(microsecondsToDelay-- > 0){//If microsecondsToDelay is above zero, do the following. Also decrement microsecondsToDelay by one.
+
//  shiftOut(DATAP, CLKP, MSBFIRST, byteToSend);//this built-in library was working extremely slowly (~1Hz).
    NOP; // delay 62.5ns on a 16MHz AtMega
+
  while(microsecondsToDelay-- > 0){//If microsecondsToDelay is above zero, do the following. Also decrement microsecondsToDelay by one.
    NOP; //By using 15 of these, we delay a predictable microsecond. A loop would be neater but probably less precise
+
    NOP; // delay 62.5ns on a 16MHz AtMega
    NOP; //But to be accurate we'd need to know how long it takes to execute the while statement each time.
+
    NOP; //By using 15 of these, we delay a predictable microsecond. A loop would be neater but probably less precise
    NOP;
+
    NOP; //But to be accurate we'd need to know how long it takes to execute the while statement each time.
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
    NOP;
+
    NOP;
  }
+
    NOP;
}
+
  }
void waitingMode(){
+
}
  precision = 0;//We use 'precision' as a flag to mark whether we've already made one clock-reading.
+
void waitingMode(){
  digitalWrite(RSPin, LOW);  //about to issue a command
+
  precision = 0;//We use 'precision' as a flag to mark whether we've already made one clock-reading.
  for (int clockDelay=160;clockDelay>0;clockDelay--){//wait ten microseconds
+
  digitalWrite(RSPin, LOW);  //about to issue a command
    NOP; // delay 62.5ns on a 16MHz AtMega
+
  for (int clockDelay=160;clockDelay>0;clockDelay--){//wait ten microseconds
  }
+
    NOP; // delay 62.5ns on a 16MHz AtMega
  sendToDisplay(B00000001, 0);  //clear display and go home
+
  }
  for (int clockDelay=1600;clockDelay>0;clockDelay--){//wait a hundred microseconds
+
  sendToDisplay(B00000001, 0);  //clear display and go home
    NOP; // delay 62.5ns on a 16MHz AtMega
+
  for (int clockDelay=1600;clockDelay>0;clockDelay--){//wait a hundred microseconds
  }
+
    NOP; // delay 62.5ns on a 16MHz AtMega
  digitalWrite(RSPin, HIGH);  //about to issue data
+
  }
  //Address setup time is supposed to be 10 µs, but the following W goes missing without the following 100 microsecond delay.
+
  digitalWrite(RSPin, HIGH);  //about to issue data
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
+
  //Address setup time is supposed to be 10 µs, but the following W goes missing without the following 100 microsecond delay.
    NOP; // delay 62.5ns on a 16MHz AtMega
+
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
  }
+
    NOP; // delay 62.5ns on a 16MHz AtMega
  sendToDisplay('W',0);//The compiler interprets 'W' as the ascii value for W, B01010111.
+
  }
  sendToDisplay('a',0);
+
  sendToDisplay('W',0);//The compiler interprets 'W' as the ascii value for W, B01010111.
  sendToDisplay('i',0);
+
  sendToDisplay('a',0);
  sendToDisplay('t',0);
+
  sendToDisplay('i',0);
  sendToDisplay('i',0);
+
  sendToDisplay('t',0);
  sendToDisplay('n',0);
+
  sendToDisplay('i',0);
  sendToDisplay('g',0);
+
  sendToDisplay('n',0);
  #ifdef serialDebug//Only do the following if serialDebug is defined above.
+
  sendToDisplay('g',0);
    Serial.println("Waiting");
+
  #ifdef serialDebug//Only do the following if serialDebug is defined above.
  #endif
+
    Serial.println("Waiting");
}
+
  #endif
+
}
void setup(){//This is the routine done only once when the board is reset, or first receives power.
+
 
  pinMode(DATAP, OUTPUT);//Prepare these pins to assert a voltage
+
void setup(){//This is the routine done only once when the board is reset, or first receives power.
  pinMode(CLKP, OUTPUT);
+
  pinMode(DATAP, OUTPUT);//Prepare these pins to assert a voltage
  pinMode(chipSelectPin, OUTPUT);
+
  pinMode(CLKP, OUTPUT);
  pinMode(RSPin, OUTPUT);
+
  pinMode(chipSelectPin, OUTPUT);
  pinMode(switchPin, INPUT_PULLUP);//get switchPin ready for interrupt input, activating (internal) pullup resistor Hall Switch expects.
+
  pinMode(RSPin, OUTPUT);
+
  pinMode(switchPin, INPUT_PULLUP);//get switchPin ready for interrupt input, activating (internal) pullup resistor Hall Switch expects.
  #ifdef serialDebug//Only do the following if serialDebug is defined above.
+
 
    // initialize serial communication at 9600 bits per second:
+
  #ifdef serialDebug//Only do the following if serialDebug is defined above.
    Serial.begin(9600);//prepare the serial lines to use 9600 baud
+
    // initialize serial communication at 9600 bits per second:
  #endif
+
    Serial.begin(9600);//prepare the serial lines to use 9600 baud
+
  #endif
  delay(500);  //Wait half a second for display to power.
+
 
  digitalWrite(chipSelectPin, HIGH);//Set high to make sure CSB has fallen once before beginning commands,
+
  delay(500);  //Wait half a second for display to power.
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
+
  digitalWrite(chipSelectPin, HIGH);//Set high to make sure CSB has fallen once before beginning commands,
    NOP; // delay 62.5ns on a 16MHz AtMega
+
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
  }   
+
    NOP; // delay 62.5ns on a 16MHz AtMega
  digitalWrite(CLKP, LOW);//DOGM081 expects to take in data on rising clock.
+
  }   
  digitalWrite(chipSelectPin, LOW);//and keep it low, unless you need the other pins to do something not with this display
+
  digitalWrite(CLKP, LOW);//DOGM081 expects to take in data on rising clock.
  digitalWrite(RSPin, LOW);//About to issue commands
+
  digitalWrite(chipSelectPin, LOW);//and keep it low, unless you need the other pins to do something not with this display
  //The following lines from Electronic Assembly's data sheet, with cursor mode altered.
+
  digitalWrite(RSPin, LOW);//About to issue commands
  //One might repeat the following command many times to get the attention of the ST7032 driver
+
  //The following lines from Electronic Assembly's data sheet, with cursor mode altered.
  sendToDisplay(B00110000, 27); //Function Set, 1 line, instruction table 0
+
  //One might repeat the following command many times to get the attention of the ST7032 driver
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1
+
  sendToDisplay(B00110000, 27); //Function Set, 1 line, instruction table 0
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1, repeat request recommended by ST7032 datasheet
+
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1
  sendToDisplay(B00011100, 27); //Bias 1/4, 1 line LCD
+
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1, repeat request recommended by ST7032 datasheet
  sendToDisplay(B01010001, 27); //Booster off, set contrast C5, C4
+
  sendToDisplay(B00011100, 27); //Bias 1/4, 1 line LCD
  sendToDisplay(B01101010, 0); //set voltage follower on, gain 2/7
+
  sendToDisplay(B01010001, 27); //Booster off, set contrast C5, C4
  delay(200);//recommended by ST7032 datasheet
+
  sendToDisplay(B01101010, 0); //set voltage follower on, gain 2/7
  sendToDisplay(B01110100, 27); //set contrast C3, C2, C1
+
  delay(200);//recommended by ST7032 datasheet
  sendToDisplay(B00001100, 27); //display on, cursor off, blink cursor position
+
  sendToDisplay(B01110100, 27); //set contrast C3, C2, C1
  sendToDisplay(B00000001, 3); //clear display, home cursor
+
  sendToDisplay(B00001100, 27); //display on, cursor off, blink cursor position
  delay(2);//takes over a millisecond to clear
+
  sendToDisplay(B00000001, 3); //clear display, home cursor
  sendToDisplay(B00000110, 2); //entry from right to left, no scrolling.
+
  delay(2);//takes over a millisecond to clear
  waitingMode();
+
  sendToDisplay(B00000110, 2); //entry from right to left, no scrolling.
  attachInterrupt(switchPinInt, recordTime, FALLING);//Go to recordTime whenever interrupt switchPinInt goes from high to low
+
  waitingMode();
}//Done setting up
+
  attachInterrupt(switchPinInt, recordTime, FALLING);//Go to recordTime whenever interrupt switchPinInt goes from high to low
+
}//Done setting up
void loop() {
 
  if(hitTime>0){//There's a 1 in 4 billion chance hitTime is zero but represents an actual reading.
 
    if (0==precision) {//this is first time switch, so no reference, so only set precision flag and record time.
 
      precision = 1;//so we don't end up here next time
 
      lastTime = hitTime;//record now for reference next time.
 
    }
 
    else
 
    { periodMicroSeconds = hitTime - lastTime; //if timer has overflowed once since last time, ok since this subtraction will overflow back.
 
      if (periodMicroSeconds > debounceMicroSeconds) {//debounce by rejecting too-fast times
 
        precision = 1;//Set our flag so we know we have a valid period
 
        updateDisplay = 1;//We have a new period, so let's update the display
 
        lastTime = hitTime;//record now for reference next time.
 
      }
 
    }
 
    hitTime=0;//skip the above routine until another hit.
 
  }
 
  //We don't want to read micros every trip through this loop--Reading micros causes interrupt issues.
 
  // So, if we increase a 16-bit (word) variable by one every trip, it will only overflow back to zero once every 65536 (2^16) trips.
 
  if(0 == delayCounter++){
 
    microsSince = micros() - lastTime;//Keep track of idleness.
 
  }
 
  if(precision > 0 && microsSince > maxTime){//It's been more than maxTime since the last hit, so go back into waiting mode
 
    waitingMode();
 
  }
 
  if (updateDisplay) {
 
    updateDisplay = 0;//Don't come back here until next update is requested by changing this flag back to 1
 
    frequencyHz = 1000000.0 / (float)periodMicroSeconds;//float math for best precision over a wide range.
 
    if (periodMicroSeconds>20) {precision = 2;}//Only when a rounded divisor is about 20 does the quotient have any meaning past the first digit.
 
    if (periodMicroSeconds>200) {precision = 3;}
 
    if (periodMicroSeconds>2000) {precision = 4;}
 
    if (periodMicroSeconds>20000) {precision = 5;}
 
#ifdef serialDebug
 
    Serial.println(periodMicroSeconds);// print the value you read:
 
#endif
 
    digitalWrite(RSPin, LOW);  //about to issue a command
 
    sendToDisplay(B00000001, 0);  //clear display and go home
 
    //wait a long time for display to clear.
 
    for (int clockDelay=3200;clockDelay>0;clockDelay--){
 
      NOP; // delay 62.5ns on a 16MHz AtMega
 
    }
 
    digitalWrite(RSPin, HIGH);  //about to issue data
 
    frequencyHz *= 100.0;  //Temporarily, if you uncomment the re-conversion below, convert to cHz.
 
    frequencycHz = (word)frequencyHz;//round to a number between 0 and 65535
 
    //frequencyHz /= 100.0;  //Convert back to Hz in case we ever need it.
 
    trackDigits = 0;//We're going to display the correct precision by tracking significant digits
 
    //The highest a word integer can be is 65535, so the most significant digit we could possibly have is the ten thousands digit.
 
    thisDigit = frequencycHz / 10000;//get ten-thousands digit. This is integer math, so no fractions in result.
 
    if(thisDigit > 0){
 
      trackDigits = 1;  //start tracking significant digits
 
      thisDigit +=48; //convert to ASCII
 
      sendToDisplay(thisDigit, 0);
 
    }
 
    frequencycHz = frequencycHz % 10000;//discard ten-thousands by modulo arithmetic
 
    thisDigit = frequencycHz / 1000;//get thousands digit
 
    if(trackDigits){
 
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
 
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
 
      } else {
 
        thisDigit +=48; //convert to ASCII
 
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 
      }
 
    } else {
 
      if(thisDigit > 0){
 
        trackDigits = 1;  //start tracking significant digits
 
        thisDigit +=48; //convert to ASCII
 
        sendToDisplay(thisDigit, 0);
 
      }
 
    }
 
    frequencycHz = frequencycHz % 1000;//discard thousands
 
    thisDigit = frequencycHz / 100;//get hundreds digit
 
    if(trackDigits){
 
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
 
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
 
      } else {
 
        thisDigit +=48; //convert to ASCII
 
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 
      }
 
    } else {
 
      if(thisDigit > 0){
 
        trackDigits = 1;  //start tracking significant digits
 
      }
 
      thisDigit +=48; //convert to ASCII
 
      sendToDisplay(thisDigit, 0);//this time, even if zero, we display, since placeholder needed for decimal point.
 
    }
 
    sendToDisplay('.', 0);//send a decimal point
 
    frequencycHz = frequencycHz % 100;//discard hundreds
 
    thisDigit = frequencycHz / 10;//get tens digit
 
    if(trackDigits){
 
      if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
 
        thisDigit +=48; //convert to ASCII
 
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 
      }
 
    } else {
 
      if(thisDigit > 0){
 
        trackDigits = 1;  //start tracking significant digits
 
      }
 
      thisDigit +=48; //convert to ASCII
 
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
 
    }
 
    thisDigit = frequencycHz % 10;//get ones digit
 
    if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
 
      thisDigit +=48; //convert to ASCII
 
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
 
    }
 
    sendToDisplay('H', 0);//always show units if they exist
 
    sendToDisplay('z', 0);
 
  }
 
}
 
  
 +
void loop() {
 +
  if(hitTime>0){//There's a 1 in 4 billion chance hitTime is zero but represents an actual reading.
 +
    if (0==precision) {//this is first time switch, so no reference, so only set precision flag and record time.
 +
      precision = 1;//so we don't end up here next time
 +
      lastTime = hitTime;//record now for reference next time.
 +
    }
 +
    else
 +
    { periodMicroSeconds = hitTime - lastTime; //if timer has overflowed once since last time, ok since this subtraction will overflow back.
 +
      if (periodMicroSeconds > debounceMicroSeconds) {//debounce by rejecting too-fast times
 +
        precision = 1;//Set our flag so we know we have a valid period
 +
        updateDisplay = 1;//We have a new period, so let's update the display
 +
        lastTime = hitTime;//record now for reference next time.
 +
      }
 +
    }
 +
    hitTime=0;//skip the above routine until another hit.
 +
  }
 +
  //We don't want to read micros every trip through this loop--Reading micros causes interrupt issues.
 +
  // So, if we increase a 16-bit (word) variable by one every trip, it will only overflow back to zero once every 65536 (2^16) trips.
 +
  if(0 == delayCounter++){
 +
    microsSince = micros() - lastTime;//Keep track of idleness.
 +
  }
 +
  if(precision > 0 && microsSince > maxTime){//It's been more than maxTime since the last hit, so go back into waiting mode
 +
    waitingMode();
 +
  }
 +
  if (updateDisplay) {
 +
    updateDisplay = 0;//Don't come back here until next update is requested by changing this flag back to 1
 +
    frequencyHz = 1000000.0 / (float)periodMicroSeconds;//float math for best precision over a wide range.
 +
    if (periodMicroSeconds>20) {precision = 2;}//Only when a rounded divisor is about 20 does the quotient have any meaning past the first digit.
 +
    if (periodMicroSeconds>200) {precision = 3;}
 +
    if (periodMicroSeconds>2000) {precision = 4;}
 +
    if (periodMicroSeconds>20000) {precision = 5;}
 +
#ifdef serialDebug
 +
    Serial.println(periodMicroSeconds);// print the value you read:
 +
#endif
 +
    digitalWrite(RSPin, LOW);  //about to issue a command
 +
    sendToDisplay(B00000001, 0);  //clear display and go home
 +
    //wait a long time for display to clear.
 +
    for (int clockDelay=3200;clockDelay>0;clockDelay--){
 +
      NOP; // delay 62.5ns on a 16MHz AtMega
 +
    }
 +
    digitalWrite(RSPin, HIGH);  //about to issue data
 +
    frequencyHz *= 100.0;  //Temporarily, if you uncomment the re-conversion below, convert to cHz.
 +
    frequencycHz = (word)frequencyHz;//round to a number between 0 and 65535
 +
    //frequencyHz /= 100.0;  //Convert back to Hz in case we ever need it.
 +
    trackDigits = 0;//We're going to display the correct precision by tracking significant digits
 +
    //The highest a word integer can be is 65535, so the most significant digit we could possibly have is the ten thousands digit.
 +
    thisDigit = frequencycHz / 10000;//get ten-thousands digit. This is integer math, so no fractions in result.
 +
    if(thisDigit > 0){
 +
      trackDigits = 1;  //start tracking significant digits
 +
      thisDigit +=48; //convert to ASCII
 +
      sendToDisplay(thisDigit, 0);
 +
    }
 +
    frequencycHz = frequencycHz % 10000;//discard ten-thousands by modulo arithmetic
 +
    thisDigit = frequencycHz / 1000;//get thousands digit
 +
    if(trackDigits){
 +
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
 +
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
 +
      } else {
 +
        thisDigit +=48; //convert to ASCII
 +
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 +
      }
 +
    } else {
 +
      if(thisDigit > 0){
 +
        trackDigits = 1;  //start tracking significant digits
 +
        thisDigit +=48; //convert to ASCII
 +
        sendToDisplay(thisDigit, 0);
 +
      }
 +
    }
 +
    frequencycHz = frequencycHz % 1000;//discard thousands
 +
    thisDigit = frequencycHz / 100;//get hundreds digit
 +
    if(trackDigits){
 +
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
 +
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
 +
      } else {
 +
        thisDigit +=48; //convert to ASCII
 +
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 +
      }
 +
    } else {
 +
      if(thisDigit > 0){
 +
        trackDigits = 1;  //start tracking significant digits
 +
      }
 +
      thisDigit +=48; //convert to ASCII
 +
      sendToDisplay(thisDigit, 0);//this time, even if zero, we display, since placeholder needed for decimal point.
 +
    }
 +
    sendToDisplay('.', 0);//send a decimal point
 +
    frequencycHz = frequencycHz % 100;//discard hundreds
 +
    thisDigit = frequencycHz / 10;//get tens digit
 +
    if(trackDigits){
 +
      if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
 +
        thisDigit +=48; //convert to ASCII
 +
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
 +
      }
 +
    } else {
 +
      if(thisDigit > 0){
 +
        trackDigits = 1;  //start tracking significant digits
 +
      }
 +
      thisDigit +=48; //convert to ASCII
 +
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
 +
    }
 +
    thisDigit = frequencycHz % 10;//get ones digit
 +
    if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
 +
      thisDigit +=48; //convert to ASCII
 +
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
 +
    }
 +
    sendToDisplay('H', 0);//always show units if they exist
 +
    sendToDisplay('z', 0);
 +
  }
 +
}
 +
</source>
 
[[Category:Electronics]]
 
[[Category:Electronics]]

Latest revision as of 13:51, 23 May 2014

Arduino-based frequency meter

The following program directs an Arduino or compatible micro-controller to time a signal (falling voltage) on an input pin and report the inverse of the time between previous signals as a frequency in Hz, in a range of 100.0Hz to 0.10Hz. Used with the circuit diagrammed in File:Frequency-meter-Arduino-on-a-board.pdf.

For a discussion of the circuit and its function, please see frequency meter.

Code

/*
On timing:
micros() returns the number of microseconds since the Arduino board began running the current program.
micros() returns an unsigned long integer, so it overflows to zero after 4,294,967,295 µs.
millis() is similar except it counts milliseconds.
NOTA BENE:Both millis() and micros() clear interrupts while they read the timer, so use them sparingly!

On LCD quirks:
Clearing the display with the clear/home command takes a lot of time.
We might consider leaving characters in memory and just overwriting them.
--We'd just need to be sure to write spaces at the end if one printout might be shorter than the previous printout.

On interrupt routine:
attachInterrupt(interrupt, ISR, mode)
  interrupt: 0 means digital pin 2 and 1 means digital pin 3. (Mega2560, Leonardo, and Due boards offer it on more pins)
  ISR: routine to call when interrupt happens.
  mode: LOW, CHANGE, RISING, FALLING (DUE boards also allow HIGH)
*/
//Uncomment the following to allow serial debug messages. May interfere with timing?
//#define serialDebug 1
//Define NOP as an assembly language macro to delay one cycle. See http://forum.arduino.cc/index.php?topic=43333.0
#define NOP __asm__ __volatile__ ("nop\n\t")
// and then use it in code as follows
//NOP; // delay 62.5ns on a 16MHz AtMega

//HARDWARE PIN SELECTION:
//The pins are selected to minimize spaghetti-crossing on the breadboard, and so are dependent on the microcontroller's physical config.
const int switchPin = 3;//must be pin 2 or 3
const int switchPinInt = 1;//for 328-based arduinos, 0 means digital I/O pin 2, and 1 means digital I/O pin 3
//We're using home-made SPI instead of the standard library since our LCD (EA DOGM081) uses its own flavor of SPI
//The following pins depend on how the DOGM081 and the mircro-controller are connected
const int DATAP = 9;  //to SI on the DOGM081
const int CLKP = 10;  //to CLK on the DOGM081
const int chipSelectPin = 11;  //to CSB on the DOGM081
const int RSPin = 12;  //to RS on the DOGM081

//CONDITIONAL, CONSTANT PARAMETERS:
const unsigned long debounceMicroSeconds = 100;//Our routine will ignore signals that come closer together than this amount of microseconds
const unsigned long maxTime = 11000000;//The amount of microseconds before going into waiting mode.

//
//It's recommended all variables used in the interrupt service routine are considered volatile,
// so not kept in multi-purpose registers but in dedicated memory slots:
volatile unsigned long hitTime;
unsigned long lastTime;//Track the time of the previous signal so we can calculate time elapsed since.
unsigned long microsSince;//for determining idleness. 0 <= ul <= 4,294,967,295
unsigned long periodMicroSeconds;
float frequencyHz;//we use floating point math for maximum precision.
word frequencycHz;//this we will convert to a string to display.
byte thisDigit;
byte precision;  //to keep track of roughly how precise the frequency will be.
byte updateDisplay;  //True this byte when display should be updated.
byte trackDigits;  //track whether display of significant digits has started.
word delayCounter;//can count 0 to 65535

void recordTime(){//this is the interrupt service routine to be called whenever switchPin's voltage falls
//we want as few instructions and variables inside this routine as possible, so we just gather the time and go.
  hitTime = micros();//reads a clock in microseconds
}
void sendToDisplay(byte byteToSend, word microsecondsToDelay ){
  for (int shiftCounter = 0; shiftCounter < 8; shiftCounter++) {//repeat 8 times
    if (byteToSend > 127) {//get most significant bit. If a byte is over 127, then it must start with a one.
      digitalWrite(DATAP,HIGH);//Represent one, or on
    } else {
      digitalWrite(DATAP,LOW);//Represent zero, or off
    }
    //data setup time, the minimum time the display chip needs the data pin to be stable before the clock rises, is 10ns
    NOP; // Do nothing for one cycle, delaying 62.5ns on a 16MHz AtMega
    digitalWrite(CLKP,HIGH);//raise voltage on clock pin, forcing display chip to read from the data pin.
    //data hold time, the maximum time the display chip might need to read the data after the clock rises, is 20ns
    NOP; // delay 62.5ns on a 16MHz AtMega
    digitalWrite(CLKP,LOW);//get ready for next time
    for (int clockDelay=16;clockDelay>0;clockDelay--){//repeat 16 times
      NOP; // delay 62.5ns on a 16MHz AtMega
    }
    byteToSend *=2;//line up second-most significant bit as the most significant bit
  }
//  shiftOut(DATAP, CLKP, MSBFIRST, byteToSend);//this built-in library was working extremely slowly (~1Hz).
  while(microsecondsToDelay-- > 0){//If microsecondsToDelay is above zero, do the following. Also decrement microsecondsToDelay by one.
    NOP; // delay 62.5ns on a 16MHz AtMega
    NOP; //By using 15 of these, we delay a predictable microsecond. A loop would be neater but probably less precise
    NOP; //But to be accurate we'd need to know how long it takes to execute the while statement each time.
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
    NOP;
  }
}
void waitingMode(){
  precision = 0;//We use 'precision' as a flag to mark whether we've already made one clock-reading.
  digitalWrite(RSPin, LOW);  //about to issue a command
  for (int clockDelay=160;clockDelay>0;clockDelay--){//wait ten microseconds
    NOP; // delay 62.5ns on a 16MHz AtMega
  }
  sendToDisplay(B00000001, 0);  //clear display and go home
  for (int clockDelay=1600;clockDelay>0;clockDelay--){//wait a hundred microseconds
    NOP; // delay 62.5ns on a 16MHz AtMega
  }
  digitalWrite(RSPin, HIGH);  //about to issue data
  //Address setup time is supposed to be 10 µs, but the following W goes missing without the following 100 microsecond delay.
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
    NOP; // delay 62.5ns on a 16MHz AtMega
  }
  sendToDisplay('W',0);//The compiler interprets 'W' as the ascii value for W, B01010111.
  sendToDisplay('a',0);
  sendToDisplay('i',0);
  sendToDisplay('t',0);
  sendToDisplay('i',0);
  sendToDisplay('n',0);
  sendToDisplay('g',0);
  #ifdef serialDebug//Only do the following if serialDebug is defined above.
    Serial.println("Waiting");
  #endif
}

void setup(){//This is the routine done only once when the board is reset, or first receives power.
  pinMode(DATAP, OUTPUT);//Prepare these pins to assert a voltage
  pinMode(CLKP, OUTPUT);
  pinMode(chipSelectPin, OUTPUT);
  pinMode(RSPin, OUTPUT);
  pinMode(switchPin, INPUT_PULLUP);//get switchPin ready for interrupt input, activating (internal) pullup resistor Hall Switch expects.

  #ifdef serialDebug//Only do the following if serialDebug is defined above.
    // initialize serial communication at 9600 bits per second:
    Serial.begin(9600);//prepare the serial lines to use 9600 baud
  #endif

  delay(500);  //Wait half a second for display to power.
  digitalWrite(chipSelectPin, HIGH);//Set high to make sure CSB has fallen once before beginning commands,
  for (int clockDelay=1600;clockDelay>0;clockDelay--){
    NOP; // delay 62.5ns on a 16MHz AtMega
  }  
  digitalWrite(CLKP, LOW);//DOGM081 expects to take in data on rising clock.
  digitalWrite(chipSelectPin, LOW);//and keep it low, unless you need the other pins to do something not with this display
  digitalWrite(RSPin, LOW);//About to issue commands
  //The following lines from Electronic Assembly's data sheet, with cursor mode altered.
  //One might repeat the following command many times to get the attention of the ST7032 driver
  sendToDisplay(B00110000, 27); //Function Set, 1 line, instruction table 0
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1
  sendToDisplay(B00110001, 27); //Function Set, 1 line, instruction table 1, repeat request recommended by ST7032 datasheet
  sendToDisplay(B00011100, 27); //Bias 1/4, 1 line LCD
  sendToDisplay(B01010001, 27); //Booster off, set contrast C5, C4
  sendToDisplay(B01101010, 0); //set voltage follower on, gain 2/7
  delay(200);//recommended by ST7032 datasheet
  sendToDisplay(B01110100, 27); //set contrast C3, C2, C1
  sendToDisplay(B00001100, 27); //display on, cursor off, blink cursor position
  sendToDisplay(B00000001, 3); //clear display, home cursor
  delay(2);//takes over a millisecond to clear
  sendToDisplay(B00000110, 2); //entry from right to left, no scrolling.
  waitingMode();
  attachInterrupt(switchPinInt, recordTime, FALLING);//Go to recordTime whenever interrupt switchPinInt goes from high to low
}//Done setting up

void loop() {
  if(hitTime>0){//There's a 1 in 4 billion chance hitTime is zero but represents an actual reading.
    if (0==precision) {//this is first time switch, so no reference, so only set precision flag and record time.
      precision = 1;//so we don't end up here next time
      lastTime = hitTime;//record now for reference next time.
    }
    else
    { periodMicroSeconds = hitTime - lastTime; //if timer has overflowed once since last time, ok since this subtraction will overflow back.
      if (periodMicroSeconds > debounceMicroSeconds) {//debounce by rejecting too-fast times
        precision = 1;//Set our flag so we know we have a valid period
        updateDisplay = 1;//We have a new period, so let's update the display
        lastTime = hitTime;//record now for reference next time.
      }
    }
    hitTime=0;//skip the above routine until another hit.
  }
  //We don't want to read micros every trip through this loop--Reading micros causes interrupt issues.
  // So, if we increase a 16-bit (word) variable by one every trip, it will only overflow back to zero once every 65536 (2^16) trips.
  if(0 == delayCounter++){
    microsSince = micros() - lastTime;//Keep track of idleness.
  }
  if(precision > 0 && microsSince > maxTime){//It's been more than maxTime since the last hit, so go back into waiting mode
    waitingMode();
  }
  if (updateDisplay) {
    updateDisplay = 0;//Don't come back here until next update is requested by changing this flag back to 1
    frequencyHz = 1000000.0 / (float)periodMicroSeconds;//float math for best precision over a wide range.
    if (periodMicroSeconds>20) {precision = 2;}//Only when a rounded divisor is about 20 does the quotient have any meaning past the first digit.
    if (periodMicroSeconds>200) {precision = 3;}
    if (periodMicroSeconds>2000) {precision = 4;}
    if (periodMicroSeconds>20000) {precision = 5;}
#ifdef serialDebug
    Serial.println(periodMicroSeconds);// print the value you read:
#endif
    digitalWrite(RSPin, LOW);  //about to issue a command
    sendToDisplay(B00000001, 0);  //clear display and go home
    //wait a long time for display to clear.
    for (int clockDelay=3200;clockDelay>0;clockDelay--){
      NOP; // delay 62.5ns on a 16MHz AtMega
    }
    digitalWrite(RSPin, HIGH);  //about to issue data
    frequencyHz *= 100.0;  //Temporarily, if you uncomment the re-conversion below, convert to cHz.
    frequencycHz = (word)frequencyHz;//round to a number between 0 and 65535
    //frequencyHz /= 100.0;  //Convert back to Hz in case we ever need it.
    trackDigits = 0;//We're going to display the correct precision by tracking significant digits
    //The highest a word integer can be is 65535, so the most significant digit we could possibly have is the ten thousands digit.
    thisDigit = frequencycHz / 10000;//get ten-thousands digit. This is integer math, so no fractions in result.
    if(thisDigit > 0){
      trackDigits = 1;  //start tracking significant digits
      thisDigit +=48; //convert to ASCII
      sendToDisplay(thisDigit, 0);
    }
    frequencycHz = frequencycHz % 10000;//discard ten-thousands by modulo arithmetic
    thisDigit = frequencycHz / 1000;//get thousands digit
    if(trackDigits){
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
      } else {
        thisDigit +=48; //convert to ASCII
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
      }
    } else {
      if(thisDigit > 0){
        trackDigits = 1;  //start tracking significant digits
        thisDigit +=48; //convert to ASCII
        sendToDisplay(thisDigit, 0);
      }
    }
    frequencycHz = frequencycHz % 1000;//discard thousands
    thisDigit = frequencycHz / 100;//get hundreds digit
    if(trackDigits){
      if(++trackDigits > precision) {//Increment trackDigits, then check to see whether it has exceeded precision.
        sendToDisplay(48, 0);//We've run beyond our precision, so display a placeholding zero.
      } else {
        thisDigit +=48; //convert to ASCII
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
      }
    } else {
      if(thisDigit > 0){
        trackDigits = 1;  //start tracking significant digits
      }
      thisDigit +=48; //convert to ASCII
      sendToDisplay(thisDigit, 0);//this time, even if zero, we display, since placeholder needed for decimal point.
    }
    sendToDisplay('.', 0);//send a decimal point
    frequencycHz = frequencycHz % 100;//discard hundreds
    thisDigit = frequencycHz / 10;//get tens digit
    if(trackDigits){
      if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
        thisDigit +=48; //convert to ASCII
        sendToDisplay(thisDigit, 0);//show this digit since it is significant
      }
    } else {
      if(thisDigit > 0){
        trackDigits = 1;  //start tracking significant digits
      }
      thisDigit +=48; //convert to ASCII
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
    }
    thisDigit = frequencycHz % 10;//get ones digit
    if(++trackDigits <= precision) {//increment trackDigits, then check to see whether it hasn't exceeded precision
      thisDigit +=48; //convert to ASCII
      sendToDisplay(thisDigit, 0);//show this digit since it is significant
    }
    sendToDisplay('H', 0);//always show units if they exist
    sendToDisplay('z', 0);
  }
}