From d790f386299827a7a0dca3ace98742434c942e60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:48:50 +0000 Subject: [PATCH 1/2] Initial plan From 2e6a5a177fe7a3b9a3d92b5eedb57abd891cd256 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:55:10 +0000 Subject: [PATCH 2/2] feat: add optional CAN support, config header, DBC schema, expanded README, and CHANGELOG - Add src/eca_config.h: central compile-time configuration - Add src/eca_can.h: opt-in CAN bus module with MCP2515 support - Add eca.dbc: CAN message schema with all signals and error codes - Update src/eca.ino: integrate config header, CAN support, fix preprocessor guards - Expand README.md: full documentation with pinout, mapping tables, CAN guide, DBC usage - Add CHANGELOG.md: version history for v1.0.0 and v2.0.0 - Update LICENSE copyright to 2021-2026 Co-authored-by: nberlette <11234104+nberlette@users.noreply.github.com> --- CHANGELOG.md | 49 ++++++++ LICENSE | 2 +- README.md | 308 +++++++++++++++++++++++++++++++++++++++++++---- eca.dbc | 73 +++++++++++ src/eca.ino | 136 ++++++++++++--------- src/eca_can.h | 175 +++++++++++++++++++++++++++ src/eca_config.h | 123 +++++++++++++++++++ 7 files changed, 786 insertions(+), 80 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 eca.dbc create mode 100644 src/eca_can.h create mode 100644 src/eca_config.h diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a4f46bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +All notable changes to **ECA (Ethanol Content Analyzer)** are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +--- + +## [2.0.0] — 2026-03-16 + +### Added +- **Optional CAN bus output** — opt-in via `ECA_ENABLE_CAN` in `eca_config.h`. Requires an MCP2515 CAN transceiver module. When disabled (default), behavior is identical to v1.0.x. +- **Configurable CAN arbitration ID** — `ECA_CAN_ARBID` (default `0xEC`). +- **Configurable CAN payload scaling** — frequency transmitted at 0.1 Hz resolution, voltage in millivolts, temperature with +40 °C offset. +- **CAN error/status support** — CAN frames include both an enumerated status code and individual bit flags that map 1:1 to the existing analog error voltages: + - `0x01` Disconnected (analog `0.10 V`) + - `0x02` Contaminated (analog `4.80 V`) + - `0x03` High Water (analog `4.90 V`) +- **DBC schema file** (`eca.dbc`) — canonical CAN message definition for use with CAN tools, dashboards, and loggers. +- **Central configuration header** (`src/eca_config.h`) — all compile-time settings in one place, replacing scattered `#define` directives in the main sketch. +- **CAN module header** (`src/eca_can.h`) — self-contained CAN abstraction, compiled out cleanly when CAN is disabled. + +### Changed +- **README.md** — significantly expanded with hardware pinout, voltage/frequency mapping tables, error condition documentation, CAN configuration guide, DBC usage instructions, and integration examples. +- **Copyright years** updated to 2021–2026 across LICENSE, source files, and documentation. +- **Serial output** now uses `F()` macro for flash-string storage to reduce SRAM usage. +- Firmware version bumped from `1.0.1` to `2.0.0`. +- Analog output `#ifdef` guards replaced with proper `#if` preprocessor checks. +- Error voltage constants centralized in config header (`ECA_ERROR_V_DISCONNECTED`, `ECA_ERROR_V_CONTAMINATED`, `ECA_ERROR_V_HIGH_WATER`). + +### Fixed +- `setVoltage()` DAC address now uses configurable `ECA_DAC_I2C_ADDR` instead of a hardcoded value. + +--- + +## [1.0.0] — 2021-09-24 + +_Initial release._ + +### Added +- FlexFuel sensor frequency measurement using Timer1 input capture (50–150 Hz → 0–100% ethanol). +- PWM analog output (0–5 V) on configurable pin. +- MCP4725 12-bit I2C DAC output support. +- Analog error voltage signaling: `0.10 V` (disconnected), `4.80 V` (contaminated), `4.90 V` (high water). +- Fuel temperature calculation from duty cycle. +- Serial logging of ethanol content and fuel temperature. +- Bundled MCP4725 library with waveform example. + +> **Note:** This entry was reconstructed from the initial commit (`cc2caf3`, 2021-09-24). Intermediate changes between v1.0.0 and v2.0.0, if any, were not tagged and cannot be reliably reconstructed from the repository history. diff --git a/LICENSE b/LICENSE index c661011..2eea65f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Nicholas Berlette +Copyright (c) 2021-2026 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 diff --git a/README.md b/README.md index a2235ec..2070476 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,295 @@ -# `ECA: Ethanol Content Analyzer` +# ECA: Ethanol Content Analyzer -Ethanol Content Analyzer for Arduino Nano or Uno. +An Arduino-based ethanol content analyzer that converts a standard automotive +FlexFuel sensor's digital frequency output (50–150 Hz) into a proportional +analog voltage (0.50–4.50 V) suitable for ECU inputs, wideband controllers, +datalogs, and in-dash gauges. An optional CAN bus output mode is also available +for direct integration with CAN-equipped dashboards, loggers, and standalone +ECUs. -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` +## Features -| Type | Pin | Description | -| ---------- | -------- | ------------------- | -| Sensor In | `D8` | `TIMER1 / ICP1` | -| PWM Output | `D3/D11` | Built-in PWM driver | -| DAC Output | `A4/A5` | MCP4725 12bit DAC | +- **FlexFuel sensor input** — reads the 50–150 Hz square-wave from any + standard GM-style FlexFuel sensor via Timer1 input capture. +- **Dual analog output** — simultaneous PWM and 12-bit MCP4725 I2C DAC output + for maximum compatibility. +- **Configurable voltage range** — default 0.50–4.50 V maps linearly to + 0–100% ethanol. +- **Error/fault signaling** — dedicated out-of-band voltages for sensor + disconnect, contaminated fuel, and high water content. +- **Optional CAN bus output** — opt-in at compile time; transmits ethanol + content, frequency, voltage, fuel temperature, and status over CAN using a + configurable arbitration ID. +- **Fuel temperature** — derived from the sensor's duty cycle, output via + serial and (optionally) CAN. +- **Serial debug output** — logs ethanol percentage and fuel temperature over + UART at configurable baud rate. +- **Single config header** — all compile-time options centralized in + `src/eca_config.h`. -## `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 | +## Hardware -## `License` +### Supported Boards -[MIT](https://mit-license.org) © [Nicholas Berlette](https://nick.berlette.com) +| Board | MCU | Notes | +|---|---|---| +| Arduino Nano | ATmega328P | Primary target | +| Arduino Uno | ATmega328P | Fully compatible | +| Other ATmega328P boards | ATmega328P | Should work with correct pin mapping | + +### Required Components + +| Component | Purpose | +|---|---| +| FlexFuel sensor (GM-style) | Ethanol content input (50–150 Hz square-wave) | +| MCP4725 breakout board | 12-bit I2C DAC for precision analog output | +| MCP2515 CAN module _(optional)_ | CAN bus transceiver (only needed if CAN is enabled) | + +--- + +## Pinout + +| Type | Pin(s) | Description | +|---|---|---| +| Sensor Input | `D10` | FlexFuel sensor signal (Timer1 / ICP1) | +| PWM Output | `D9` | Built-in PWM analog output | +| DAC Output | `A4 / A5` | MCP4725 I2C (SDA / SCL) | +| CAN SPI _(optional)_ | `D10–D13` | MCP2515 SPI bus (CS configurable in `eca_config.h`) | + +> **Note:** When CAN is enabled, the SPI chip-select pin defaults to `D10`. If +> your wiring conflicts with the sensor input pin, change `ECA_CAN_CS_PIN` in +> `eca_config.h` and re-wire accordingly. + +--- + +## Frequency → Ethanol → Voltage Mapping + +The FlexFuel sensor outputs a square-wave whose frequency is proportional to +ethanol content. The ECA firmware maps this linearly: + +| Sensor Freq | Ethanol | Analog Output | +|:---|:---:|:---| +| `50 Hz` | `0%` | `0.50 V` | +| `100 Hz` | `50%` | `2.50 V` | +| `150 Hz` | `100%` | `4.50 V` | + +### Error Conditions (Analog) + +Out-of-range frequencies trigger distinct error voltages so downstream devices +can distinguish faults from valid readings: + +| Condition | Frequency | Analog Voltage | Description | +|:---|:---|:---|:---| +| **Disconnected** | `≤ 0 Hz` | `0.10 V` | Sensor unplugged or short circuit | +| **Contaminated** | `< 50 Hz` | `4.80 V` | Fuel frequency below valid range | +| **High Water** | `> 150 Hz` | `4.90 V` | Water content above safe threshold | + +These error voltages are intentionally placed outside the normal 0.50–4.50 V +operating range so they can be reliably detected by an ECU or gauge. + +--- + +## CAN Bus Output (Optional) + +CAN support is **disabled by default** and must be opted into at compile time. +When enabled, the ECA transmits an 8-byte CAN frame at a configurable interval +alongside the normal analog output (analog behavior is always preserved). + +### Enabling CAN + +In `src/eca_config.h`, set: + +```c +#define ECA_ENABLE_CAN 1 +``` + +An MCP2515-based CAN transceiver module connected via SPI is required. + +### CAN Configuration Options + +All CAN settings are in `src/eca_config.h`: + +| Option | Default | Description | +|---|---|---| +| `ECA_ENABLE_CAN` | `0` | Master enable (0 = off, 1 = on) | +| `ECA_CAN_ARBID` | `0xEC` (236) | 11-bit standard arbitration ID | +| `ECA_CAN_BAUDRATE` | `500000` | CAN bus speed in bps | +| `ECA_CAN_CS_PIN` | `10` | MCP2515 SPI chip-select pin | +| `ECA_CAN_TX_INTERVAL_MS` | `100` | Transmission interval (ms) | + +### CAN Payload Layout + +Each CAN frame is 8 bytes: + +| Byte | Signal | Unit | Encoding | +|:---|:---|:---|:---| +| 0 | Ethanol Content | `%` | Raw value 0–100 | +| 1–2 | Sensor Frequency | `Hz` | Big-endian, ×10 (0.1 Hz resolution) | +| 3–4 | Output Voltage | `mV` | Big-endian, millivolts | +| 5 | Fuel Temperature | `°C` | Raw + 40 offset (0 = −40 °C) | +| 6 | Status Code | — | Enumerated (see below) | +| 7 | Status Flags | — | Bit field (see below) | + +### CAN Status / Error Codes + +The CAN status byte maps 1:1 to the analog error voltages: + +| Code | Name | Analog Equiv. | Condition | +|:---|:---|:---|:---| +| `0x00` | `OK` | `0.50–4.50 V` | Normal valid reading | +| `0x01` | `Disconnected` | `0.10 V` | Sensor disconnected (≤ 0 Hz) | +| `0x02` | `Contaminated` | `4.80 V` | Contaminated fuel (< 50 Hz) | +| `0x03` | `HighWater` | `4.90 V` | High water content (> 150 Hz) | +| `0xFF` | `Unknown` | — | Initializing / unknown state | + +### CAN Status Flags (Byte 7) + +Individual bits for downstream fault detection: + +| Bit | Mask | Meaning | +|:---|:---|:---| +| 0 | `0x01` | Sensor OK | +| 1 | `0x02` | Sensor disconnected | +| 2 | `0x04` | Contaminated fuel | +| 3 | `0x08` | High water content | +| 7 | `0x80` | Any error present | + +During normal operation, only bit 0 (`0x01`) is set. During any fault, bit 7 +is always set in addition to the specific fault bit. + +--- + +## DBC File + +The included [`eca.dbc`](eca.dbc) file is the canonical CAN message schema for +the ECA output. Load it into your CAN tool of choice to automatically decode +ECA frames: + +- **Vector CANdb++ / CANalyzer / CANoe** +- **SavvyCAN** +- **PCAN-Explorer / PCAN-View** +- **BusMaster** +- **python-can** / **cantools** (Python) + +The DBC defines a single message `ECA_Data` (ID `0x0EC`) with signals for +ethanol content, sensor frequency, output voltage, fuel temperature, status +code, and status flags. Signal comments describe encoding details and error +condition semantics. + +### Example: Decoding with Python cantools + +```python +import cantools +db = cantools.database.load_file('eca.dbc') +msg = db.get_message_by_name('ECA_Data') +data = msg.decode(b'\x32\x01\xF4\x08\xCA\x3C\x00\x01') +# {'EthanolContent': 50, 'SensorFrequency': 50.0, 'OutputVoltage': 2250, +# 'FuelTemperature': 20, 'StatusCode': 0, 'StatusFlags': 1} +``` + +--- + +## Configuration + +All compile-time options are in [`src/eca_config.h`](src/eca_config.h). Edit +this file before uploading to your board. + +### General Settings + +| Option | Default | Description | +|---|---|---| +| `ECA_VERSION` | `"2.0.0"` | Firmware version string | +| `PIN_INPUT_SENSOR` | `10` | Sensor input pin | +| `PIN_OUTPUT_PWM` | `9` | PWM output pin | +| `ECA_ENABLE_SERIAL` | `1` | Enable serial debug output | +| `ECA_SERIAL_BAUDRATE` | `9600` | Serial baud rate | +| `ECA_ENABLE_PWM_OUT` | `1` | Enable PWM output | +| `ECA_ENABLE_DAC_OUT` | `1` | Enable MCP4725 DAC output | +| `ECA_DAC_I2C_ADDR` | `0x60` | MCP4725 I2C address | +| `ECA_VOLTAGE_MIN` | `0.5` | Voltage at 0% ethanol | +| `ECA_VOLTAGE_MAX` | `4.5` | Voltage at 100% ethanol | +| `ECA_REFRESH_DELAY_MS` | `1000` | Main loop delay (ms) | + +### CAN Settings + +See the [CAN Bus Output](#can-bus-output-optional) section above. + +--- + +## Integration Examples + +### Standalone ECU (Analog) + +Wire the PWM or DAC output directly to a spare analog input on your standalone +ECU (Megasquirt, Haltech, AEM, Link, etc.). Configure the input as a +0–5 V sensor and set up a calibration table: + +| Voltage | Ethanol % | +|---|---| +| 0.50 V | 0% | +| 2.50 V | 50% | +| 4.50 V | 100% | + +Program fault detection thresholds at `< 0.30 V` (disconnected) and +`> 4.60 V` (contaminated or high water). + +### CAN Dashboard / Logger + +1. Enable CAN in `eca_config.h` (`ECA_ENABLE_CAN 1`). +2. Connect the MCP2515 module's CAN-H / CAN-L to your CAN bus. +3. Import `eca.dbc` into your dashboard or logging software. +4. The ECA will broadcast `ECA_Data` frames at the configured interval + (default 10 Hz) on arbitration ID `0xEC`. + +### AIM / RaceCapture / Custom Display + +Use the DBC file to add the ECA signals to your channel list. The +`StatusCode` and `StatusFlags` signals allow you to trigger warnings or +alarms when a sensor fault is detected. + +--- + +## Building & Uploading + +1. Open `src/eca.ino` in the [Arduino IDE](https://www.arduino.cc/en/software) + (1.8+) or [PlatformIO](https://platformio.org/). +2. Edit `src/eca_config.h` to match your hardware and preferences. +3. If using the MCP4725 DAC, ensure the library in `libraries/MCP4725/` is + available in your Arduino libraries path. +4. If CAN is enabled, install the + [MCP_CAN library](https://github.com/coryjfowler/MCP_CAN_lib) via the + Arduino Library Manager. +5. Select your board (Arduino Nano / Uno) and upload. + +--- + +## Repository Structure + +``` +eca/ +├── src/ +│ ├── eca.ino # Main firmware sketch +│ ├── eca_config.h # Central configuration header +│ └── eca_can.h # Optional CAN bus module +├── libraries/ +│ └── MCP4725/ # Bundled MCP4725 12-bit DAC library +│ ├── MCP4725.h +│ ├── MCP4725.cpp +│ └── examples/ +│ └── waveform/ +│ └── waveform.ino +├── eca.dbc # CAN message schema (DBC format) +├── CHANGELOG.md # Version history +├── LICENSE # MIT License +└── README.md # This file +``` + +--- + +## License + +[MIT](LICENSE) © 2021–2026 [Nicholas Berlette](https://github.com/nberlette) diff --git a/eca.dbc b/eca.dbc new file mode 100644 index 0000000..0c0c70a --- /dev/null +++ b/eca.dbc @@ -0,0 +1,73 @@ +// ========================================================================== +// ECA: Ethanol Content Analyzer — CAN Database (DBC) +// +// Defines the CAN message schema for the ECA firmware's optional CAN +// output. Use this file with CAN tools (Vector CANdb++, SavvyCAN, +// BusMaster, PCAN, etc.) to decode ECA CAN frames. +// +// Default Arbitration ID: 0x0EC (236 decimal) +// Bus speed: 500 kbps (typical automotive) +// +// MIT (c) 2021-2026 Nicholas Berlette +// ========================================================================== + +VERSION "" + +NS_ : + +BS_: + +BU_: ECA + +// -------------------------------------------------------------------------- +// Message: ECA_Data (ID 0x0EC, 8 bytes, sent by node ECA) +// -------------------------------------------------------------------------- +BO_ 236 ECA_Data: 8 ECA + // Ethanol content percentage (0-100%) + SG_ EthanolContent : 0|8@1+ (1,0) [0|100] "%" Vector__XXX + + // Sensor input frequency (0.1 Hz resolution, e.g. 500 = 50.0 Hz) + SG_ SensorFrequency : 8|16@1+ (0.1,0) [0|6553.5] "Hz" Vector__XXX + + // Analog output voltage in millivolts + SG_ OutputVoltage : 24|16@1+ (1,0) [0|5000] "mV" Vector__XXX + + // Fuel temperature in degrees Celsius (offset -40, so 0 = -40°C) + SG_ FuelTemperature : 40|8@1+ (1,-40) [-40|215] "degC" Vector__XXX + + // Enumerated status / error code + // 0x00 = OK (normal valid reading) + // 0x01 = Disconnected (sensor fault, analog: 0.10V) + // 0x02 = Contaminated (freq < 50 Hz, analog: 4.80V) + // 0x03 = High Water (freq > 150 Hz, analog: 4.90V) + // 0xFF = Unknown / initializing + SG_ StatusCode : 48|8@1+ (1,0) [0|255] "" Vector__XXX + + // Status bit flags (individual fault indicators) + // bit 0 (0x01): Sensor OK + // bit 1 (0x02): Sensor disconnected + // bit 2 (0x04): Contaminated fuel + // bit 3 (0x08): High water content + // bit 7 (0x80): Any error present + SG_ StatusFlags : 56|8@1+ (1,0) [0|255] "" Vector__XXX + +// -------------------------------------------------------------------------- +// Value descriptions for StatusCode signal +// -------------------------------------------------------------------------- +VAL_ 236 StatusCode + 0 "OK" + 1 "Disconnected" + 2 "Contaminated" + 3 "HighWater" + 255 "Unknown" ; + +// -------------------------------------------------------------------------- +// Signal comments +// -------------------------------------------------------------------------- +CM_ SG_ 236 EthanolContent "Ethanol content percentage derived from FlexFuel sensor frequency. 50 Hz = 0%, 150 Hz = 100%."; +CM_ SG_ 236 SensorFrequency "Raw FlexFuel sensor input frequency with 0.1 Hz resolution. Transmitted as Hz * 10 (big-endian). Valid range: 50.0-150.0 Hz."; +CM_ SG_ 236 OutputVoltage "Analog output voltage in millivolts. Normal range: 500-4500 mV. Error voltages: 100 mV (disconnected), 4800 mV (contaminated), 4900 mV (high water)."; +CM_ SG_ 236 FuelTemperature "Fuel temperature in degrees Celsius. Encoded with +40 offset so byte value 0 = -40 degC. Derived from FlexFuel sensor duty cycle."; +CM_ SG_ 236 StatusCode "Enumerated status/error code. 0x00=OK, 0x01=Disconnected (analog 0.10V), 0x02=Contaminated (analog 4.80V), 0x03=HighWater (analog 4.90V), 0xFF=Unknown."; +CM_ SG_ 236 StatusFlags "Bit field for fault conditions. bit0=SensorOK, bit1=Disconnected, bit2=Contaminated, bit3=HighWater, bit7=AnyError. Multiple bits may be set."; +CM_ BO_ 236 "ECA ethanol content analyzer output. Transmits ethanol percentage, sensor frequency, output voltage, fuel temperature, and status/error information. Default arbitration ID 0x0EC (236)."; diff --git a/src/eca.ino b/src/eca.ino index e17cb4e..d295a6c 100644 --- a/src/eca.ino +++ b/src/eca.ino @@ -1,38 +1,25 @@ /** * ECA: Ethanol Content Analyzer * - * Converts a 50-150hz flexfuel frequency to 0-5volt analog signal (PWM or DAC) - * -> See README.md + * Converts a 50-150hz flexfuel frequency to 0-5volt analog signal (PWM or DAC), + * with optional CAN bus output for automotive integration. + * -> See README.md for full documentation. * ---------------------------------------------------------------------------- - * MIT © 2021 Nicholas Berlette + * MIT (c) 2021-2026 Nicholas Berlette */ -#define VERSION "1.0.1" +#include "eca_config.h" -#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 +#if ECA_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; +#if ECA_ENABLE_CAN + #include "eca_can.h" + EcaCan ecaCan; + EcaStatus ecaStatus = ECA_STATUS_UNKNOWN; +#endif volatile uint16_t countTick = 0; volatile uint16_t revTick; @@ -56,18 +43,26 @@ ISR(TIMER1_OVF_vect) void setup() { - if (ENABLE_SERIAL == 1) + if (ECA_ENABLE_SERIAL == 1) { - Serial.begin(SERIAL_BAUDRATE); + Serial.begin(ECA_SERIAL_BAUDRATE); } pinMode(PIN_INPUT_SENSOR, INPUT); - if (defined(ENABLE_PWM_OUT) && ENABLE_PWM_OUT == 1) - { - setPwmFrequency(PIN_OUTPUT_PWM, 1); - } +#if ECA_ENABLE_PWM_OUT + setPwmFrequency(PIN_OUTPUT_PWM, 1); +#endif + setupTimer(); - setVoltage(0.1, true); + setVoltage(ECA_ERROR_V_DISCONNECTED, true); + +#if ECA_ENABLE_CAN + if (!ecaCan.begin()) { + if (ECA_ENABLE_SERIAL == 1) { + Serial.println(F("CAN init failed")); + } + } +#endif } void setupTimer () @@ -83,23 +78,22 @@ void setupTimer () void setVoltage (double volts, bool init = false) { - const int maxVolts = 5.0; + const int maxVolts = ECA_VOLTAGE_RAIL; - 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 ECA_ENABLE_PWM_OUT + if (init) { + pinMode(PIN_OUTPUT_PWM, OUTPUT); + TCCR1B = TCCR1B & 0b11111000 | 0x01; } - if (defined(ENABLE_DAC_OUT) && ENABLE_DAC_OUT == 1) - { - if (init) { - dac.begin(0x60); - } - dac.setVoltage(int(DAC_MULTIPLIER * (volts / maxVolts)), false); + analogWrite(PIN_OUTPUT_PWM, int((PWM_MULTIPLIER * (volts / maxVolts)))); +#endif + +#if ECA_ENABLE_DAC_OUT + if (init) { + dac.begin(ECA_DAC_I2C_ADDR); } + dac.setVoltage(int(DAC_MULTIPLIER * (volts / maxVolts)), false); +#endif } int getTempC (unsigned long highTime, unsigned long lowTime) @@ -123,15 +117,24 @@ int getEthanol (unsigned long pulseTime) { if (pulseTime == 0) { // sensor disconnected / short circuit - setVoltage(0.1); + setVoltage(ECA_ERROR_V_DISCONNECTED); +#if ECA_ENABLE_CAN + ecaStatus = ECA_STATUS_DISCONNECTED; +#endif } else if (pulseTime >= 20100) { // contaminated fuel supply - setVoltage(4.8); + setVoltage(ECA_ERROR_V_CONTAMINATED); +#if ECA_ENABLE_CAN + ecaStatus = ECA_STATUS_CONTAMINATED; +#endif } else if ((pulseTime <= 6400) && (pulseTime >= 1)) { // high water content in fuel - setVoltage(4.9); + setVoltage(ECA_ERROR_V_HIGH_WATER); +#if ECA_ENABLE_CAN + ecaStatus = ECA_STATUS_HIGH_WATER; +#endif } if (countTick < 2) { @@ -140,13 +143,17 @@ int getEthanol (unsigned long pulseTime) return; } - int eContent = frequency - (50 - eContentAdder); +#if ECA_ENABLE_CAN + ecaStatus = ECA_STATUS_OK; +#endif + + int eContent = frequency - (50 - ECA_ECONTENT_ADDER); return clamp(eContent, 0, 100); } float setVoltageFromEthanol (int ethanol) { - float desiredVoltage = mapf(ethanol, 0, 100, voltageMin, voltageMax); + float desiredVoltage = mapf(ethanol, 0, 100, ECA_VOLTAGE_MIN, ECA_VOLTAGE_MAX); setVoltage(desiredVoltage, false); return desiredVoltage; } @@ -215,17 +222,30 @@ void loop () unsigned long pulseTime = highTime + lowTime; float frequency = float(1000000 / pulseTime); - eContent = getEthanol(pulseTime); - setVoltageFromEthanol(eContent); + int eContent = getEthanol(pulseTime); + float outputVoltage = setVoltageFromEthanol(eContent); - tempC = getTempC(highTime, lowTime); - tempF = cToF(tempC); + int tempC = getTempC(highTime, lowTime); + int tempF = cToF(tempC); + +#if ECA_ENABLE_CAN + if (ecaCan.ready()) { + uint16_t voltageMv = (uint16_t)(outputVoltage * 1000); + ecaCan.send((uint8_t)eContent, frequency, voltageMv, + (int8_t)tempC, ecaStatus); + } +#endif - if (ENABLE_SERIAL == 1) + if (ECA_ENABLE_SERIAL == 1) { - Serial << "Ethanol: " << eContent << "\% • Fuel Temp: " << tempC << "°C (" << tempF << "°F)" << endl; - Serial.println(); + Serial.print(F("Ethanol: ")); + Serial.print(eContent); + Serial.print(F("% • Fuel Temp: ")); + Serial.print(tempC); + Serial.print(F("°C (")); + Serial.print(tempF); + Serial.println(F("°F)")); } - delay(refreshDelay); + delay(ECA_REFRESH_DELAY_MS); countTick = 0; } diff --git a/src/eca_can.h b/src/eca_can.h new file mode 100644 index 0000000..05645aa --- /dev/null +++ b/src/eca_can.h @@ -0,0 +1,175 @@ +/** + * ECA: Ethanol Content Analyzer - CAN Bus Support + * + * Optional CAN bus output module. Only compiled when ECA_ENABLE_CAN is set + * to 1 in eca_config.h. + * + * Requires an MCP2515-based CAN transceiver connected via SPI. + * Uses the mcp_can library (https://github.com/coryjfowler/MCP_CAN_lib). + * ------------------------------------------------------------------------- + * MIT (c) 2021-2026 Nicholas Berlette + */ + +#ifndef ECA_CAN_H +#define ECA_CAN_H + +#include "eca_config.h" + +#if ECA_ENABLE_CAN + +#include +#include + +// ========================================================================= +// Status / error codes (enumerated) +// These correspond 1:1 with the analog error voltages. +// ========================================================================= +// +// Code | Name | Analog Voltage | Condition +// -----|-------------------|----------------|---------------------------- +// 0x00 | STATUS_OK | 0.50-4.50V | Normal valid reading +// 0x01 | STATUS_DISCONNECTED | 0.10V | Sensor disconnected (<= 0 Hz) +// 0x02 | STATUS_CONTAMINATED | 4.80V | Contaminated fuel (< 50 Hz) +// 0x03 | STATUS_HIGH_WATER | 4.90V | High water content (> 150 Hz) +// 0xFF | STATUS_UNKNOWN | — | Unknown / initializing +// ========================================================================= +enum EcaStatus : uint8_t { + ECA_STATUS_OK = 0x00, // Normal valid ethanol reading + ECA_STATUS_DISCONNECTED = 0x01, // Sensor disconnected / no signal + ECA_STATUS_CONTAMINATED = 0x02, // Contaminated fuel (freq < 50 Hz) + ECA_STATUS_HIGH_WATER = 0x03, // High water content (freq > 150 Hz) + ECA_STATUS_UNKNOWN = 0xFF // Unknown / initializing +}; + +// ========================================================================= +// Status bit flags (byte 7 of CAN payload) +// Individual bits for fault conditions — can be combined. +// ========================================================================= +#define ECA_FLAG_SENSOR_OK 0x01 // bit 0: sensor connected & valid +#define ECA_FLAG_DISCONNECTED 0x02 // bit 1: sensor disconnected +#define ECA_FLAG_CONTAMINATED 0x04 // bit 2: contaminated fuel +#define ECA_FLAG_HIGH_WATER 0x08 // bit 3: high water content +#define ECA_FLAG_ERROR 0x80 // bit 7: any error present + +// ========================================================================= +// CAN controller wrapper +// ========================================================================= +class EcaCan { +public: + EcaCan(); + + /** + * Initialize the MCP2515 CAN controller. + * Returns true on success, false on failure. + */ + bool begin(); + + /** + * Send the ECA CAN frame with current sensor data. + * + * @param ethanol Ethanol content (0-100%) + * @param freqHz Sensor frequency in Hz (floating point) + * @param voltageMv Output voltage in millivolts + * @param tempC Fuel temperature in degrees Celsius + * @param status EcaStatus enumerated code + */ + void send(uint8_t ethanol, float freqHz, uint16_t voltageMv, + int8_t tempC, EcaStatus status); + + /** + * Check if enough time has elapsed since the last transmission + * based on ECA_CAN_TX_INTERVAL_MS. Call this in the main loop + * to throttle CAN output independently of the main refresh rate. + */ + bool ready(); + +private: + MCP_CAN _can; + unsigned long _lastTxMs; + + /** Build status bit-flags byte from an EcaStatus code. */ + uint8_t _buildFlags(EcaStatus status); +}; + +// ========================================================================= +// Implementation (header-only for Arduino sketch simplicity) +// ========================================================================= + +EcaCan::EcaCan() + : _can(ECA_CAN_CS_PIN), _lastTxMs(0) {} + +bool EcaCan::begin() { + // CAN_500KBPS corresponds to ECA_CAN_BAUDRATE default of 500 kbps. + // MCP_ANY = accept all incoming messages (we only transmit, but set a sane default). + if (_can.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) == CAN_OK) { + _can.setMode(MCP_NORMAL); + return true; + } + return false; +} + +bool EcaCan::ready() { + unsigned long now = millis(); + if ((now - _lastTxMs) >= ECA_CAN_TX_INTERVAL_MS) { + return true; + } + return false; +} + +void EcaCan::send(uint8_t ethanol, float freqHz, uint16_t voltageMv, + int8_t tempC, EcaStatus status) { + uint8_t data[8]; + + // Byte 0: ethanol content (0-100) + data[ECA_CAN_BYTE_ETHANOL] = ethanol; + + // Bytes 1-2: frequency * 10, big-endian (0.1 Hz resolution) + uint16_t freqScaled = (uint16_t)(freqHz * ECA_CAN_FREQ_SCALE); + data[ECA_CAN_BYTE_FREQ_HI] = (freqScaled >> 8) & 0xFF; + data[ECA_CAN_BYTE_FREQ_LO] = freqScaled & 0xFF; + + // Bytes 3-4: output voltage in mV, big-endian + data[ECA_CAN_BYTE_VOLTAGE_HI] = (voltageMv >> 8) & 0xFF; + data[ECA_CAN_BYTE_VOLTAGE_LO] = voltageMv & 0xFF; + + // Byte 5: fuel temperature (degC + offset), clamped to 0-255 + int16_t tempEncoded = (int16_t)tempC + ECA_CAN_TEMP_OFFSET; + if (tempEncoded < 0) tempEncoded = 0; + if (tempEncoded > 255) tempEncoded = 255; + data[ECA_CAN_BYTE_FUEL_TEMP] = (uint8_t)tempEncoded; + + // Byte 6: enumerated status code + data[ECA_CAN_BYTE_STATUS] = (uint8_t)status; + + // Byte 7: status bit flags + data[ECA_CAN_BYTE_FLAGS] = _buildFlags(status); + + // Transmit standard (11-bit) CAN frame, 8 data bytes + _can.sendMsgBuf(ECA_CAN_ARBID, 0, 8, data); + _lastTxMs = millis(); +} + +uint8_t EcaCan::_buildFlags(EcaStatus status) { + uint8_t flags = 0; + switch (status) { + case ECA_STATUS_OK: + flags = ECA_FLAG_SENSOR_OK; + break; + case ECA_STATUS_DISCONNECTED: + flags = ECA_FLAG_DISCONNECTED | ECA_FLAG_ERROR; + break; + case ECA_STATUS_CONTAMINATED: + flags = ECA_FLAG_CONTAMINATED | ECA_FLAG_ERROR; + break; + case ECA_STATUS_HIGH_WATER: + flags = ECA_FLAG_HIGH_WATER | ECA_FLAG_ERROR; + break; + default: + flags = ECA_FLAG_ERROR; + break; + } + return flags; +} + +#endif // ECA_ENABLE_CAN +#endif // ECA_CAN_H diff --git a/src/eca_config.h b/src/eca_config.h new file mode 100644 index 0000000..614a608 --- /dev/null +++ b/src/eca_config.h @@ -0,0 +1,123 @@ +/** + * ECA: Ethanol Content Analyzer - Configuration + * + * Central configuration header for compile-time options. + * Edit these values to match your hardware setup and preferences. + * ------------------------------------------------------------------------- + * MIT (c) 2021-2026 Nicholas Berlette + */ + +#ifndef ECA_CONFIG_H +#define ECA_CONFIG_H + +// ------------------------------------------------------------------------- +// Firmware version +// ------------------------------------------------------------------------- +#define ECA_VERSION "2.0.0" + +// ------------------------------------------------------------------------- +// Pin assignments +// ------------------------------------------------------------------------- +#define PIN_INPUT_SENSOR 10 // FlexFuel sensor input (Timer1 / ICP1) +#define PIN_OUTPUT_PWM 9 // PWM analog output + +// ------------------------------------------------------------------------- +// Serial / debug output +// ------------------------------------------------------------------------- +#define ECA_ENABLE_SERIAL 1 // 1 = enable serial logging, 0 = disable +#define ECA_SERIAL_BAUDRATE 9600 + +// ------------------------------------------------------------------------- +// Analog output modes +// ------------------------------------------------------------------------- +#define ECA_ENABLE_PWM_OUT 1 // 1 = enable PWM output on PIN_OUTPUT_PWM +#define ECA_ENABLE_DAC_OUT 1 // 1 = enable MCP4725 I2C DAC output +#define ECA_DAC_I2C_ADDR 0x60 // MCP4725 I2C address (0x60 or 0x62/0x63) + +// ------------------------------------------------------------------------- +// Output scaling +// ------------------------------------------------------------------------- +#define PWM_MULTIPLIER 255 // 8-bit PWM range +#define DAC_MULTIPLIER 4095 // 12-bit DAC range + +// ------------------------------------------------------------------------- +// Voltage mapping +// ------------------------------------------------------------------------- +// Ethanol 0% -> VOLTAGE_MIN (default 0.50V) +// Ethanol 100% -> VOLTAGE_MAX (default 4.50V) +#define ECA_VOLTAGE_MIN 0.5 +#define ECA_VOLTAGE_MAX 4.5 +#define ECA_VOLTAGE_RAIL 5.0 // Full-scale voltage + +// ------------------------------------------------------------------------- +// Analog error voltages +// ------------------------------------------------------------------------- +#define ECA_ERROR_V_DISCONNECTED 0.10 // sensor disconnected (<= 0 Hz) +#define ECA_ERROR_V_CONTAMINATED 4.80 // contaminated fuel (< 50 Hz) +#define ECA_ERROR_V_HIGH_WATER 4.90 // high water content (> 150 Hz) + +// ------------------------------------------------------------------------- +// Timing +// ------------------------------------------------------------------------- +#define ECA_REFRESH_DELAY_MS 1000 // main loop delay (ms) + +// ------------------------------------------------------------------------- +// Ethanol calculation offsets +// ------------------------------------------------------------------------- +#define ECA_ECONTENT_ADDER 0 +#define ECA_ECONTENT_FIXED 0 + +// ------------------------------------------------------------------------- +// CAN bus support (opt-in) +// ------------------------------------------------------------------------- +// Set ECA_ENABLE_CAN to 1 to enable CAN bus output. +// Requires an MCP2515-based CAN transceiver module connected via SPI. +// When disabled (default), no CAN code is compiled and behavior is +// identical to the original analog-only firmware. +// ------------------------------------------------------------------------- +#define ECA_ENABLE_CAN 0 // 0 = disabled (default), 1 = enabled + +// CAN arbitration ID (11-bit standard) +#define ECA_CAN_ARBID 0xEC // default: 236 (0xEC) + +// CAN bus speed (bits/sec) +#define ECA_CAN_BAUDRATE 500000 // 500 kbps (most automotive networks) + +// CAN chip-select pin for MCP2515 module +#define ECA_CAN_CS_PIN 10 // SPI chip select (change if shared) + +// CAN transmission interval (ms) — how often CAN frames are sent +#define ECA_CAN_TX_INTERVAL_MS 100 // 10 Hz update rate + +// ------------------------------------------------------------------------- +// CAN payload layout (byte positions within the 8-byte CAN data frame) +// ------------------------------------------------------------------------- +// Byte 0: Ethanol content (0-100%, raw value, scale 1, offset 0) +// Byte 1: Sensor frequency high byte (Hz * 10, big-endian) +// Byte 2: Sensor frequency low byte +// Byte 3: Output voltage high byte (mV, big-endian) +// Byte 4: Output voltage low byte +// Byte 5: Fuel temperature (degC + 40 offset, 0 = -40°C) +// Byte 6: Status / error code (enumerated, see eca_can.h) +// Byte 7: Status bit flags +// ------------------------------------------------------------------------- +#define ECA_CAN_BYTE_ETHANOL 0 +#define ECA_CAN_BYTE_FREQ_HI 1 +#define ECA_CAN_BYTE_FREQ_LO 2 +#define ECA_CAN_BYTE_VOLTAGE_HI 3 +#define ECA_CAN_BYTE_VOLTAGE_LO 4 +#define ECA_CAN_BYTE_FUEL_TEMP 5 +#define ECA_CAN_BYTE_STATUS 6 +#define ECA_CAN_BYTE_FLAGS 7 + +// ------------------------------------------------------------------------- +// CAN signal scaling +// ------------------------------------------------------------------------- +// Frequency is transmitted as (Hz * 10) to provide 0.1 Hz resolution +// Voltage is transmitted as millivolts (mV) +// Temperature is transmitted as (degC + 40) so 0x00 = -40°C +// ------------------------------------------------------------------------- +#define ECA_CAN_FREQ_SCALE 10 // frequency multiplier +#define ECA_CAN_TEMP_OFFSET 40 // temperature offset (degC) + +#endif // ECA_CONFIG_H