diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cde961 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.other +*.pem +.cache +*.log +*.workspace +*.code-workspace +*.*-workspace +.npm +.yarn +.pnpm +.env.local diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c661011 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Nicholas Berlette + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2235ec --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# `ECA: Ethanol Content Analyzer` + +Ethanol Content Analyzer for Arduino Nano or Uno. + +Converts digital FlexFuel sensor data (`50~150 Hz`) into analog (`0-5v` or `0.5-4.5v`), for tuners, datalogs, and in-dash gauges. + +## `Pins` + +| Type | Pin | Description | +| ---------- | -------- | ------------------- | +| Sensor In | `D8` | `TIMER1 / ICP1` | +| PWM Output | `D3/D11` | Built-in PWM driver | +| DAC Output | `A4/A5` | MCP4725 12bit DAC | + +## `Hz -> E % -> V` + +| Input (Hz) | E (%) | Output (V) | +| :--------- | :----: | :------------------------- | +| `50 hz` | ` 0 %` | `0.50v` | +| `100 hz` | `50 %` | `2.25v` | +| `150 hz` | `100%` | `4.50v` | +| **Errors** | | | +| `< 50 hz` | `---` | `4.80v` - contaminated | +| `> 150 hz` | `---` | `4.90v` - high water level | +| `<= 0 hz` | `---` | `0.10v` - disconnected | + +## `License` + +[MIT](https://mit-license.org) © [Nicholas Berlette](https://nick.berlette.com) diff --git a/libraries/MCP4725/MCP4725.cpp b/libraries/MCP4725/MCP4725.cpp new file mode 100644 index 0000000..9ae3159 --- /dev/null +++ b/libraries/MCP4725/MCP4725.cpp @@ -0,0 +1,148 @@ +#include "Arduino.h" +#include +#include "MCP4725.h" +/*See page 19 of the MCP4726 DataSheet. + *[C2,C1]=00 -- Fast Mode + *[C2,C1,C0]=010 --- Write to DAC register only + *[C2,C1,C0]=011 --- Write to DAC and EEPROM registers + *[PD1,PD0]=00 -- Power Normal mode + *[PD1,PD0]=01 -- Power Down mode with 1k pull down + *[PD1,PD0]=10 -- Power Down mode with 100k pull down + *[PD1,PD0]=11 -- Power Down mode with 500k pull down + *The contents of bytes 2nd,3rd,4th are orgnized differently + *based on whether the DAC is operated in Normal-Speed or Fast-Speed. Let + *D denote data byte, x denote don't-care. Then: + * + *For Normal-Speed: + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] + * + *For Fast-Speed: + *[Addr.Byte]+[C2,C1,PD1,PD0,D11,D10,D9,D8],[D7,D6,D5,D4,D3,D2,D1,D0] + * + *The address byte for our dac is (1,1,0,0,0,1,A0). By default A0=GND, but we can connect it to VCC. + *For A0=GND the hex value of address is 0x62 (1100010) + *For A0=VCC the hex value of address is 0x63 (1100011) + *We must pass the address in our Arduino code in the argument of the function .begin(). But the argument must + *be an 8-bit value and the address is only 7-bit long. What is happening internally is left shifting by 1 bit. + *You can see this if you trace the definition of the .begin() function and the definition of the twi_setAddress() + *in the file twi.c. The register TWAR is set to (address << 1). The R/W bit (bit 8) is actually determined based + *on the function you send after .begin() + */ +MCP4725::MCP4725() {} +void MCP4725::begin(uint8_t addr) { //in the Arduino code, we'd send 0x62 as arumenment when A0_bit=GND (default). + _i2caddr = addr; //or if A0_bit=vcc then we would pass 0x63 as argument + Wire.begin(); +} +void MCP4725::setFastMode(){ +#ifdef TWBR // in case this library is used on a chip that does not have TWBR reg defined. + TWBR = ((F_CPU / 400000L) - 16) / 2; // Set I2C frequency for the ATMega to 400kHz +#endif +} +/* + *Wire.write() takes an 8-bit unsigned int. We thus need to convert our 16-bit + *argument to 2 8-bit variables. 16 to 8-bit, conversion keeps the 8 LSBs. + */ +void MCP4725::setVoltageAndSave(uint16_t output){ + /*For Normal-Speed: (Only normal speed includes the C3 bit, needed for writing to EEPROM) + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] */ + Wire.beginTransmission(_i2caddr); + Wire.write(WRITEDACEEPROM); //[C2,C1,C0,x,x,PD1,PD0,x]=[0,1,1,0,0,0,0,0] + /*The 12-bit output is in 16-bit form (0,0,0,0,D11.D10.D9.D8.D7.D6.D5.D4,D3.D2.D1.D0).*/ + uint8_t firstbyte=(output>>4);//(0,0,0,0,0,0,0,0,D11.D10.D9.D8.D7.D6.D5.D4) of which only the 8 LSB's survive + output = output << 12; //(D3.D2.D1.D0,0,0,0,0,0,0,0,0,0,0,0,0) + uint8_t secndbyte=(output>>8);//(0,0,0,0,0,0,0,0,D3,D2,D1,D0,0,0,0,0) of which only the 8 LSB's survive. + Wire.write(firstbyte); + Wire.write(secndbyte); + Wire.endTransmission(); +} +void MCP4725::setVoltage(uint16_t output){ + /*For Normal-Speed: + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] */ + Wire.beginTransmission(_i2caddr); + Wire.write(WRITEDAC); //[C2,C1,C0,x,x,PD1,PD0,x]=[0,1,0,0,0,0,0,0] + /*The 12-bit output is in 16-bit form (0,0,0,0,D11.D10.D9.D8.D7.D6.D5.D4,D3.D2.D1.D0).*/ + uint8_t firstbyte=(output>>4);//(0,0,0,0,0,0,0,0,D11.D10.D9.D8.D7.D6.D5.D4) of which only the 8 LSB's survive + output = output << 12; //(D3.D2.D1.D0,0,0,0,0,0,0,0,0,0,0,0,0) + uint8_t secndbyte=(output>>8);//(0,0,0,0,0,0,0,0,D3,D2,D1,D0,0,0,0,0) of which only the 8 LSB's survive. + Wire.write(firstbyte); + Wire.write(secndbyte); + Wire.endTransmission(); +} +void MCP4725::setVoltageFast( uint16_t output){ + /*For Fast-Speed: + *[Addr.Byte]+[C2,C1,PD1,PD0,D11,D10,D9,D8],[D7,D6,D5,D4,D3,D2,D1,D0] */ + Wire.beginTransmission(_i2caddr); + //output is a 12-bit value in 16-bit form, namely: [0,0,0,0,D11,D10,D9,D8,D7,D6,D5,D4,D3,D2,D1,D0] + uint8_t firstbyte=(output>>8); //[0,0,0,0,0,0,0,0,0,0,0,0,D11,D10,D9,D8] only the 8 LSB's survive + uint8_t secndbyte=(output); //only the 8 LSB's survive. + Wire.write(firstbyte); // Upper data bits (0,0,0,0,D11,D10,D9,D8) + Wire.write(secndbyte); // Lower data bits (D7,D6,D5,D4,D3,D2,D1,D0) + Wire.endTransmission(); +} +void MCP4725::powerDown1kPullDown(){ //[PD1,PD0]=01; [C2,C1,C0]=010 - Write to DAC only + /*For Normal-Speed: + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] */ + Wire.beginTransmission(_i2caddr); + Wire.write(0b01000010); + Wire.write(0b00000000); + Wire.write(0b00000000); + Wire.endTransmission(); +} +void MCP4725::powerDown100kPullDown(){//[PD1,PD0]=10; [C2,C1,C0]=010 - Write to DAC only + /*For Normal-Speed: + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] */ + Wire.beginTransmission(_i2caddr); + Wire.write(0b01000100); + Wire.write(0b00000000); + Wire.write(0b00000000); + Wire.endTransmission(); +} +void MCP4725::powerDown500kPullDown(){//[PD1,PD0]=11; [C2,C1,C0]=010 - Write to DAC only + /*For Normal-Speed: + *[Addr.Byte]+[C2,C1,C0,x,x,PD1,PD0,x]+[D11,D10,D9,D8,D7,D6,D5,D4]+[D3,D2,D1,D0,x,x,x,x] */ + Wire.beginTransmission(_i2caddr); + Wire.write(0b01000110); + Wire.write(0b00000000); + Wire.write(0b00000000); + Wire.endTransmission(); +} + +uint16_t MCP4725::readCurrentDacVal(){ + Wire.requestFrom(_i2caddr, (uint8_t) 5); + while(Wire.available()!=5){ + Serial.println("Waiting for readValFromEEPROM() to complete."); + //just wait for a while until the DAC sends the data to the reabuffer + } + Wire.read(); //status + uint8_t upper8bits = Wire.read(); //DAC Register data (D11,D10,D9,D8,D7,D6,D5,D4) + uint8_t lower8bits = Wire.read(); //DAC Register data (D3,D2,D1,D0,0,0,0,0) + Wire.read(); //forth returned byte is EEPROM data (x,PD1,PD0,x,D11,D10,D9,D8) + Wire.read(); //fifth returned byte is EEPROM data (D7,D6,D5,D4,D3,D2,D1,D0) + + /*We now need to return the value (0,0,0,0,D11,D10,D9,D8,D7,D6,D5,D4,D3,D2,D1,D0). */ + return (upper8bits<<4) | (lower8bits>>4); //This is how we get the 16-bit result we want to return. +} + + +uint16_t MCP4725::readValFromEEPROM(){ + Wire.requestFrom(_i2caddr, (uint8_t) 5); + while(Wire.available()!=5){ + Serial.println("Waiting for readValFromEEPROM() to complete."); + //just wait for a while until the DAC sends the data to the reabuffer + } + uint8_t statusBit = Wire.read() >> 7; + Wire.read(); //secnd returned byte is DAC Register data (upper 8 bits) + Wire.read(); //third returned byte is DAC Register data (lower 4 bits + 0000) + uint8_t upper8bits = Wire.read(); //forth returned byte is EEPROM data (x,PD1,PD0,x,D11,D10,D9,D8) + uint8_t lower8bits = Wire.read(); //fifth returned byte is EEPROM data (D7,D6,D5,D4,D3,D2,D1,D0) + + if(statusBit==0){ + Serial.println("Currently writing to EEPROM. Trying to read again..."); + return readValFromEEPROM(); + } + else{ + /*We now need to return the value (0,0,0,0,D11,D10,D9,D8,D7,D6,D5,D4,D3,D2,D1,D0). */ + upper8bits = upper8bits & 0b00001111; //clear the first 4 bits. + return (upper8bits<<8) | lower8bits; //This is how we get the 16-bit result we want to return. + } +} \ No newline at end of file diff --git a/libraries/MCP4725/MCP4725.h b/libraries/MCP4725/MCP4725.h new file mode 100644 index 0000000..5b8186d --- /dev/null +++ b/libraries/MCP4725/MCP4725.h @@ -0,0 +1,22 @@ +#include "Arduino.h" +#include + +#define WRITEDAC (0x40) //(0100 0000) Writes to the DAC +#define WRITEDACEEPROM (0x60) //(0110 0000) Writes to the DAC and the EEPROM (persisting the assigned value after reset) + +class MCP4725{ + public: + MCP4725(); + void begin(uint8_t a); + void setFastMode(); + void setVoltageAndSave(uint16_t output); + void setVoltage(uint16_t output); + void setVoltageFast(uint16_t output); + void powerDown500kPullDown(); + void powerDown100kPullDown(); + void powerDown1kPullDown(); + uint16_t readValFromEEPROM(); + uint16_t readCurrentDacVal(); + private: + uint8_t _i2caddr; +}; diff --git a/libraries/MCP4725/examples/waveform/waveform.ino b/libraries/MCP4725/examples/waveform/waveform.ino new file mode 100644 index 0000000..3480131 --- /dev/null +++ b/libraries/MCP4725/examples/waveform/waveform.ino @@ -0,0 +1,23 @@ +#include "Arduino.h" +#include +#include "MCP4725.h" +MCP4725 dac; //create a dac object + +const PROGMEM uint16_t DACLookup_FullSine_6Bit[64] ={ + 2048, 2248, 2447, 2642, 2831, 3013, 3185, 3346, 3495, 3630, 3750, 3853, 3939, 4007, 4056, 4085, 4095, 4085, 4056, 4007, 3939, 3853, 3750, 3630, + 3495, 3346, 3185, 3013, 2831, 2642, 2447, 2248, 2048, 1847, 1648, 1453, 1264, 1082, 910, 749, 600, 465, 345, 242, 156, 88, 39, 10, + 0, 10, 39, 88, 156, 242, 345, 465, 600, 749, 910, 1082, 1264, 1453, 1648, 1847}; + +void setup(void) { + dac.begin(0x62); //addres for the dac is 0x62 (default) or 0x63 (A0 pin tied to VCC) + dac.setFastMode(); //comment this out to unset fastmode +} +void loop(void) { + uint16_t i; + for (i = 0; i < 64; i++){ + //dac.setVoltage(pgm_read_word(&(DACLookup_FullSine_6Bit[i]))); //gives: 37Hz with FastMode unset; 110Hz with FastMode set. + dac.setVoltageFast(pgm_read_word(&(DACLookup_FullSine_6Bit[i]))); //gives: 48Hz with FastMode unset; 143Hz with FastMode set. + } + dac.powerDown500kPullDown(); + delay(5); +} diff --git a/src/eca.ino b/src/eca.ino new file mode 100644 index 0000000..e17cb4e --- /dev/null +++ b/src/eca.ino @@ -0,0 +1,231 @@ +/** + * ECA: Ethanol Content Analyzer + * + * Converts a 50-150hz flexfuel frequency to 0-5volt analog signal (PWM or DAC) + * -> See README.md + * ---------------------------------------------------------------------------- + * MIT © 2021 Nicholas Berlette + */ + +#define VERSION "1.0.1" + +#define PIN_INPUT_SENSOR 10 +#define PIN_OUTPUT_PWM 9 + +#define ENABLE_SERIAL 1 +#define SERIAL_BAUDRATE 9600 + +#define ENABLE_DAC_OUT 1 +#define ENABLE_PWM_OUT 1 + +#define PWM_MULTIPLIER 255 +#define DAC_MULTIPLIER 4095 + +#ifdef ENABLE_DAC_OUT + #include + MCP4725 dac; +#endif + +const int voltageMin = 0.5; +const int voltageMax = 4.5; + +const int eContentAdder = 0; +const int eContentFixed = 0; + +const int refreshDelay = 1000; + +volatile uint16_t countTick = 0; +volatile uint16_t revTick; + +static long highTime = 0; +static long lowTime = 0; +static long tempPulse; + +ISR(TIMER1_CAPT_vect) +{ // Pulse detected, interrupt triggered + // save duration of last revolution + revTick = ICR1; + // restart timer for next revolution + TCNT1 = 0; +} + +ISR(TIMER1_OVF_vect) +{ // counter overflow/timeout + revTick = 0; +} + +void setup() +{ + if (ENABLE_SERIAL == 1) + { + Serial.begin(SERIAL_BAUDRATE); + } + pinMode(PIN_INPUT_SENSOR, INPUT); + + if (defined(ENABLE_PWM_OUT) && ENABLE_PWM_OUT == 1) + { + setPwmFrequency(PIN_OUTPUT_PWM, 1); + } + setupTimer(); + setVoltage(0.1, true); +} + +void setupTimer () +{ + TCCR1A = 0; + // Falling edge trigger, Timer = CPU/256, noise-cancellation + TCCR1B = 132; + TCCR1C = 0; + // Enable input capture (ICP1) and overflow interrupt (OVF1) + TIMSK1 = 33; + TCNT1 = 0; +} + +void setVoltage (double volts, bool init = false) +{ + const int maxVolts = 5.0; + + if (defined(ENABLE_PWM_OUT) && ENABLE_PWM_OUT == 1) + { + if (init) { + pinMode(PIN_OUTPUT_PWM, OUTPUT); + TCCR1B = TCCR1B & 0b11111000 | 0x01; + } + analogWrite(PIN_OUTPUT_PWM, int((PWM_MULTIPLIER * (volts / maxVolts)))); + } + if (defined(ENABLE_DAC_OUT) && ENABLE_DAC_OUT == 1) + { + if (init) { + dac.begin(0x60); + } + dac.setVoltage(int(DAC_MULTIPLIER * (volts / maxVolts)), false); + } +} + +int getTempC (unsigned long highTime, unsigned long lowTime) +{ + // fuel temperature (degC) + // 1 millisecond = -40C, 5 milliseconds = 125C + unsigned long pulseTime = highTime + lowTime; + float frequency = float(1000000 / pulseTime); + float dutyCycle = 100 * (highTime / float(lowTime + highTime)); + float totalTime = float(1.0 / frequency); + float period = float(100 - dutyCycle) * totalTime; + + return int((40.25 * (10 * period)) - 81.25); +} + +int getEthanol (unsigned long pulseTime) +{ + float frequency = float(1000000 / pulseTime); + // 20000 uS = 50 HZ - ~6667 uS = 150 HZ + if (pulseTime >= 20100 || pulseTime <= 6400) + { + if (pulseTime == 0) + { // sensor disconnected / short circuit + setVoltage(0.1); + } + else if (pulseTime >= 20100) + { // contaminated fuel supply + setVoltage(4.8); + } + else if ((pulseTime <= 6400) && (pulseTime >= 1)) + { // high water content in fuel + setVoltage(4.9); + } + if (countTick < 2) + { + countTick++; + } + return; + } + + int eContent = frequency - (50 - eContentAdder); + return clamp(eContent, 0, 100); +} + +float setVoltageFromEthanol (int ethanol) +{ + float desiredVoltage = mapf(ethanol, 0, 100, voltageMin, voltageMax); + setVoltage(desiredVoltage, false); + return desiredVoltage; +} + +double mapf (double val, double x1, double x2, double y1, double y2) +{ + return (val - x1) * (y2 - y1) / (x2 - x1) + y1; +} + +int clamp (int val, int min, int max) +{ + if (val < min) { + val = min; + } else if (val > max) { + val = max; + } + return val; +} + +int cToF (int tempC) +{ + return clamp(int(tempC * 1.8 + 32), -39, 250); +} + +void setPwmFrequency(int pin, int divisor) +{ + // This code snippet raises the timers linked to the PWM outputs + // This way the PWM frequency can be raised or lowered. + // Prescaler of 1 sets PWM output to 32KHz (pin 3, 11) + byte mode; + + if(pin == 5 || pin == 6 || pin == 9 || pin == 10) { + switch(divisor) { + case 1: mode = 0x01; break; + case 8: mode = 0x02; break; + case 64: mode = 0x03; break; + case 256: mode = 0x04; break; + case 1024: mode = 0x05; break; + default: return; + } + if(pin == 5 || pin == 6) { + TCCR0B = TCCR0B & 0b11111000 | mode; + } else { + TCCR1B = TCCR1B & 0b11111000 | mode; + } + } else if(pin == 3 || pin == 11) { + switch(divisor) { + case 1: mode = 0x01; break; + case 8: mode = 0x02; break; + case 32: mode = 0x03; break; + case 64: mode = 0x04; break; + case 128: mode = 0x05; break; + case 256: mode = 0x06; break; + case 1024: mode = 0x7; break; + default: return; + } + TCCR2B = TCCR2B & 0b11111000 | mode; + } +} + +void loop () +{ + unsigned long highTime = pulseIn(PIN_INPUT_SENSOR, HIGH); + unsigned long lowTime = pulseIn(PIN_INPUT_SENSOR, LOW); + + unsigned long pulseTime = highTime + lowTime; + float frequency = float(1000000 / pulseTime); + + eContent = getEthanol(pulseTime); + setVoltageFromEthanol(eContent); + + tempC = getTempC(highTime, lowTime); + tempF = cToF(tempC); + + if (ENABLE_SERIAL == 1) + { + Serial << "Ethanol: " << eContent << "\% • Fuel Temp: " << tempC << "°C (" << tempF << "°F)" << endl; + Serial.println(); + } + delay(refreshDelay); + countTick = 0; +}