Frequency meter on Arduino

From ShawnReevesWiki
Jump to navigationJump to search

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);
  }
}