Attiny 45 - Timer based input capturing vs. pin change interrupts

Hello,

as part of my CarDuino project, I would like to monitor my car’s crankshaft sensor and then calculate the engine’s current revs.

Here’s how the crankshaft sensor signal looks in XOscillo at 3000 rpm (click on image for larger view):

I was able to work out that at the car’s maximum engine speed of 7000 rpm, the signal should have a frequency of some 5.3 kHz.

I am not sure why the XOscillo, running on an Arduino Uno board, missed a few pulses in the graph, but I guess it’s because my old laptop is painfully slow.

At the moment, my rpm monitoring sketch, which is executed by an Attiny45 that runs separately from the main microcontroller and which does nothing but monitor the crankshaft, clocks the signal using pin change interrupts:

/*--------------------------------------------------------------------------------------------------------------------

Crankshaft Signal Measurement Sketch

This sketch clocks the crankshaft induction signal that is present at pins 31 and 32
of the engine control unit and calculates the engine speed in rounds per minute (rpm).
It then sends the actual momentary rpm value over to the head unit via I2C.

*/
#include <TinyWireS.h>
#include <avr/interrupt.h>


//--------------------------------------------------------------------------------------------------------------------
// -------------------------------- Pin Definitions

#define crankshaft PB1
#define SCL PB2
#define SDA PB0

#define ledRequestEvent PB3

//--------------------------------------------------------------------------------------------------------------------
// -------------------------------- Crankshaft Signal Calculation Variables

volatile unsigned long crankRisingTimestamp;
unsigned long crankTimestampCurrent;
int crankTimestampCounter;

unsigned long crank_cumul;
unsigned long crankAverage;

unsigned long microsCrank;

unsigned long microsDifference;
unsigned long microsNow;
unsigned long microsStartCount;

boolean firstByte = true;

byte low_Byte;
byte high_Byte;

volatile uint16_t rpm;

void setup() {

  //--------------------------------------------------------------------------------------------------------------------
  // -------------------------------- TinyWireS Initialization, Pin Modes, States and Interrupts


   // TinyWireS Initialization

  TinyWireS.begin(3); // Joining I2C Network As Slave # 3
  TinyWireS.onRequest(requestEvent);

  pinMode(crankshaft, INPUT);

  pinMode(ledRequestEvent, OUTPUT);

  // Setting Interrupt Registers

  GIMSK = 0b00100000;    // turns on pin change interrupts
  PCMSK = 0b00000010;    // turn on interrupt on pins PB1
  sei();                 // enables interrupts
}


void loop() {

  microsDifference = 0;
  crankTimestampCounter = 0;
  crank_cumul = 0;
  crankAverage = 0;

  microsStartCount = micros();

  //--------------------------------------------------------------------------------------------------------------------
  // -------------------------------- Counting Crankshaft Signal Intervals


  while (microsDifference <= 1000000) {

    microsNow = micros();
    microsDifference = microsNow - microsStartCount;


    if (crankRisingTimestamp != crankTimestampCurrent) {

      microsCrank = crankRisingTimestamp - crankTimestampCurrent;
      crank_cumul += microsCrank;
      crankTimestampCounter += 1;
      crankTimestampCurrent = crankRisingTimestamp;
    }

  }

  crankAverage = crank_cumul / crankTimestampCounter;


  // Test Rig rpm Equation... to be replaced by an actual accurate equation:

  rpm = (int) ( 10750 - (0.5 * crankAverage));
}

//--------------------------------------------------------------------------------------------------------------------
// -------------------------------- Crankshaft ISR

ISR(PCINT0_vect) {

  if ((PINB & 1 << crankshaft)) crankRisingTimestamp = micros();
    
  }

//--------------------------------------------------------------------------------------------------------------------
// -------------------------------- I2C Request Event Definition
// -------------------------------- Data is requested from master every 1000 ms.
void requestEvent() {

  bitWrite(PORTB, ledRequestEvent, HIGH);


  if (firstByte) {

    low_Byte = (byte) (rpm & 0xff);
    high_Byte = (byte) ((rpm >> 8) & 0xff);

    TinyWireS.send(low_Byte);
    firstByte = false;
  }

  else {

    TinyWireS.send(high_Byte);
    firstByte = true;
  }

  delay(50);

  bitWrite(PORTB, ledRequestEvent, LOW);

  TinyWireS_stop_check();
}

