diff --git a/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.h b/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.h new file mode 100644 index 0000000..8e00934 --- /dev/null +++ b/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.h @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2018 Nis Wechselberg + * + * 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. + */ + +/** + * Firmware for MiCS-6841-based I2C adapter board. + * Implements a behaviour compatible to the Seeedstudio + * Grove Mutichannel Gas Sensor-library. + * + * Using ATmega core from: + * https://raw.githubusercontent.com/carlosefr/atmega/master/package_carlosefr_atmega_index.json + * + * Pin mapping: + * Heater Enable (out): PB0 / Digital 8 (Active low) + * NH3 resistance (in): PC0 / Analog 0 + * RED resistance (in): PC1 / Analog 1 + * OX resistance (in): PC2 / Analog 2 + * + * SDA: PC4 / Analog 4 + * SCL: PC5 / Analog 5 + */ +#ifndef MiCS6814_V1_H +#define MiCS6814_V1_H + + +// Include Wire library for I2C support +#include +#include + + +// Pin mapping constants +#define HEATER 8 +#define NH3_IN A0 +#define RED_IN A1 +#define OX_IN A2 + + +/* + * I2C commands + * + * Taken from Grove Mutichannel Gas Sensor Library, just some renaming done here + */ +#define CMD_GET_NH3 1 // Retrieve current resistance for NH3 sensor channel +#define CMD_GET_RED 2 // Retrieve current resistance for RED sensor channel +#define CMD_GET_OX 3 // Retrieve current resistance for OX sensor channel +#define CMD_GET_ALL 4 // Retrieve all sensor resistance channels +#define CMD_CHANGE_I2C 5 // Change I2C address of the sensor +#define CMD_READ_EEPROM 6 // Read stored uint16_t data from EEPROM (MSB is transmitted first) +#define CMD_SET_R0 7 // Set custom R0 values +#define CMD_GET_R0 8 // Retrieve current R0 values +#define CMD_GET_R0_DEFAULT 9 // Retrieve default R0 values +#define CMD_CONTROL_LED 10 // Control status LED (no LED on my board, but Grove supports is, so ... meh!) +#define CMD_CONTROL_PWR 11 // Heater control + + +/* EEPROM Addresses + * + * These match the addresses used in the Grove library + * If the library was implemented in a sensible fashion I would + * be free to use my own mapping here. + * However, the library accesses eeprom data directly quite frequently, so ... meh! + */ +#define EEPROM_VERSION_ID 0 // Identifier for the current version, Grove uses value 1126 to identify version 2 of their sensor. +#define EEPROM_R0_DEFAULT_NH3 2 +#define EEPROM_R0_DEFAULT_RED 4 +#define EEPROM_R0_DEFAULT_OX 6 +#define EEPROM_R0_NH3 8 +#define EEPROM_R0_RED 10 +#define EEPROM_R0_OX 12 +#define EEPROM_I2C_ADDR 20 + +#define DATA_VERSION_ID 1126 +#define DATA_I2C_ADDR 0x04 +#define DATA_R0_DEFAULT_NH3 123 +#define DATA_R0_DEFAULT_RED 123 +#define DATA_R0_DEFAULT_OX 123 + + +// Runtime data +uint32_t timer; // Timer value of last update +uint16_t resistanceNH3; // Current resistance value for NH3 channel +uint16_t resistanceRED; // Current resistance value for RED channel +uint16_t resistanceOX; // Current resistance value for OX channel +uint8_t i2CResp[6]; // Buffer for I2C response +uint8_t i2CRespLength; // Amount of bytes to be sent to master + +#endif diff --git a/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.ino b/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.ino new file mode 100644 index 0000000..61bb7f6 --- /dev/null +++ b/MiCS6814-I2C-Firmware-V1/MiCS6814-I2C-Firmware-V1.ino @@ -0,0 +1,266 @@ +/* + * MIT License + * + * Copyright (c) 2018 Nis Wechselberg + * + * 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. + */ + +/** + * Firmware for MiCS-6841-based I2C adapter board. + * Implements a behaviour compatible to the Seeedstudio + * Grove Mutichannel Gas Sensor-library. + */ + +#include "MiCS6814-I2C-Firmware-V1.h" + +/** + * EEPROM helper function to write a single uint16_t value + * to two adjacient eeprom cells. + * The upper half (MSB) will be written to the given address, + * the lower half (LSB) will be written to the next address. + * + * @param addr + * The eeprom addr to store the MSB in. + * The next eeprom address will be used for the LSB. + * @param value + * The uint16_t value to store. + */ +void eeprom_write16(uint16_t addr, uint16_t value) { + // Store MSB in address, shift value and mask + EEPROM.write(addr, (value >> 8) & 0xFF); + // Store LSB in next address + EEPROM.write(addr + 1, value & 0xFF); +} + +/** + * EEPROM helper function to read a single uint16_t value + * from two adjacient eeprom cells. + * + * @param addr + * The eeprom addr to read the MSB from. + * The the LSB will be read from the next eeprom address. + * @return The stored uint16_t value. + */ +uint16_t eeprom_read16(uint16_t addr) { + // Read two bytes separately + uint8_t msb = EEPROM.read(addr); + uint8_t lsb = EEPROM.read(addr + 1); + + // Combine through shifting and bitwise-or + return (msb << 8) | lsb; +} + +/** + * Initialize EEPROM values if not already written + */ +void eeprom_init() { + // Check if data has already been stored in EEPROM + if (eeprom_read16(EEPROM_VERSION_ID)) { + eeprom_write16(EEPROM_VERSION_ID, DATA_VERSION_ID); + eeprom_write16(EEPROM_I2C_ADDR, DATA_I2C_ADDR); + eeprom_write16(EEPROM_R0_DEFAULT_NH3, DATA_R0_DEFAULT_NH3); + eeprom_write16(EEPROM_R0_DEFAULT_RED, DATA_R0_DEFAULT_RED); + eeprom_write16(EEPROM_R0_DEFAULT_OX, DATA_R0_DEFAULT_OX); + eeprom_write16(EEPROM_R0_NH3, DATA_R0_DEFAULT_NH3); + eeprom_write16(EEPROM_R0_RED, DATA_R0_DEFAULT_RED); + eeprom_write16(EEPROM_R0_OX, DATA_R0_DEFAULT_OX); + } +} + +/** + * Read the analog pin repeatedly and return the average value. + * + * @param pin + * The (analog) pin to read from. + * @return The average value read. + */ +uint16_t analogReadAvg(uint8_t pin) { + // Analog read returns value between 0 and 1024, so at most 10 bits are needed. + // If we read at most 64 times we can just store that in 16 bits of a uint16_t. + + uint16_t sum = 0; + for (int c = 0; c < 16; ++c) { + sum += analogRead(pin); + } + + // Shift 4 bits right to calculate average (div by 16) + return (sum >> 4); +} + +/** + * Handler for data received from the master. + * + * @param availData + * Number of bytes received from the master + */ +void receiveHandler(uint16_t numBytes) { + // Check possible package sizes + uint8_t cmd = 0; + uint8_t data = 0; + uint16_t dataArr[3]; + + switch (numBytes) { + case 1: + // A single byte usually means we are going to get a request next. + // All we need to do is prepare the data for the requestHandler + cmd = Wire.read(); + // Check the command from master + switch (cmd) { + case CMD_GET_NH3: + i2CResp[0] = resistanceNH3 >> 8; + i2CResp[1] = resistanceNH3 & 0xFF; + i2CRespLength = 2; + break; + case CMD_GET_RED: + i2CResp[0] = resistanceRED >> 8; + i2CResp[1] = resistanceRED & 0xFF; + i2CRespLength = 2; + break; + case CMD_GET_OX: + i2CResp[0] = resistanceOX >> 8; + i2CResp[1] = resistanceOX & 0xFF; + i2CRespLength = 2; + break; + case CMD_GET_ALL: + i2CResp[0] = resistanceNH3 >> 8; + i2CResp[1] = resistanceNH3 & 0xFF; + i2CResp[2] = resistanceRED >> 8; + i2CResp[3] = resistanceRED & 0xFF; + i2CResp[4] = resistanceOX >> 8; + i2CResp[5] = resistanceOX & 0xFF; + i2CRespLength = 6; + break; + case CMD_GET_R0: + i2CResp[0] = EEPROM.read(EEPROM_R0_NH3); + i2CResp[1] = EEPROM.read(EEPROM_R0_NH3 + 1); + i2CResp[2] = EEPROM.read(EEPROM_R0_RED); + i2CResp[3] = EEPROM.read(EEPROM_R0_RED + 1); + i2CResp[4] = EEPROM.read(EEPROM_R0_OX); + i2CResp[5] = EEPROM.read(EEPROM_R0_OX + 1); + i2CRespLength = 6; + break; + case CMD_GET_R0_DEFAULT: + i2CResp[0] = EEPROM.read(EEPROM_R0_DEFAULT_NH3); + i2CResp[1] = EEPROM.read(EEPROM_R0_DEFAULT_NH3 + 1); + i2CResp[2] = EEPROM.read(EEPROM_R0_DEFAULT_RED); + i2CResp[3] = EEPROM.read(EEPROM_R0_DEFAULT_RED + 1); + i2CResp[4] = EEPROM.read(EEPROM_R0_DEFAULT_OX); + i2CResp[5] = EEPROM.read(EEPROM_R0_DEFAULT_OX + 1); + i2CRespLength = 6; + break; + default: + break; + } + break; + case 2: + // Two bytes from the master could mean multiple things + cmd = Wire.read(); + data = Wire.read(); + // Check the command from master + switch (cmd) { + case CMD_CHANGE_I2C: + // Store new I2C address and restart connection + eeprom_write16(EEPROM_I2C_ADDR, data); + Wire.begin(data); + break; + case CMD_READ_EEPROM: + // Prepare the data from eeprom for the next request + i2CResp[0] = EEPROM.read(data); + i2CResp[1] = EEPROM.read(data + 1); + i2CRespLength = 2; + break; + case CMD_CONTROL_PWR: + digitalWrite(HEATER, data ? LOW : HIGH); + break; + default: + break; + } + break; + case 7: + // Seven bytes from the master are only used to set new R0 values + cmd = Wire.read(); + for (uint8_t i = 0; i < 3; ++i) { + uint8_t msb = Wire.read(); + uint8_t lsb = Wire.read(); + dataArr[i] = (msb << 8) | lsb; + } + + if (cmd == CMD_SET_R0) { + eeprom_write16(EEPROM_R0_NH3, dataArr[0]); + eeprom_write16(EEPROM_R0_RED, dataArr[1]); + eeprom_write16(EEPROM_R0_OX, dataArr[2]); + } + break; + default: + break; + } +} + +/** + * Handler for sending data to the master. + * Requires data to be prepared by receiveHandler. + */ +void requestHandler() { + Wire.write(i2CResp, i2CRespLength); +} + +/** + * Setup code for Arduino envrionment. + * Called once at startup. + */ +void setup() { + // Prepare data pins + pinMode(NH3_IN, INPUT); + pinMode(RED_IN, INPUT); + pinMode(OX_IN, INPUT); + // Prepare heater pin + pinMode(HEATER, OUTPUT); + // Initially disable heater + digitalWrite(HEATER, HIGH); + // Prepare EEPROM if needed + eeprom_init(); + // Read I2C address from EEPROM + uint8_t i2cAddr = eeprom_read16(EEPROM_I2C_ADDR); + + // Connect to I2C bus + Wire.begin(i2cAddr); + // Register callbacks + Wire.onReceive(receiveHandler); + Wire.onRequest(requestHandler); +} + +/** + * Default arduino event loop + */ +void loop() { + // Check if the timer is expired + uint32_t newTime = millis(); + if (newTime - timer > 1000) { + // Timer is expired or overflown + // (overflow of unsigned values is properly handled automatically) + timer = newTime; + + // Update the values repeatedly + resistanceNH3 = analogReadAvg(NH3_IN); + resistanceRED = analogReadAvg(RED_IN); + resistanceOX = analogReadAvg(OX_IN); + } +} +