/* * Sketch for counting impulses in a defined interval * e.g. for power meters with an s0 interface that can be * connected to an input of an arduino board * * the sketch uses pin change interrupts which can be anabled * for any of the inputs on e.g. an arduino uno or a jeenode * * the pin change Interrupt handling used here * is based on the arduino playground example on PCINT: * http://playground.arduino.cc/Main/PcInt * * Refer to avr-gcc header files, arduino source and atmega datasheet. */ /* Pin to interrupt map: * D0-D7 = PCINT 16-23 = PCIR2 = PD = PCIE2 = pcmsk2 * D8-D13 = PCINT 0-5 = PCIR0 = PB = PCIE0 = pcmsk0 * A0-A5 (D14-D19) = PCINT 8-13 = PCIR1 = PC = PCIE1 = pcmsk1 */ #include "pins_arduino.h" char* version = "ArduCounter V1.0"; char* error = "error "; /* arduino pins that are typically ok to use * (some are left out because they are used * as reset, serial, led or other things on most boards) */ byte allowedPins[20] = { 0, 0, 0, 3, 4, 5, 6, 7, 0, 9, 10, 11, 12, 0, 14, 15, 16, 17, 0, 0}; /* Pin change mask for each chip port */ volatile uint8_t *port_to_pcmask[] = { &PCMSK0, &PCMSK1, &PCMSK2 }; /* last PIN States to detect individual pin changes in ISR */ volatile static uint8_t PCintLast[3]; unsigned long intervalMin = 10000; // default 10 sec unsigned long intervalMax = 120000; // default 2 min unsigned long timeNextReport; unsigned long now; boolean doReport = false; /* index to the following arrays is the internal PCINT pin number, not the arduino * pin number because the PCINT pin number corresponds to the physical ports * and this saves time for mapping to the arduino numbers */ /* pin change mode (RISING etc.) as parameter for ISR */ byte PCintMode[24]; /* pin number for PCINT number if active - otherwise -1 */ char PCintActivePin[24]; /* did we get first interrupt yet? */ volatile boolean initialized[24]; /* individual counter for each real pin */ volatile unsigned long counter[24]; /* count at last report to get difference */ unsigned long lastCount[24]; /* millis at first interrupt for current interval * (is also last interrupt of old interval) */ volatile unsigned long startTime[24]; /* millis at last interrupt */ volatile unsigned long lastTime[24]; /* millis at last report * to find out when maxInterval is over * and report has to be done even if * no impulses were counted */ unsigned long lastReport[24]; /* max for SplitLine */ #define MAXLINEPARTS 5 String inputString = ""; // a string to hold incoming data boolean newCommand = false; // whether the command string is complete String linePart[MAXLINEPARTS]; int lineParts = 0; /* Add a pin to be handled */ int AddPin(uint8_t aPin, int mode) { uint8_t pcintPin; // PCINT pin number for the pin to be added (this is used as index for most arrays) volatile uint8_t *pcmask; // pointer to PCMSK0 or 1 or 2 depending on the port corresponding to the pin unsigned long now = millis(); uint8_t bit = digitalPinToBitMask(aPin); // bit in PCMSK to enable pin change interrupt for this pin (arduino pin number!) uint8_t port = digitalPinToPort(aPin); // port that this pin belongs to for enabling interrupts for the whole port (arduino pin number!) if (port == NOT_A_PORT) { return 1; } else { // map port to bit in PCIR register port -= 2; pcmask = port_to_pcmask[port]; // point to PCMSK0 or 1 or 2 depending on the port corresponding to the pin } if (port == 1) { // now calculate the PCINT pin number that corresponds to the arduino pin number pcintPin = aPin - 6; // port 1: PC0-PC5 (A0-A5 or D14-D19) is PCINT 8-13 (PC6 is reset) } else { // arduino numbering continues at D14 since PB6/PB7 are used for other things pcintPin = port * 8 + (aPin % 8); // port 0: PB0-PB5 (D8-D13) is PCINT 0-5 (PB6/PB7 is crystal) } // port 2: PD0-PD7 (D0-D7) is PCINT 16-23 PCintMode[pcintPin] = mode; // save mode for ISR which uses the pcintPin as index because this is easy to get in ISR PCintActivePin[pcintPin] = aPin; // save real arduino pin number and flag this pin as active for reporting initialized[pcintPin] = false; // initialize arrays for this pin counter[pcintPin] = 0; lastCount[pcintPin] = 0; startTime[pcintPin] = now; lastTime[pcintPin] = now; lastReport[pcintPin] = now; *pcmask |= bit; // set the pin change interrupt mask through a pointer to PCMSK0 or 1 or 2 depending on the port corresponding to the pin PCICR |= 0x01 << port; // enable the interrupt return 0; } /* Remove a pin to be handled */ int RemovePin(uint8_t aPin) { uint8_t pcintPin; volatile uint8_t *pcmask; uint8_t bit = digitalPinToBitMask(aPin); uint8_t port = digitalPinToPort(aPin); if (port == NOT_A_PORT) { return 1; } else { port -= 2; pcmask = port_to_pcmask[port]; } if (port == 1) { // see comments at AddPin above pcintPin = port * 8 + (aPin - 14); } else { pcintPin = port * 8 + (aPin % 8); } PCintActivePin[pcintPin] = -1; *pcmask &= ~bit; // disable the mask. if (*pcmask == 0) { // if that's the last one, disable the interrupt. PCICR &= ~(0x01 << port); } return 0; } // common interrupt handler. "port" is the PCINT port number (0-2) static void PCint(uint8_t port) { uint8_t bit; uint8_t curr; uint8_t mask; uint8_t pcintPin; // get the pin states for the indicated port. curr = *portInputRegister(port+2); // current pin states at port mask = curr ^ PCintLast[port]; // xor gets bits that are different PCintLast[port] = curr; // store new pin state for next interrupt if ((mask &= *port_to_pcmask[port]) == 0) { // mask is pins that have changed. screen out non pcint pins. return; /* no handled pin changed */ } for (uint8_t i=0; i < 8; i++) { bit = 0x01 << i; // loop over each pin that changed if (bit & mask) { // is pin change interrupt enabled for this pin? pcintPin = port * 8 + i; // pcint pin numbers directly follow the bit numbers, only arduino pin numbers are special // count if mode is CHANGE, or if mode is RISING and // the bit is currently high, or if mode is FALLING and bit is low. if ((PCintMode[pcintPin] == CHANGE || ((PCintMode[pcintPin] == RISING) && (curr & bit)) || ((PCintMode[pcintPin] == FALLING) && !(curr & bit)))) { lastTime[pcintPin] = millis(); // remember time of this impulse in case it will be the last in the interval if (initialized[pcintPin]) { counter[pcintPin]++; // count } else { startTime[pcintPin] = lastTime[pcintPin]; // if this is the first impulse on this pin, remember time as first impulse in interval initialized[pcintPin] = true; // and start counting the next impulse } } } } } SIGNAL(PCINT0_vect) { PCint(0); } SIGNAL(PCINT1_vect) { PCint(1); } SIGNAL(PCINT2_vect) { PCint(2); } /* split a line read from serial into individual words * and store them in the array lineParts[] */ void SplitLine () { int index = 0; int sepPos = 0; lineParts = 0; while (sepPos > -1 && index < inputString.length()) { // as long as a blank was found an not at end of line ... while (inputString.charAt(index) == ' ' // if next char is another blank && lineParts < MAXLINEPARTS && index < inputString.length()) index++; // skip more blanks if (index < inputString.length()) { sepPos = inputString.indexOf(' ', index+1); // find next blank after the word? if (sepPos == -1) linePart[lineParts] = inputString.substring(index); // no more blanks -> take rest of string else linePart[lineParts] = inputString.substring(index, sepPos); // more blanks -> take word nefore next blank and go on index = sepPos + 1; // continue looking after last blank lineParts ++; } } for (int i=lineParts; i= 0) { // -1 means pin is not active for reporting newCount = counter[pcintPin]; // get current counter countDiff = newCount - lastCount[pcintPin]; // how many impulses since last report? if (countDiff == 0 && (now - lastReport[pcintPin] < intervalMax)) // if nothing to report, take next pin continue; if (countDiff > 0) { // if there was an impulse, report timeDiff = lastTime[pcintPin] - startTime[pcintPin]; // time between first and last impulse during interval lastCount[pcintPin] = newCount; // remember current count for next interval startTime[pcintPin] = lastTime[pcintPin]; // time of last impulse in this interval becomes also time of first impulse in next interval } else { timeDiff = now - startTime[pcintPin]; // there was no impulse, but maxInterval is over: show from last impulse to now startTime[pcintPin] = now; // start a new interval for next report - last one will be reported as 0 imulses ... lastTime[pcintPin] = now; // also time of first impulse in next interval } Serial.println((String) "R" + aPin + // report on serial out " C" + newCount + " D" + countDiff + " T" + timeDiff); lastReport[pcintPin] = now; // remember when we reported } } } /* give status report in between if requested over serial input */ void showCmd() { unsigned long newCount; unsigned long countDiff; unsigned long timeDiff; char* pName; Serial.println((String) version); Serial.println((String) "Min " + intervalMin); Serial.println((String) "Max " + intervalMax); for (int i=0; i<24; i++) { int aPin = PCintActivePin[i]; if (aPin != -1) { timeDiff = lastTime[i] - startTime[i]; newCount = counter[i]; countDiff = newCount - lastCount[i]; if (!timeDiff) timeDiff = millis() - startTime[i]; Serial.println((String) "PCInt " + i + " aPin " + aPin + " Cnt " + newCount + " (+" + countDiff + " ) in " + timeDiff + " Millis"); } } Serial.println((String) "Next in " + (timeNextReport - millis())); } void addCmd() { String pinArg = linePart[1]; // given arduino pin number or name as string String modeArg = linePart[2]; // mode falling, rising or change String pullArg = linePart[3]; // optional pullup int aPin = -1; int mode = RISING; // default if (pinArg.charAt(0) == 'D' || pinArg.charAt(0) == 'd') aPin = pinArg.substring(1).toInt(); // arduino pin name starting with a "d"? if (aPin == -1) { aPin = pinArg.toInt(); // interpret string as pin number if (aPin >= 20 || aPin < 1) { Serial.print(error); Serial.println(aPin); return; } } if (allowedPins[aPin] == 0) { Serial.print(error); Serial.println(aPin); return; }; if (lineParts > 2) { if (modeArg.equalsIgnoreCase("f")) mode = FALLING; else if (modeArg.equalsIgnoreCase("c")) mode = CHANGE; else if (modeArg.equalsIgnoreCase("r")) mode = RISING; else { Serial.print(error); Serial.println(modeArg); return; } } pinMode (aPin, INPUT); if (lineParts > 3) { if (pullArg.equalsIgnoreCase("p")) digitalWrite (aPin, HIGH); // enable pullup resistor else { Serial.print(error); Serial.println(pullArg); return; } } if (AddPin(aPin, mode) == 0) { // call AddPin with arduino pin number Serial.print("added "); Serial.println(aPin); } else { Serial.print(error); Serial.println(pinArg); } } void removeCmd() { String pinArg = linePart[1]; // given arduino pin number or name as string in first part int aPin = -1; if (pinArg.charAt(0) == 'D' || pinArg.charAt(0) == 'd') aPin = pinArg.substring(1).toInt(); // arduino pin name starting with a "d"? if (aPin == -1) { aPin = pinArg.toInt(); // interpret string as pin number if (aPin >= 20 || aPin < 1) { Serial.print(error); Serial.println(aPin); return; } } if (allowedPins[aPin] == 0) { Serial.print(error); Serial.println(aPin); return; }; if (RemovePin(aPin) == 0) { // call RemovePin with arduino pin number Serial.println((String)"removed " + aPin); } else { Serial.print(error); Serial.println(pinArg); } } void intervalCmd() { String timeArgMin = linePart[1]; String timeArgMax = linePart[2]; int timeMin = timeArgMin.toInt(); if (timeMin < 1 || timeMin > 3600) { Serial.print(error); Serial.println(timeArgMin); return; } int timeMax = timeArgMax.toInt(); if (timeMax < 1 || timeMax > 3600 || timeMax < timeMin) { Serial.print(error); Serial.println(timeArgMax); return; } intervalMin = (long)timeMin * 1000; intervalMax = (long)timeMax * 1000; if (millis() + intervalMin < timeNextReport) timeNextReport = millis() + intervalMin; } void doCommand() { SplitLine(); if (linePart[0].equals("show")) { showCmd(); } else if (linePart[0].equals("add")) { addCmd(); } else if (linePart[0].equals("rem")) { removeCmd(); } else if (linePart[0].equals("int")) { intervalCmd(); } else { Serial.print(error); Serial.println(linePart[0]); } } void setup() { unsigned long now = millis(); for (int pcintPin=0; pcintPin < 24; pcintPin++) { PCintActivePin[pcintPin] = -1; // set all pins to inactive (-1) } timeNextReport = millis() + intervalMin; // time for first output Serial.begin(9600); // initialize serial inputString.reserve(200); // reserve 200 bytes for the inputString interrupts(); Serial.println((String) version + " Setup done."); } void loop() { now = millis(); doReport = false; // check if report nedds to be called if((long)(now - timeNextReport) >= 0) doReport = true; // intervalMin is over else for (byte pcintPin=0; pcintPin<24; pcintPin++) if (PCintActivePin[pcintPin] >= 0) if((long)(now - (lastReport[pcintPin] + intervalMax)) >= 0) doReport = true; // active pin has not been reported for langer than maxInterval if (doReport) { report(); timeNextReport = now + intervalMin; // do it again after interval millis } if (newCommand) { doCommand(); inputString = ""; newCommand = false; } } /* SerialEvent occurs whenever a new data comes in the hardware serial RX. This routine is run between each time loop() runs, so using delay inside loop can delay response. Multiple bytes of data may be available. */ void serialEvent() { while (Serial.available()) { char inChar = (char)Serial.read(); if (inChar == '\n' or inChar == '\r') { if (inputString.length() > 0) newCommand = true; } else { inputString += inChar; } } }