I’ve chosen to do the crankshaft monitoring on a separate chip because the Atmega328P-PU which gathers the rest of the engine data, including fuel injectors, vehicle speed and a few analog readings, is really already quite busy with that and probably wouldn’t be able to correctly monitor such a fast signal on top of everything.

So anyway, on the rising edge, I take the time, and then calculate the time that passes between two rising edges and work that out to the actual engine revs. I then average all the readings from one second and pass that value on to the I2C master, which also requests data every 1000 ms.

I am not sure if this method is quick enough and what the latencies are. Would it be faster if I do the crankshaft monitoring by means of the Attiny’s timer and combine it with input capturing?

(16 megahertz) / (5.3E3 hertz) = ~3019

Basically, you should be able to use an interrupt on the ATMEGA pretty easily.
It is about 1 in ever 3000 clock cycles an interrupt would occur.

Have the crankshaft on an interrupt pin increment a count.
After say 1 second, read the number of counts.

This sketch runs on an Attiny, though. At 8 MHz internal at the moment. Will that also be enough to just use pin change interrupts, with the way my whole code is at the moment?

Still is only say a few clock cycles every 1500.

It is like 0.2% of the “CPU” use.

Actually, your code seems "the wrong way" to do things...
Give me 5 mins.

I would expect the Arduino is more taxed in requesting I2C data every second then using an interrupt to read the edge and calculating every second. As you eluded to it would also be possible to use the timer input pin to count the edges and consume no MCU cycles until you read the timer value and reset it.

Not done…not checked…but something like this?
You were using Pin Change interrupts…which was another issue. You want External Interrupts. Check the data sheet.

volatile unsigned int count;

void setup() {
  // put your setup code here, to run once:
  GIMSK = 0b01000000;    // Turn on INT0 EXTERNAL INTERRUPTinterrupt (Only works on PB0 = INT0). 
                         //Keep PCIE (Any pin change) intterupts OFF...
  MCUCR = 0b00000011;    // Rising Edge of INT0 causes interrupt. 11 = RISING, 00 = LOW LEVEL, 01 = CHANGE, 10 = FALLING
  sei();                 // enables interrupts
}

unsigned long timer = 0;

void loop() {
  if (millis()-timer>1000){
    //millis passed...
    float time_elapsed = millis()-timer;
    //A second or more has passed, so get the count...
    float revs_per_second = count*(1000.0/time_elapsed);    // Say if 1200 millis had passed, then "per 1000millis (1 second)" is count * (1000/1200).
    float revs_per_min = revs_per_second * 60.0;
count =0;
  }

  // DO GENERAL STUFF...like your LCD screen or whatever.

}

ISR(PCINT0_vect) {

  count++;

}

no prob.

In the mean time, here’s the code that runs on the main “engine stats module” chip, an Atmega 328P-PU. At the moment, indeed at 16 MHz.

It reports its data to a head unit Atmega1284P, which in turn gives out the data on a small TFT screen.

#include <Wire.h>

#include <avr/interrupt.h>
/*

  Engine Stats Module

  This sketch monitors engine data and engine related peripheral functions:

  - injection intervals
  - oil temperature
  - coolant temperature
  - coolant level
  - vehicle speed
  - fuel tank level


  This data is then sent over to the head unit module via I2C,
  where it is displayed on the screen.

  Some data, like the injection intervals and vehicle speed, is used for
  complex calculations of fuel consumption, fuel range, average speed, etc.

*/

// -------------------------------- Pin definitions

// Interrupt Pins
#define injector 2
#define kmh 3

// Analog Gauges
#define fuelTank A1
#define oilTemp A2
#define coolantTemp A3
#define coolantLevel 6

// System Clock and System Data
#define SCL 5
#define SDA 4

//Status LED
#define led 8

#define Vref AREF;

// -------------------------------- Calculation variables

// Microsecond markers for start and stop of injection intervals
volatile unsigned long micros_start;
volatile unsigned long micros_stop;
volatile unsigned long microsInjector;

// Fuel consumption variables
unsigned long microsInjector_cumul;
float millisInjector_cumul;
float mlInjectedPer3sec;
float lPerHour;

float fuelConsumption;

unsigned long injectorTimestamp = 0;
unsigned long injectorTimestampCurrent = 0;

unsigned long microsStartCount;
unsigned long microsNow;
unsigned long microsDifference;

// Analog gauge variables
unsigned int fuelTankVoltage;
unsigned int oilTempVoltage;
unsigned int coolantTempVoltage;

float fuelLevelFinal;
float fuelTankPercentage;
int oilTempFinal;
int coolantTempFinal;

byte lowCoolant;

// Fuel tank averaging variables
boolean startFuelAveraging = false;
volatile unsigned long fuelAveragingTimestamp = 0;

// Vehicle Speed Calculation variables
volatile unsigned long kmhTimestamp = micros();

unsigned long kmhTimestampCurrent;
unsigned long kmhTimestampDifference;
unsigned long kmhTimestampDifference_cumul;
unsigned long kmhTimestampDifference_average;
unsigned long kmhTimestampCounter;

int kmh_speed;

// I2C data transmission variables
volatile byte* fuelConsFloatPtr;
volatile byte* fuelTankFloatPtr;

void setup() {

  Serial.begin(115200);
  Wire.begin(2);                // join i2c bus with address #2
  Wire.onRequest(requestEvent);         // register event

  analogReference(EXTERNAL);
  
  // -------------------------------- Pin modes, states and ISRs

  //Interrupt pins
  pinMode (injector, INPUT);
  pinMode (kmh, INPUT);

  //Analog gauges
  pinMode (fuelTankVoltage, INPUT);
  pinMode (oilTempVoltage, INPUT);
  pinMode (coolantTempVoltage, INPUT);

  //LED
  pinMode (led, OUTPUT);
  digitalWrite (led, LOW);


  //Interrupt Service Routine for fuel injector on external interrupt 0 and speed transducer on external interrupt 1

    attachInterrupt(digitalPinToInterrupt(injector), injectorSignal, CHANGE);
    attachInterrupt(digitalPinToInterrupt(kmh), kmhSignal, RISING);
 }

void loop() {

  // Resetting variables for 3-second interval
  microsDifference = 0;
  microsInjector_cumul = 0;
  kmhTimestampDifference_cumul = 0;
  kmhTimestampCounter = 0;

    // -------------------------------- Recurring 3-second interval for injection quantity and average speed calculations

  microsStartCount = micros();

  while (microsDifference <= 3000000) {

    microsNow = micros();

    microsDifference = microsNow - microsStartCount;


    // ------------------ Fuel consumption calculation routine

    injectorTimestamp = micros_start;


    // Injection interval is only counted if it hasn't been counted yet

    if (injectorTimestamp != injectorTimestampCurrent) {

      microsInjector_cumul += microsInjector;

      injectorTimestampCurrent = injectorTimestamp;
    }

    // ------------------ speed calculation routine

    if (kmhTimestamp != kmhTimestampCurrent) {

      kmhTimestampDifference = kmhTimestamp - kmhTimestampCurrent;
      kmhTimestampDifference_cumul += kmhTimestampDifference;
      kmhTimestampCounter += 1;

      kmhTimestampCurrent = kmhTimestamp;
    }
  }

  // Calculating average of speed signal intervals

  kmhTimestampDifference_average = kmhTimestampDifference_cumul / kmhTimestampCounter;

  kmh_speed = (int) ((-0.0065761 * kmhTimestampDifference_average) + 220);

  if (kmh_speed <= 0) kmh_speed = 0;


  // Calculating cumulated injected fuel during 3 second cycle

  millisInjector_cumul = (float) microsInjector_cumul / 1000.0;

  mlInjectedPer3sec = millisInjector_cumul * 0.0033578431 * 4.8;

  // Calculating Liters Per Hour

  lPerHour = mlInjectedPer3sec * 0.12;

  // Extrapolating from 3 seconds to one hour, for all four injectors:

  if (kmh_speed == 0) fuelConsumption = (float) lPerHour;

  else if (kmh_speed > 0) fuelConsumption = (float) (mlInjectedPer3sec / 1000) * (100 / (kmh_speed * 0.0008333333333));

  // --------------------------------  Analog gauges input

  // -------- These calculation formulas still need to be replaced by accurate formulas
  // -------- that are adjusted to the actual behavior of the car's sensors.
  // -------- So far, they just read a number of 5K potentiometers that are powered with 5V.

  fuelTankVoltage = analogRead(fuelTank);
  fuelTankPercentage =  (float) fuelTankVoltage / 1023;
  fuelLevelFinal = fuelTankPercentage * 50;


  oilTempVoltage = analogRead(oilTemp);
  oilTempFinal = (int) ((oilTempVoltage / 5.11) - 20);

  coolantTempVoltage = analogRead(coolantTemp);
  coolantTempFinal = (int) ((coolantTempVoltage / 7.30) - 20);

  lowCoolant = digitalRead(coolantLevel);

  Serial.print("\nFuel consumption: \t");
  Serial.println(fuelConsumption);

  Serial.print("Vehicle speed:\t\t");
  Serial.println(kmh_speed);

  Serial.print("Fuel Tank: \t\t");
  Serial.println(fuelLevelFinal);
  Serial.print("Oil Temp: \t\t");
  Serial.println(oilTempFinal);
  Serial.print("Coolant Temp: \t\t");
  Serial.println(coolantTempFinal);
}

// -------------------------------- Interrupt Service Routines

// Injector Signal ISR

void injectorSignal() {

  if (!digitalRead(injector))  {
    micros_start = micros();
  }

  else if (digitalRead(injector)) {

    micros_stop = micros();
    microsInjector = micros_stop - micros_start;
  }
}

// Speedometer ISR

void kmhSignal() {

  kmhTimestamp = micros();
}

// -------------------------------- I2C Request Event definition

void requestEvent() {

  bitWrite(PORTB, PB0, HIGH);

  // Generating Fuel Consumption and Speed Bytes

  byte  Data[5];

  fuelConsFloatPtr = (byte*) & fuelConsumption;

  Data[0] = fuelConsFloatPtr[0];
  Data[1] = fuelConsFloatPtr[1];
  Data[2] = fuelConsFloatPtr[2];
  Data[3] = fuelConsFloatPtr[3];

  // Maximum speed is 190 kph, so one byte will be enough

  Data[4] = (byte) (kmh_speed);
  
  Wire.write(Data, 5);

  // Generating Fuel Tank, Oil Temp and Coolant Temp Bytes

  byte  DataGauges[9];

  // Fuel Tank

  fuelTankFloatPtr = (byte*) & fuelLevelFinal;

  DataGauges[0] = fuelTankFloatPtr[0];
  DataGauges[1] = fuelTankFloatPtr[1];
  DataGauges[2] = fuelTankFloatPtr[2];
  DataGauges[3] = fuelTankFloatPtr[3];

  // Oil and Coolant... 16 bit, always Low Byte First, Second Byte Last

  DataGauges[4] = (byte) (oilTempFinal & 0xff);
  DataGauges[5] = (byte) ((oilTempFinal >> 8) & 0xff);

  DataGauges[6] = (byte) (coolantTempFinal & 0xff);
  DataGauges[7] = (byte) ((coolantTempFinal >> 8) & 0xff);

  DataGauges[8] = lowCoolant;

  Wire.write(DataGauges, 9);

  delay(50);

  bitWrite(PORTB, PB0, LOW);
}

Here are the two signals that are monitored by the 328 on its external interrupts (click on images for larger view):

The fuel injector(s) at 2500 rpm (fuel is injected on the falling edges, always at intervals between 1 and 2 ms):

And the vehicle speed transducer signal. Measured here at 90 mph:

So - could I burden the 328 with the crankshaft signal as well?

It would indeed simplify my whole setup… but I am not sure…

Johnny010:
Not done...not checked...but something like this?
You were using Pin Change interrupts...which was another issue. You want External Interrupts. Check the data sheet.

As I said, at the moment, the crankshaft monitoring is done by an Attiny45. I thought the Attiny x5s don't have an external interrupt?

OK, this is maybe a bit more complicated than I though.

How many interrupts do you need?

The 32u4 ATMEGA devices have 5 available specific EXTERNAL interrupt pins. Also run at 16Mhz.
I still think a 1 chip solution would be best...but yeah...offloading the graphics may be an idea...but again it may not be required!

The 16u4 and 32u4 come in a package you could solder yourself on some PCBs...

The x85 and x45 series have a single External Interrupt on PB0 (IC pin 5) called INT0.

You should use hardware counters for this. It would remove most of the burden from CPU. ATiny x5 has 2 and ATMega328p (from Arduino Uno) has three. They can be clocked from external source (only 1 timer in Tiny x5 sadly) so you don't have to fire interrupt for every edge but only for every timer overflow. If you are interested in current speed (or what you are measuring) you simply read the timer and make the calculations...

Didn’t even think of that! Seems like a better idea.
One of the timers is used for millis() mind…so maybe you can only use 2?

I've put the graphics on a separate 1284P because they're going to be quite "memory-intense". The way I want the display to look, it takes up some 58 KB of the 1284's progmem. Carefully calculated so that it's still below the 64 KB threshold above which the 1284 is rumoured to no longer boot :wink:

Just to give you an idea (this is still in experimental stage):

I've created all these layout graphics and two bespoke numerical fonts to display the values. All of it stored in PROGMEM for quick access.

Also, the 1284 is going to do all the fuel consumption, fuel range and mileage calculations based on the data retrieved from the I2C slave(s). I think I will also have to store a certain amount of data in the 1284's EEPROM, so it won't be lost when you turn off the ignition.

I plan to eventually run the 1284 with a 20 MHz external crystal, but at the moment, it's on a 16 MHz one.

One reason why I opted for a modular I2C layout is also that it will save a great bit of wiring in my car. It's a mid-engined 1998 MG F, so the engine control unit is all the way in the back, while the TFT display will of course be in the dashboard in the front. There is a possibility that I could just let the 1284 do everything... although I'm not sure it'd still have enough free input pins... but it would come at the cost of having lots and lots of individual wires running from the back to the front of the car were the CarDuino's display will be located.

Johnny010:
How many interrupts do you need?

I need three interrupts on the engine stats module. One for vehicle speed, another one for the fuel injectors, and a third one to monitor the crankshaft sensor.

So far, as I said, my plan was to first of all put a 328P-PU PDIP (or instead, possibly an Attiny 84) in my "engine stats module" to monitor vehicle speed, fuel injection and a few other things like oil and coolant temperature and fuel tank level.

And then have a separate Attiny, on the same circuit board, do nothing but monitor the crankshaft signal.

If I could have one single chip do all of that, then of course that would simplify the design by a fair bit. The reason why I chose two separate chips in my initial design was that I really wasn't sure the Atmega328 could handle three quite fast moving interrupts, AND a number of analog reads, AND basic calculations of raw data.

ATMega1284 has 4 counters, 2 of them 16 bit, all of them can be (independently) clocked from external source. I don't know if your project uses the counters somehow (to generate the picture?) but if not you can do all the monitoring on the one chip, no need even for Mega328. Proper use of counters will use the CPU very little and also code should be small - mostly comparable to requesting data from the slave.

EDIT:

carguy:
Carefully calculated so that it's still below the 64 KB threshold above which the 1284 is rumoured to no longer boot :wink:

I doubt Atmel put 128kB of memory to chip but using it causes the chip to fail. Probably you are using poor bootloader.

So one interrupt is about 0.75Khz (the fuel injectors)
One is up to 6Khz ish (the crankshaft)
I can't see the other for the speed transducer...but I am guessing it might be under 10Khz as a guess?

For a 16 MHz clock, these still seem fairly low.

I'd say try with just the ATMEGA328p.

Use the EXTERNAL interrupts (not pin change!) . The ATMEGA has 2 of them...you can always later try a third on a Leonardo (a MEGA16u4) that has like 5 available.

Use increment counts on the ISR_vectors.

At the iteration of each loop, get the time (millis()) and collect the values from each count.

It may make life easier...or it may not work at all...but I have a feeling that the interrupts wont be the main issue.

Smajdalf:
ATMega1284 has 4 counters, 2 of them 16 bit, all of them can be (independently) clocked from external source. I

This is even better!
Hardware counting (so no code blocking by interrupts).

16bit =~65,000 ticks.

10Khz = about 6.5 seconds before you overflow a counter.

If you can get your main loop() to cycle under say 5 seconds, then yeah, this is a good option.

I know the 1284 would probably be able to handle it all on its own.

But again, I would have to run a considerable number of individual wires all across my car. It's just easier to use different modules that will be connected using I2C, and thus only having three, maybe four wires to deal with between modules.

And the way I am envisioning the system at present, I am not really sure the Atmega would have enough free pins for everything.

This is a major project, which has almost taken a year already to this stage now. I do not expect to be completely finished before next year. But that is part of the fun of it. :wink:

Here's an overview of the whole system as I envision it (click on image for larger picture):

Johnny010:
So one interrupt is about 0.75Khz (the fuel injectors)
One is up to 6Khz ish (the crankshaft)
I can't see the other for the speed transducer...but I am guessing it might be under 10Khz as a guess?

I just had another look at the above screenshot of the speed transducer. At 90 mph, it seems to take 7 ms between rising edges. That should work out to about 142 Hz. The car's top speed, which should probably also be accounted for, is around 120 mph, so that should be around 184 Hz.

Johnny010:
I'd say try with just the ATMEGA328p.

Good. It will simplify the design.

Johnny010:
but I have a feeling that the interrupts wont be the main issue.

...meaning? :frowning:

"Carefully calculated so that it's still below the 64 KB threshold above which the 1284 is rumoured to no longer boot ;)"
Where is that rumor from?
From the datasheet:
"Since all AVR instructions are 16 or 32 bits wide,the Flash is organized as 32/64 x 16."

If a bootloader is installed, it starts at one of these addresses:
0xF000 (8192 byte bootloader) (starting byte 122880)
0xF800 (4096 byte), (starting byte 126976)
0xFC00 (2048 byte), (starting byte 129024)
0xFE00 (1024 byte), (starting byte 130048)
going to the top of memory at 0xFFFF (final byte 131071) - all of which are well above the 64K threshold. The bootloader then starts the code at 0x0000.
If no bootloader, the code starts at 0x0000.

So I think there may some mis-interpretation of the datasheet, not taking into account that the addresses referenced are int addresses and not byte addresses. I use 1284's all the time, altho I don't think I've ever gone past a 64K byte sketch. I have created very large arrays in SRAM (over 14K byte) and used all the IO.

ok I see… well, even if that 64 kB threshold does not exist, there’s no harm in trying to be efficient about what and how much you put into your PROGMEM, I guess :wink:

The designing and layouting of all the PROGMEM graphics is more or less complete now, and either way, it will probably remain at something like the 58 kB at which it is now.

@Johnny010: thanks so far for your ideas about how to do all the timing… I will now take a few minutes to go over it carefully. I haven’t done much so far with timer counters, so there’s going to be a bit of reading involved for me at this point :wink:

I have no real life experience with this but what I have read
I2C and long distances = problems
I2C + noise (such as in car) = problems even on short distance
You should surely try if you can communicate I2C reliably over the whole car! Systems in cars usually use differential bus because it is much less sensitive to EMI.

If I were you the measuring module would be ATMega328 (or possibly smaller) with Timer/Counter1 (16bit) counting the fastest signal; Timer/Counter0 (8bit) counting the middle signal; Timer/Counter2 with external watch crystal counting time (to get precise readings). The <1kHz signal can be safely counted by PinChange/External Interrupt or simply by polling in the main loop. My guess is <10% CPU usage this way.