From ac60943b6225762645ce7dba97a0650e70c5f784 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 10 Jul 2021 16:47:45 +0200 Subject: [PATCH] Add Arduino and ESP32 code --- Arduino/DMAC.h | 46 ++ Arduino/I2C_DMAC.cpp | 734 ++++++++++++++++++++++++++++++++ Arduino/I2C_DMAC.h | 141 ++++++ Arduino/NoiseSensor.cpp | 284 ++++++++++++ Arduino/NoiseSensor.h | 90 ++++ Arduino/NoiseSensorNode.cpp | 11 + Arduino/NoiseSensorNode.h | 21 + Arduino/noisesensor_arduino.ino | 225 ++++++++++ ESP32/NoiseSensor.cpp | 268 ++++++++++++ ESP32/NoiseSensor.h | 91 ++++ ESP32/NoiseSensorNode.cpp | 11 + ESP32/NoiseSensorNode.h | 21 + ESP32/noisesensor_esp32.ino | 140 ++++++ 13 files changed, 2083 insertions(+) create mode 100644 Arduino/DMAC.h create mode 100644 Arduino/I2C_DMAC.cpp create mode 100644 Arduino/I2C_DMAC.h create mode 100644 Arduino/NoiseSensor.cpp create mode 100644 Arduino/NoiseSensor.h create mode 100644 Arduino/NoiseSensorNode.cpp create mode 100644 Arduino/NoiseSensorNode.h create mode 100644 Arduino/noisesensor_arduino.ino create mode 100644 ESP32/NoiseSensor.cpp create mode 100644 ESP32/NoiseSensor.h create mode 100644 ESP32/NoiseSensorNode.cpp create mode 100644 ESP32/NoiseSensorNode.h create mode 100644 ESP32/noisesensor_esp32.ino diff --git a/Arduino/DMAC.h b/Arduino/DMAC.h new file mode 100644 index 0000000..78294f0 --- /dev/null +++ b/Arduino/DMAC.h @@ -0,0 +1,46 @@ +/* + DMAC descriptor configuration file. + + Copyright (C) Martin Lindupp 2018 + + V1.0.0 -- Initial release + V1.0.1 -- Include SAMD51 support for 32 channels + V1.0.2 -- Allow other classes to simultaneously use remaining DMAC channels + + The MIT License (MIT) + + 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. +*/ + +// DMAC descriptor configuration file + +#ifndef _DMAC_h +#define _DMAC_h + +typedef struct { // DMAC descriptor structure + uint16_t btctrl; + uint16_t btcnt; + uint32_t srcaddr; + uint32_t dstaddr; + uint32_t descaddr; +} dmacdescriptor ; + +extern volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16))); // DMAC write back descriptor array +extern dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16))); // DMAC channel descriptor array +extern dmacdescriptor descriptor __attribute__ ((aligned (16))); // DMAC place holder descriptor + +#endif diff --git a/Arduino/I2C_DMAC.cpp b/Arduino/I2C_DMAC.cpp new file mode 100644 index 0000000..21d6554 --- /dev/null +++ b/Arduino/I2C_DMAC.cpp @@ -0,0 +1,734 @@ +/* + I2C_DMAC is a non-blocking I2C library that uses a SERCOM in + conjunction with the Direct Memory Access Controller (DMAC). + + Copyright (C) Martin Lindupp 2018 + + V1.0.0 -- Initial release + V1.1.0 -- Add Arduino MKR and SAMD51 support, plus multiple I2C instances + V1.1.1 -- Replaced pinPeripheral() function with port register manipulation + V1.1.2 -- Allow other classes to simultaneously use remaining DMAC channels + V1.1.3 -- Fixed issue with consecutive calls to writeByte() overwriting data + V1.1.4 -- Allow the DMAC to resume normal operation after an early NACK is received + V1.1.5 -- Activate internal pull-up resistors and increase driver strength + V1.1.6 -- Add SERCOM ALT (alternative) peripheral switch for the Metro M4 + V1.1.7 -- Arduino IDE library manager release + V1.1.8 -- Code optimisation + V1.1.9 -- Use default arguments for begin() member function + V1.1.10 -- Remove I2C instance on writeBusy flag in ReadByte() and ReadBytes() functions + V1.1.11 -- Arduino IDE library manager release + + The MIT License (MIT) + 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. +*/ + +#include "I2C_DMAC.h" + +volatile I2C_DMAC* I2C_DMAC::i2cDmacPtrs[SERCOM_INST_NUM]; // Array of pointer to each instance (object) of this class +volatile uint8_t I2C_DMAC::instanceCounter; // Number of instances (objects) of this class + +volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16))); // DMAC write back descriptor array +dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16))); // DMAC channel descriptor array +dmacdescriptor descriptor __attribute__ ((aligned (16))); // DMAC place holder descriptor + +// +// Initialisation section: prep the the port pins, SERCOM and the DMAC +// +I2C_DMAC::I2C_DMAC(SERCOM* sercomClass, uint8_t pinSDA, uint8_t pinSCL) : // Constructor to initialise SERCOM, pins and DMAC channel member variables + dmacWriteChannel(0), dmacReadChannel(1), dmacPriority(0) +{ + this->pinSCL = pinSCL; // Copy the I2C pins + this->pinSDA = pinSDA; + + if (sercomClass == &sercom0) // Use the sercom class to acquire the underlying SERCOMx peripheral + { + sercom = SERCOM0; // This is used to allow the variant.h and variant.cpp files to + dmacWriteTrigger = SERCOM0_DMAC_ID_TX; // specify the sercom peripheral and the SCL/SDA pins used, just like + dmacReadTrigger = SERCOM0_DMAC_ID_RX; // the Wire library +#ifdef __SAMD51__ + genericClockId = SERCOM0_GCLK_ID_CORE; + nvicId = SERCOM0_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM0_CORE; + nvicId = SERCOM0_IRQn; +#endif + } + else if (sercomClass == &sercom1) // The DMAC read and write trigger source as well as the generic clock id + { // and sercom's NVIC interrupt id are also copied + sercom = SERCOM1; + dmacWriteTrigger = SERCOM1_DMAC_ID_TX; + dmacReadTrigger = SERCOM1_DMAC_ID_RX; +#ifdef __SAMD51__ + genericClockId = SERCOM1_GCLK_ID_CORE; + nvicId = SERCOM1_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM1_CORE; + nvicId = SERCOM1_IRQn; +#endif + } + else if (sercomClass == &sercom2) + { + sercom = SERCOM2; + dmacWriteTrigger = SERCOM2_DMAC_ID_TX; + dmacReadTrigger = SERCOM2_DMAC_ID_RX; +#ifdef __SAMD51__ + genericClockId = SERCOM2_GCLK_ID_CORE; + nvicId = SERCOM2_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM2_CORE; + nvicId = SERCOM2_IRQn; +#endif + } + else if (sercomClass == &sercom3) + { + sercom = SERCOM3; + dmacWriteTrigger = SERCOM3_DMAC_ID_TX; + dmacReadTrigger = SERCOM3_DMAC_ID_RX; +#ifdef __SAMD51__ + genericClockId = SERCOM3_GCLK_ID_CORE; + nvicId = SERCOM3_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM3_CORE; + nvicId = SERCOM3_IRQn; +#endif + } + else if (sercomClass == &sercom4) + { + sercom = SERCOM4; + dmacWriteTrigger = SERCOM4_DMAC_ID_TX; + dmacReadTrigger = SERCOM4_DMAC_ID_RX; +#ifdef __SAMD51__ + genericClockId = SERCOM4_GCLK_ID_CORE; + nvicId = SERCOM4_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM4_CORE; + nvicId = SERCOM4_IRQn; +#endif + } + else if (sercomClass == &sercom5) + { + sercom = SERCOM5; + dmacWriteTrigger = SERCOM5_DMAC_ID_TX; + dmacReadTrigger = SERCOM5_DMAC_ID_RX; +#ifdef __SAMD51__ + genericClockId = SERCOM5_GCLK_ID_CORE; + nvicId = SERCOM5_3_IRQn; +#else + genericClockId = GCLK_CLKCTRL_ID_SERCOM5_CORE; + nvicId = SERCOM5_IRQn; +#endif + } +} + +void I2C_DMAC::begin(uint32_t baudrate, uint8_t regAddrMode, EPioType ulPeripheral) // Set baud rate and the register address mode: 8-bit or 16-bit +{ + if (ulPeripheral != PIO_SERCOM && ulPeripheral != PIO_SERCOM_ALT) // Check that the peripheral mutliplexer is set to a SERCOM or SERCOM_ALT + { + return; + } + + this->regAddrMode = regAddrMode; + + // Enable the SCL and SDA pins on the sercom: includes increased driver strength, pull-up resistors and pin multiplexer + PORT->Group[g_APinDescription[pinSCL].ulPort].PINCFG[g_APinDescription[pinSCL].ulPin].reg = + PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN; + PORT->Group[g_APinDescription[pinSDA].ulPort].PINCFG[g_APinDescription[pinSDA].ulPin].reg = + PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN; + PORT->Group[g_APinDescription[pinSDA].ulPort].PMUX[g_APinDescription[pinSDA].ulPin >> 1].reg = + PORT_PMUX_PMUXO(ulPeripheral) | PORT_PMUX_PMUXE(ulPeripheral); + + if (!DMAC->CTRL.bit.DMAENABLE) // Enable the DMAC, if it hasn't already been enabled + { +#ifdef __SAMD51__ + for (uint8_t i = 0; i < 5; i++) // Iterate through the SAMD51's 5 DMAC interrupt channels + { + NVIC_ClearPendingIRQ((IRQn_Type)(DMAC_0_IRQn + i)); // Clear any pending DMAC interrupts + NVIC_SetPriority((IRQn_Type)(DMAC_0_IRQn + i), 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for the DMAC to 0 (highest) + NVIC_EnableIRQ((IRQn_Type)(DMAC_0_IRQn + i)); // Connect the DMAC to the Nested Vector Interrupt Controller (NVIC) + } +#else + NVIC_ClearPendingIRQ(DMAC_IRQn); // Clear any pending DMAC interrupts + NVIC_SetPriority(DMAC_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for the DMAC to 0 (highest) + NVIC_EnableIRQ(DMAC_IRQn); // Connect the DMAC to the Nested Vector Interrupt Controller (NVIC) +#endif + DMAC->CTRL.bit.SWRST = 1; // Reset the DMAC + while (DMAC->CTRL.bit.SWRST); // Wait for synchronization + DMAC->BASEADDR.reg = (uint32_t)descriptor_section; // Set the DMAC descriptor base address + DMAC->WRBADDR.reg = (uint32_t)wrb; // Set the DMAC descriptor write-back address + DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); // Enable the DMAC peripheral and enable priority levels + } + + NVIC_ClearPendingIRQ(nvicId); // Clear any Nested Vector Interrupt Controller (NVIC) pending interrupts + NVIC_SetPriority(nvicId, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for the selected sercom to 0 (highest) + NVIC_EnableIRQ(nvicId); // Connect selected sercom to the Nested Vector Interrupt Controller (NVIC) + +#ifdef __SAMD51__ + // Enable GCLK1 (48MHz) on the selected sercom + GCLK->PCHCTRL[genericClockId].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN_GCLK1; +#else + // Enable GCLK0 (48MHz) on the selected sercom + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Enable GCLK0 to the selected sercom + GCLK_CLKCTRL_GEN_GCLK0 | // Select GCLK0 as source + GCLK_CLKCTRL_ID(genericClockId); // Select the selected sercom as destination + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization +#endif + + sercom->I2CM.CTRLA.bit.SWRST = 1; // Reset the SERCOM + while (sercom->I2CM.CTRLA.bit.SWRST || sercom->I2CM.SYNCBUSY.bit.SWRST); // Wait for synchronization + + sercom->I2CM.CTRLA.reg = SERCOM_I2CM_CTRLA_MODE(I2C_MASTER_OPERATION); // Set I2C master mode + sercom->I2CM.CTRLB.reg = SERCOM_I2CM_CTRLB_SMEN; // Enable Smart Mode + sercom->I2CM.BAUD.bit.BAUD = SERCOM_FREQ_REF / (2 * baudrate) - 7; // Set I2C master SCL baud rate + sercom->I2CM.CTRLA.bit.ENABLE = 1 ; // Enable SERCOM in I2C master mode + while (sercom->I2CM.SYNCBUSY.bit.ENABLE); // Wait for synchronization + sercom->I2CM.STATUS.bit.BUSSTATE = 0x01; // Set the I2C bus to IDLE state + while (sercom->I2CM.SYNCBUSY.bit.SYSOP); // Wait for synchronization + sercom->I2CM.INTENSET.bit.ERROR = 1; // Enable SERCOM ERROR interrupts + + i2cDmacPtrs[instanceCounter++] = this; // Copy the pointer to "this" object to the I2C_DMAC pointer array +} // and increment the instance counter + +// +// Tear down and tidy up resources +// +void I2C_DMAC::end() +{ + for (uint8_t i = 0; i < instanceCounter; i++) // Iterate through each I2C_DMAC instance + { + if (i2cDmacPtrs[i] == this) // Find the location of "this" instance in the I2C_DMAC array pointer + { + i2cDmacPtrs[i] = 0; // Pull this instance pointer from the array + if (i < instanceCounter - 1) // If the current instance not the last instance + { + for (uint8_t j = i; j < instanceCounter - 1; j++) + { + i2cDmacPtrs[j] = i2cDmacPtrs[j + 1]; // Shift the I2C_DMAC pointer down the array + } + i2cDmacPtrs[instanceCounter - 1] = 0; // Set the last instance pointer to NULL + } + instanceCounter--; // Decrement the instance counter + break; // Escape from the (for) loop + } + } + + // Return the SCL and SDA lines on the sercom to GPIO + PORT->Group[g_APinDescription[pinSCL].ulPort].PINCFG[g_APinDescription[pinSCL].ulPin].reg = 0; + PORT->Group[g_APinDescription[pinSDA].ulPort].PINCFG[g_APinDescription[pinSDA].ulPin].reg = 0; + PORT->Group[g_APinDescription[pinSDA].ulPort].PMUX[g_APinDescription[pinSDA].ulPin >> 1].reg = + PORT_PMUX_PMUXO(0) | PORT_PMUX_PMUXE(0); + + sercom->I2CM.CTRLA.bit.ENABLE = 0; // Disable the I2C master mode + while (sercom->I2CM.SYNCBUSY.bit.ENABLE); // Wait for synchronization + sercom->I2CM.CTRLA.bit.SWRST = 1; // Reset SERCOM3 + while (sercom->I2CM.CTRLA.bit.SWRST || sercom->I2CM.SYNCBUSY.bit.SWRST); // Wait for synchronization + +#ifdef __SAMD51__ + // Disable GCLK1 (48Mhz) on the selected sercom + GCLK->PCHCTRL[genericClockId].reg = /*GCLK_PCHCTRL_CHEN |*/ GCLK_PCHCTRL_GEN_GCLK1; +#else + // Disable GCLK0 (48MHz) on the selected sercom + GCLK->CLKCTRL.reg = /*GCLK_CLKCTRL_CLKEN |*/ // Disable GCLK0 to the selected sercom - intentionally commented out + GCLK_CLKCTRL_GEN_GCLK0 | // Select GCLK0 as source + GCLK_CLKCTRL_ID(genericClockId); // Select the selected sercom as destination + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization +#endif + + if (instanceCounter == 0) + { + NVIC_DisableIRQ(nvicId); // Disconnect selected sercom from the Nested Vector Interrupt Controller (NVIC) + + DMAC->CTRL.bit.DMAENABLE = 0; // Disable the DMAC + while(DMAC->CTRL.bit.DMAENABLE); // Wait for synchronization + DMAC->CTRL.bit.SWRST = 1; // Reset the DMAC + while (DMAC->CTRL.bit.SWRST); // Wait for synchronization +#ifdef __SAMD51__ + for (uint8_t i = 0; i < 5; i++) // Iterate through the SAMD51's 5 DMAC interrupt channels + { + NVIC_DisableIRQ((IRQn_Type)(DMAC_0_IRQn + i)); // Disconnect the DMAC from the Nested Vector Interrupt Controller (NVIC) + } +#else + NVIC_DisableIRQ(DMAC_IRQn); // Disconnect the DMAC from the Nested Vector Interrupt Controller (NVIC) +#endif + } +} + +void I2C_DMAC::setClock(uint32_t baudrate) +{ + sercom->I2CM.CTRLA.bit.ENABLE = 0; // Disable SERCOM3 in I2C master mode + while (sercom->I2CM.SYNCBUSY.bit.ENABLE); // Wait for synchronization + sercom->I2CM.BAUD.bit.BAUD = SERCOM_FREQ_REF / (2 * baudrate) - 7; // Set I2C master SCL baud rate + sercom->I2CM.CTRLA.bit.ENABLE = 1 ; // Enable SERCOM3 in I2C master mode + while (sercom->I2CM.SYNCBUSY.bit.ENABLE); // Wait for synchronization +} + +void I2C_DMAC::setWriteChannel(uint8_t channel) +{ + dmacWriteChannel = channel < DMAC_CH_NUM ? channel : dmacWriteChannel; // Set the write DMAC channel, (default channel 0) +} + +void I2C_DMAC::setReadChannel(uint8_t channel) +{ + dmacReadChannel = channel < DMAC_CH_NUM ? channel : dmacReadChannel; // Set the read DMAC channel, (default channel 1) +} + +void I2C_DMAC::setPriority(uint8_t priority) +{ + dmacPriority = priority < DMAC_LVL_NUM ? priority : dmacPriority; // Set the priority of both write and read channels (0 lowest, 3 highest) +} + +// +// DMAC Section: Load the DMAC's transfer descriptors +// +void I2C_DMAC::setRegAddrMode(uint8_t regAddrMode) +{ + this->regAddrMode = regAddrMode; // Set the register address mode: REG_ADDR_8BIT or REG_ADDR_16BIT +} + +// Generic initialise write DMAC transfer function +void I2C_DMAC::initWriteBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count, uint8_t regAddrLength) +{ + while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2); // Wait while the I2C bus is BUSY + if (sercom->I2CM.STATUS.bit.BUSSTATE == 0x1) // Check if the I2C bus state is at IDLE + { + this->devAddress = devAddress; // Copy the device address, plus write byte count and register address length + writeCount = count; + this->regAddrLength = regAddrLength; +#ifdef __SAMD51__ + // Set the DMAC level, trigger source and trigger action to burst (trigger for every byte transmitted) + DMAC->Channel[dmacWriteChannel].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(dmacWriteTrigger) | DMAC_CHCTRLA_TRIGACT_BURST; + DMAC->Channel[dmacWriteChannel].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL(dmacPriority); // Set the channel priority level + DMAC->Channel[dmacWriteChannel].CHINTENSET.reg = DMAC_CHINTENSET_MASK; // Enable all 3 interrupts: SUSP, TCMPL and TERR +#else + DMAC->CHID.reg = DMAC_CHID_ID(dmacWriteChannel); // Activate specified DMAC write channel + // Set the DMAC level, trigger source and trigger action to beat (trigger for every byte transmitted) + DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(dmacPriority) | DMAC_CHCTRLB_TRIGSRC(dmacWriteTrigger) | DMAC_CHCTRLB_TRIGACT_BEAT; + DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK; // Enable all 3 interrupts: SUSP, TCMPL and TERR +#endif + if (regAddrLength == 0) // Writing data only + { + descriptor.descaddr = 0; // Set this to the last descriptor (no linked list) + descriptor.srcaddr = (uint32_t)data + count; // Set the source address + descriptor.dstaddr = (uint32_t)&sercom->I2CM.DATA.reg; // Set the destination address + descriptor.btcnt = count; // Number of data bytes to transmit + descriptor.btctrl = DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID; // Increment source address on BEAT transfer and validate descriptor + memcpy(&descriptor_section[dmacWriteChannel], &descriptor, sizeof(dmacdescriptor)); // Copy the descriptor into SRAM descriptor array + return; // Data only initialisation complete + } + else if (regAddrLength == 1) // 8-bit write address mode + { + this->regAddress[0] = (uint8_t)regAddress; // Copy the 8-bit register address + } + else // 16-bit write address mode + { + this->regAddress[0] = (uint8_t)(regAddress >> 8); // Copy the 16-bit register address MSB + this->regAddress[1] = (uint8_t)(regAddress & 0xFF); // Copy the 16-bit register address LSB + } + descriptor.descaddr = count > 0 ? (uint32_t)&linked_descriptor : 0; // Link to next descriptor if there's data + descriptor.srcaddr = (uint32_t)this->regAddress + regAddrLength; // Set the source address + descriptor.dstaddr = (uint32_t)&sercom->I2CM.DATA.reg; // Set the destination address + descriptor.btcnt = regAddrLength; // Size of the register address in bytes + descriptor.btctrl = DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID; // Increment source address on BEAT transfer and validate descriptor + memcpy(&descriptor_section[dmacWriteChannel], &descriptor, sizeof(dmacdescriptor)); // Copy the descriptor into SRAM descriptor array + + if (count > 0) // Append write data as linked descriptor + { + linked_descriptor.descaddr = 0; // Set linked_descriptor to last in the list + linked_descriptor.srcaddr = (uint32_t)data + count; // Set the source address + linked_descriptor.dstaddr = (uint32_t)&sercom->I2CM.DATA.reg; // Set the destination address + linked_descriptor.btcnt = count; // Number of data bytes to transmit + linked_descriptor.btctrl = DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID; // Increment source address on BEAT transfer and validate descriptor + } + } +} + +// +// Base (1st) layer functions - Independent DMAC initialisation with separate read/write +// +void I2C_DMAC::initWriteBytes(uint8_t devAddress, uint8_t* data, uint8_t count) +{ + initWriteBytes(devAddress, 0, data, count, 0); // Initialise DMAC write transfer: data only, no register address +} + +void I2C_DMAC::initWriteBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count) +{ + initWriteBytes(devAddress, regAddress, data, count, regAddrMode); // Initialise DMAC write transfer: register address + data +} + +void I2C_DMAC::initWriteByte(uint8_t devAddress, uint16_t regAddress, uint8_t data) +{ + while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2); // Wait while the I2C bus is BUSY + if (sercom->I2CM.STATUS.bit.BUSSTATE == 0x1) // Check if the I2C bus state is at IDLE + { + this->data = data; + initWriteBytes(devAddress, regAddress, (uint8_t*)&this->data, 1, regAddrMode); // Initialise DMAC write transfer: register address + 1 data byte + } +} + +void I2C_DMAC::initWriteRegAddr(uint8_t devAddress, uint16_t regAddress) +{ + initWriteBytes(devAddress, regAddress, 0, 0, regAddrMode); // Initialise DMAC write transfer: register address only, no data +} + +uint8_t I2C_DMAC::getData() +{ + return data; // Return the received data byte +} + +void I2C_DMAC::initReadBytes(uint8_t devAddress, uint8_t* data, uint8_t count) // Initialise DMAC read transfer: count bytes of data +{ + while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2); // Wait while the I2C bus is BUSY + if (sercom->I2CM.STATUS.bit.BUSSTATE == 0x1) // Check if the I2C bus state is at IDLE + { + this->devAddress = devAddress; // Copy device address and read byte count + readCount = count; +#ifdef __SAMD51__ + // Set the DMAC level, trigger source and trigger action to burst (trigger for every byte received) + DMAC->Channel[dmacReadChannel].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(dmacReadTrigger) | DMAC_CHCTRLA_TRIGACT_BURST; + DMAC->Channel[dmacReadChannel].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL(dmacPriority); // Set the channel priority level + DMAC->Channel[dmacReadChannel].CHINTENSET.reg = DMAC_CHINTENSET_MASK; // Enable all 3 interrupts: SUSP, TCMPL and TERR +#else + DMAC->CHID.reg = DMAC_CHID_ID(dmacReadChannel); // Activate the specified DMAC channel + // Set the DMAC level, trigger source and trigger action to beat (trigger for every byte received) + DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(dmacPriority) | DMAC_CHCTRLB_TRIGSRC(dmacReadTrigger) | DMAC_CHCTRLB_TRIGACT_BEAT; + DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK; // Enable all 3 interrupts: SUSP, TCMPL and TERR +#endif + descriptor.descaddr = 0; // Single descriptor (no linked list) + descriptor.srcaddr = (uint32_t)&sercom->I2CM.DATA.reg; // Set the source address + descriptor.dstaddr = (uint32_t)data + count; // Set the destination address + descriptor.btcnt = count; // Number of data bytes to receive + descriptor.btctrl = DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID; // Increment destination address on BEAT transfer and validate descriptor + memcpy(&descriptor_section[dmacReadChannel], &descriptor, sizeof(dmacdescriptor)); // Copy the descriptor into SRAM descriptor array + } +} + +void I2C_DMAC::initReadByte(uint8_t devAddress) +{ + this->devAddress = devAddress; + initReadBytes(devAddress, &data, 1); // Initialise DMAC read transfer: 1 byte of data +} + +void I2C_DMAC::write() // Initiate DMAC write transmission on the I2C bus +{ + while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2); // Wait while the I2C bus is BUSY + if (sercom->I2CM.STATUS.bit.BUSSTATE == 0x1) // Check if the I2C bus state is at IDLE + { + writeBusy = true; +#ifdef __SAMD51__ + DMAC->Channel[dmacWriteChannel].CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; // Enable the DMAC write channel +#else + DMAC->CHID.reg = DMAC_CHID_ID(dmacWriteChannel); // Activate the DMAC write channel + DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; // Enable the DMAC write channel +#endif + + sercom->I2CM.ADDR.reg = SERCOM_I2CM_ADDR_LEN(writeCount + regAddrLength) | // Load the device address into the SERCOM3 ADDR register + SERCOM_I2CM_ADDR_LENEN | + SERCOM_I2CM_ADDR_ADDR(devAddress << 1 | WRITE); + while (sercom->I2CM.SYNCBUSY.bit.SYSOP); // Wait for synchronization + } +} + +void I2C_DMAC::read() // Initiate DMAC read transmission on the I2C bus +{ + while (sercom->I2CM.STATUS.bit.BUSSTATE == 0x2); // Wait while the I2C bus is BUSY + if (sercom->I2CM.STATUS.bit.BUSSTATE == 0x1) // Check if the I2C bus state is at IDLE + { + readBusy = true; +#ifdef __SAMD51__ + DMAC->Channel[dmacReadChannel].CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; // Enable the DMAC read channel +#else + DMAC->CHID.reg = DMAC_CHID_ID(dmacReadChannel); // Activate the DMAC read channel + DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; // Enable the DMAC read channel +#endif + + sercom->I2CM.ADDR.reg = SERCOM_I2CM_ADDR_LEN(readCount) | // Load the I2C slave device address into the SERCOM3 ADDR register + SERCOM_I2CM_ADDR_LENEN | + SERCOM_I2CM_ADDR_ADDR(devAddress << 1 | READ); + while (sercom->I2CM.SYNCBUSY.bit.SYSOP); // Wait for synchronization + } +} + +// +// 2nd layer functions - Combined DMAC initialisation with read or write +// +void I2C_DMAC::writeBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count) +{ + initWriteBytes(devAddress, regAddress, data, count); // Initialise DMAC write transfer: register address + data + write(); // Transmit the register address + data +} + +void I2C_DMAC::writeByte(uint8_t devAddress, uint16_t regAddress, uint8_t data) +{ + initWriteByte(devAddress, regAddress, data); // Initialise DMAC write transfer: register address + 1 byte data + write(); // Transmit the register address + data +} + +void I2C_DMAC::writeRegAddr(uint8_t devAddress, uint16_t regAddress) +{ + initWriteRegAddr(devAddress, regAddress); // Initialise DMAC write transfer: register address, no data + write(); // Transmit the register address +} + +void I2C_DMAC::readBytes(uint8_t devAddress, uint8_t* data, uint8_t count) +{ + initReadBytes(devAddress, data, count); // Initialise DMAC read transfer: data + read(); // Receive the data +} + +void I2C_DMAC::readByte(uint8_t devAddress) +{ + initReadByte(devAddress); // Initialise DMAC read transfer: 1 byte data + read(); // Receive the data +} + +// +// 3rd layer functions - Combined DMAC initialisation with read and write +// +void I2C_DMAC::readBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count) +{ + writeRegAddr(devAddress, regAddress); // Set the register address on the I2C slave device + while(writeBusy); // Wait for the write to complete + readBytes(devAddress, data, count); // Receive the returned data +} + +void I2C_DMAC::readByte(uint8_t devAddress, uint16_t regAddress) +{ + writeRegAddr(devAddress, regAddress); // Set the register address on the I2C slave device + while(writeBusy); // Wait for the write to complete + readByte(devAddress); // Receive the returned data byte +} + +// +// DMAC Interrupt Handler: Busy Flag and Callback Section +// +void I2C_DMAC::attachWriteCallback(voidFuncPtr callback) +{ + writeCallback = callback; // Attach a write complete callback function +} + +void I2C_DMAC::attachReadCallback(voidFuncPtr callback) +{ + readCallback = callback; // Attach a read complete callback function +} + +void I2C_DMAC::attachDmacErrorCallback(voidFuncPtr callback) +{ + errorDmacCallback = callback; // Attach a DMAC error callback function +} + +void I2C_DMAC::attachSercomErrorCallback(voidFuncPtr callback) +{ + errorSercomCallback = callback; // Attach a SERCOM error callback function +} + +void I2C_DMAC::detachWriteCallback() +{ + writeCallback = 0; // Detach the write complete callback function +} + +void I2C_DMAC::detachReadCallback() +{ + readCallback = 0; // Detach the read complete callback function +} + +void I2C_DMAC::detachDmacErrorCallback() +{ + errorDmacCallback = 0; // Detach the DMAC error callback function +} + +void I2C_DMAC::detachSercomErrorCallback() +{ + errorSercomCallback = 0; // Detach the SERCOM error callback function +} + +void I2C_DMAC::DMAC_IrqHandler() +{ + //__disable_irq(); // Disable interrupts + volatile I2C_DMAC* i2cDmacPtr = i2cDmacPtrs[0]; // Set I2C_DMAC pointer to the first I2C_DMAC instance + uint8_t activeChannel = DMAC->INTPEND.reg & DMAC_INTPEND_ID_Msk; // Get DMAC channel number + + if (instanceCounter > 1) // If there's only one instance of the I2C_DMAC object, + { // skip finding the active channel instance + for (uint8_t i = 0; i < instanceCounter; i++) // Find the I2C_DMAC instance from the active channel + { + if (activeChannel == i2cDmacPtrs[i]->dmacWriteChannel || // Compare the active channel against the object's + activeChannel == i2cDmacPtrs[i]->dmacReadChannel) // + { + i2cDmacPtr = i2cDmacPtrs[i]; // Assign I2C_DMAC pointer to the object instance with + } // the active channel + } + } + +#ifdef __SAMD51__ + if (DMAC->Channel[activeChannel].CHINTFLAG.bit.TERR) // DMAC Transfer error (TERR) +#else + DMAC->CHID.reg = DMAC_CHID_ID(activeChannel); // Switch to the active DMAC channel + if (DMAC->CHINTFLAG.bit.TERR) // DMAC Transfer error (TERR) +#endif + { + if (i2cDmacPtr->errorDmacCallback) // Check if there's a DMAC error callback function + { + i2cDmacPtr->errorDmacCallback(); // Call the callback function + } + } +#ifdef __SAMD51__ + else if (DMAC->Channel[activeChannel].CHINTFLAG.bit.TCMPL) // DMAC transfer complete (TCMPL) +#else + else if (DMAC->CHINTFLAG.bit.TCMPL) // DMAC transfer complete (TCMPL) +#endif + { + if (activeChannel == i2cDmacPtr->dmacWriteChannel) // Check write DMAC channel + { + i2cDmacPtr->writeBusy = false; // Clear the write busy flag + if (i2cDmacPtr->writeCallback) // Check if there's a write callback function + { + i2cDmacPtr->writeCallback(); // Call the write callback function + } + } + else if (activeChannel == i2cDmacPtr->dmacReadChannel) // Check read DMAC channel + { + i2cDmacPtr->readBusy = false; // Clear the read busy flag + if (i2cDmacPtr->readCallback) // Check if there's a read callback function + { + i2cDmacPtr->readCallback(); // Call the read callback function + } + } + } +#ifdef __SAMD51__ + DMAC->Channel[activeChannel].CHINTFLAG.reg = DMAC_CHINTFLAG_MASK; // Clear the DMAC channel interrupt flags +#else + DMAC->CHINTFLAG.reg = DMAC_CHINTFLAG_MASK; // Clear the DMAC channel interrupt flags +#endif + //__enable_irq(); // Enable interrupts +} + +void I2C_DMAC::SERCOM_IrqHandler() +{ + if (sercom->I2CM.INTFLAG.bit.ERROR && sercom->I2CM.INTENSET.bit.ERROR) + { +#ifdef __SAMD51__ + DMAC->Channel[dmacWriteChannel].CHCTRLA.bit.ENABLE = 0; // Disable the DMAC write channel + DMAC->Channel[dmacReadChannel].CHCTRLA.bit.ENABLE = 0; // Disable the DMAC read channel +#else + DMAC->CHID.reg = DMAC_CHID_ID(dmacWriteChannel); // Switch to the active DMAC write channel + DMAC->CHCTRLA.bit.ENABLE = 0; // Disable the DMAC write channel + DMAC->CHID.reg = DMAC_CHID_ID(dmacReadChannel); // Switch to the active DMAC read channel + DMAC->CHCTRLA.bit.ENABLE = 0; // Disable the DMAC read channel +#endif + writeBusy = false; // Clear the write busy flag + readBusy = false; // Clear the read busy flag + + if (errorSercomCallback) // Check if there's a SERCOM3 error callback function + { + errorSercomCallback(); // Call the SERCOM3 error callback function + } + sercom->I2CM.STATUS.reg |= SERCOM_I2CM_STATUS_LENERR | // Clear the status register flags - cleared by + SERCOM_I2CM_STATUS_BUSERR; // writing to SERCOM3 ADDR.ADDR register anyway + sercom->I2CM.INTFLAG.bit.ERROR = 1; // Clear the SERCOM error interrupt flag + } +} + +#ifdef __SAMD51__ +void DMAC_0_Handler() __attribute__((weak)); // Set as weakly declared linker symbol, so that the function can be overriden +void DMAC_0_Handler() // The DMAC_0_Handler() ISR +{ + I2C_DMAC::DMAC_IrqHandler(); // Call the I2C_DMAC's DMAC interrupt handler member function +} +// Set the DMAC Handler function for the other DMAC channels, assign to weak aliases +void DMAC_1_Handler(void) __attribute__((weak, alias("DMAC_0_Handler"))); +void DMAC_2_Handler(void) __attribute__((weak, alias("DMAC_0_Handler"))); +void DMAC_3_Handler(void) __attribute__((weak, alias("DMAC_0_Handler"))); +void DMAC_4_Handler(void) __attribute__((weak, alias("DMAC_0_Handler"))); +#else +void DMAC_Handler() __attribute__((weak)); // Set as weakly declared linker symbol, so that the function can be overriden +void DMAC_Handler() // The DMAC_Handler() ISR +{ + I2C_DMAC::DMAC_IrqHandler(); // Call the I2C_DMAC's DMAC interrupt handler member function +} +#endif + +#if WIRE_INTERFACES_COUNT > 0 // + /* In case new variant doesn't define these macros, + * we put here the ones for Arduino Zero. + * + * These values should be different on some variants! + */ + #ifndef PERIPH_WIRE + #define PERIPH_WIRE sercom3 + #ifdef __SAMD51__ + #define WIRE_IT_HANDLER SERCOM3_3_Handler + #else + #define WIRE_IT_HANDLER SERCOM3_Handler + #endif + #endif // PERIPH_WIRE + I2C_DMAC I2C(&PERIPH_WIRE, PIN_WIRE_SDA, PIN_WIRE_SCL); // Instantiate the I2C object + + void WIRE_IT_HANDLER() __attribute__((weak)); + void WIRE_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C.SERCOM_IrqHandler(); + } +#endif + +#if WIRE_INTERFACES_COUNT > 1 + I2C_DMAC I2C1(&PERIPH_WIRE1, PIN_WIRE1_SDA, PIN_WIRE1_SCL); + + void WIRE1_IT_HANDLER() __attribute__((weak)); + void WIRE1_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C1.SERCOM_IrqHandler(); + } +#endif + +#if WIRE_INTERFACES_COUNT > 2 + I2C_DMAC I2C2(&PERIPH_WIRE2, PIN_WIRE2_SDA, PIN_WIRE2_SCL); + + void WIRE2_IT_HANDLER() __attribute__((weak)); + void WIRE2_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C2.SERCOM_IrqHandler(); + } +#endif + +#if WIRE_INTERFACES_COUNT > 3 + I2C_DMAC I2C3(&PERIPH_WIRE3, PIN_WIRE3_SDA, PIN_WIRE3_SCL); + + void WIRE3_IT_HANDLER() __attribute__((weak)); + void WIRE3_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C3.SERCOM_IrqHandler(); + } +#endif + +#if WIRE_INTERFACES_COUNT > 4 + I2C_DMAC I2C4(&PERIPH_WIRE4, PIN_WIRE4_SDA, PIN_WIRE4_SCL); + + void WIRE4_IT_HANDLER() __attribute__((weak)); + void WIRE4_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C4.SERCOM_IrqHandler(); + } +#endif + +#if WIRE_INTERFACES_COUNT > 5 + I2C_DMAC I2C5(&PERIPH_WIRE5, PIN_WIRE5_SDA, PIN_WIRE5_SCL); + + void WIRE5_IT_HANDLER() __attribute__((weak)); + void WIRE5_IT_HANDLER() // Call the I2C_DMAC's SERCOM interrupt handler member function + { + I2C5.SERCOM_IrqHandler(); + } +#endif diff --git a/Arduino/I2C_DMAC.h b/Arduino/I2C_DMAC.h new file mode 100644 index 0000000..31a54aa --- /dev/null +++ b/Arduino/I2C_DMAC.h @@ -0,0 +1,141 @@ +/* + I2C_DMAC is a non-blocking I2C library that uses a SERCOM in + conjunction with the Direct Memory Access Controller (DMAC). + + Copyright (C) Martin Lindupp 2018 + + V1.0.0 -- Initial release + V1.1.0 -- Add Arduino MKR and SAMD51 support, plus multiple I2C instances + V1.1.1 -- Replaced pinPeripheral() function with port register manipulation + V1.1.2 -- Allow other classes to simultaneously use remaining DMAC channels + V1.1.3 -- Fixed issue with consecutive calls to writeByte() overwriting data + V1.1.4 -- Allow the DMAC to resume normal operation after an early NACK is received + V1.1.5 -- Activate internal pull-up resistors and increase driver strength + V1.1.6 -- Add SERCOM ALT (alternative) peripheral switch for the Metro M4 + V1.1.7 -- Arduino IDE library manager release + V1.1.8 -- Code optimisation + V1.1.9 -- Use default arguments for begin() member function + V1.1.10 -- Remove I2C instance on writeBusy flag in ReadByte() and ReadBytes() functions + V1.1.11 -- Arduino IDE library manager release + + The MIT License (MIT) + 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. +*/ + +#ifndef _I2C_DMAC_h +#define _I2C_DMAC_h + +#include +#include "DMAC.h" + +enum { REG_ADDR_8BIT = 1, REG_ADDR_16BIT }; // Register address mode definitions, 8-bit address or 16-bit address + +class I2C_DMAC { + public: + I2C_DMAC(SERCOM* s, uint8_t pinSDA, uint8_t pinSCL); // Class constructor to initialise, SERCOM class, pins and member variables + void begin(uint32_t baudrate = 100000, // Begin with 100kHz default clock rate and 8-bit register address mode + uint8_t regAddrMode = REG_ADDR_8BIT, + EPioType ulPeripheral = PIO_SERCOM); + void end(); // Tear down and tidy up resources + void setClock(uint32_t baudrate); // Set the I2C bus clock speed to the specified baud rate + void setWriteChannel(uint8_t channel); // Set the DMAC write channel number (0 - 11), default 0 + void setReadChannel(uint8_t channel); // Set the DMAC read channel number (0 - 11), default 1 + void setPriority(uint8_t priority); // Set the priority level for both read and write DMAC channels (0 - 3), default 0 + void setRegAddrMode(uint8_t regAddrMode); // Set the register address mode REG_ADDR_8BIT or REG_ADDR_16BIT + void initWriteBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Initialise DMAC to send data, (no register address) + void initWriteBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Initialise DMAC to send register address + data + void initWriteByte(uint8_t devAddress, uint16_t regAddress, uint8_t data); // Initialise DMAC to send register address + 1 data byte + void initWriteRegAddr(uint8_t devAddress, uint16_t regAddress); // Initialise DMAC to send register address, (no data) + void initReadBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Initialise DMAC to receive data + void initReadByte(uint8_t devAddress); // Initialise DMAC to receive 1 data byte + uint8_t getData(); // Retrieve the received data byte + void write(); // Transmit on I2C bus + void read(); // Receive on I2C bus + void writeBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Transmit data to the I2C device's register address + void writeByte(uint8_t devAddress, uint16_t regAddress, uint8_t data); // Transmit 1 data byte to the I2C device's register address + void writeRegAddr(uint8_t devAddress, uint16_t regAddress); // Write the register address to the I2C device + void readBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Receive data from the I2C device + void readByte(uint8_t devAddress); // Receive 1 data byte from the I2C device + void readBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Receive data from the I2C device's register address + void readByte(uint8_t devAddress, uint16_t regAddress); // Receive a byte from the I2C device's register address + void attachWriteCallback(voidFuncPtr callback); // Attach a DMAC write callback function + void attachReadCallback(voidFuncPtr callback); // Attach a DMAC read callback function + void attachDmacErrorCallback(voidFuncPtr callback); // Attach a DMAC error callback function + void attachSercomErrorCallback(voidFuncPtr callback); // Attach a SERCOM error callback function + void detachWriteCallback(); // Detach the DMAC write callback function + void detachReadCallback(); // Detach the DMAC read callback function + void detachDmacErrorCallback(); // Detach the DAMC error callback function + void detachSercomErrorCallback(); // Detach the SERCOM error callback function + static void DMAC_IrqHandler(); // DMAC interrupt handler function + void SERCOM_IrqHandler(); // SERCOM interrupt handler function + + volatile boolean writeBusy; // Write busy flag - indicates write transfer in progress + volatile boolean readBusy; // Read busy flag - indicates read transfer in progress + static volatile I2C_DMAC* i2cDmacPtrs[SERCOM_INST_NUM]; // Array of pointer to each instance (object) of this class + static volatile uint8_t instanceCounter; // Number of instances (objects) of this class + protected: + // Generic initialise write DMAC transfer function + void initWriteBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count, uint8_t readAddrLength); + private: + enum { WRITE, READ }; // I2C read and write bits definitions, WRITE: 0, READ: 1 + dmacdescriptor linked_descriptor __attribute__ ((aligned (16))); // DMAC linked descriptor + + Sercom* sercom; // Pointer to the selected SERCOMx peripheral + uint8_t pinSDA; // The selected SDA pin + uint8_t pinSCL; // The selected SCL pin + uint8_t devAddress; // The I2C device address + uint8_t regAddress[2]; // The I2C device's register address sent MSB first, LSB last + uint8_t data; // Used for single byte data transfer + uint8_t writeCount; // The number of data bytes to transmit + uint8_t readCount; // The number of data bytes to receive + uint8_t regAddrLength; // The length of the register address, 0: data only, 1: 8-bit, 2: 16-bit + uint8_t regAddrMode; // The current register address mode: REG_ADDR_8BIT or REG_ADDR_16BIT + uint8_t dmacPriority; // The DMAC read and write priority level + uint8_t dmacWriteTrigger; // The DMAC write peripheral trigger source + uint8_t dmacReadTrigger; // The DMAC read peripheral trigger source + uint8_t genericClockId; // The generic clock ID used to select the sercom peripheral to be clocked + IRQn_Type nvicId; // The Nested Vector Interrupt (NVIC) interrupt type for the SERCOMx peripheral + + typedef void (*voidFuncPtr)(void); // Alias for function pointer declarations + volatile voidFuncPtr writeCallback; // Write callback function pointer + volatile voidFuncPtr readCallback; // Read callback function pointer + volatile voidFuncPtr errorDmacCallback; // DMAC error callback function pointer + volatile voidFuncPtr errorSercomCallback; // SERCOM error callback function pointer + volatile uint8_t dmacWriteChannel; // DMAC write channel number (0 to 11), default 0 + volatile uint8_t dmacReadChannel; // DMAC read channel number (0 to 11), default 1 +}; + +#if WIRE_INTERFACES_COUNT > 0 + extern I2C_DMAC I2C; // Indicate that the I2C object is externally instantiated +#endif +#if WIRE_INTERFACES_COUNT > 1 + extern I2C_DMAC I2C1; // Indicate that the I2C1 object is externally instantiated +#endif +#if WIRE_INTERFACES_COUNT > 2 + extern I2C_DMAC I2C2; // Indicate that the I2C2 object is externally instantiated +#endif +#if WIRE_INTERFACES_COUNT > 3 + extern I2C_DMAC I2C3; // Indicate that the I2C3 object is externally instantiated +#endif +#if WIRE_INTERFACES_COUNT > 4 + extern I2C_DMAC I2C4; // Indicate that the I2C4 object is externally instantiated +#endif +#if WIRE_INTERFACES_COUNT > 5 + extern I2C_DMAC I2C5; // Indicate that the I2C5 object is externally instantiated +#endif + +#endif diff --git a/Arduino/NoiseSensor.cpp b/Arduino/NoiseSensor.cpp new file mode 100644 index 0000000..105db5e --- /dev/null +++ b/Arduino/NoiseSensor.cpp @@ -0,0 +1,284 @@ +// vim: noai:sw=2:tw=88 + +#include +#include "NoiseSensor.h" +//#include "NoiseSensorNode.h" + + +bool NoiseSensorClass::begin() { + _PACKET_SIZE = ( _tick_count + LORA_HEADER_LENGTH + BATTERY_FIELD_SIZE ); + _REQUEST_SIZE = ( PACKET_COUNT * _PACKET_SIZE * 2 ); // * 2, it's 16 bit. + + for (int i = 0; i < PACKET_COUNT; i++) { + _nodes[i] = new NoiseSensorNode(i, _PACKET_SIZE * 2); + } + + if (this->_tick_duration < 2) { + Serial.println("--> Using a tick duration of less than two seconds. This " + "will fail unless you've increased the MCUs SYSCLK!"); + } + + if (this->_tick_count >= 100) { + Serial.println( + "--> Using a tick count of more than 100 gets dangerously close to the " + "LoRa transmission limitations and is not supported as of now."); + } else if (this->_tick_count < 10) { + Serial.println( + "--> Using a tick count smaller than 10 might screw things up in a " + "unpredictable manner and is not recommended."); + } + + + if (this->_tick_count >= 120) { + Serial.println( + "--> Increased the tick count over 120. This results in I²C packages of " + "(tick_count + 3 ) * 2. " + "Note that I²C packets longer than 256 bytes will need to be split into " + "smaller chunks to allow them to transfer." + } + + if (!setupNTPClient()) { + Serial.println("!!!-------------------------!!!"); + Serial.println("NTPClient couldn't be started."); + Serial.println("!!!-------------------------!!!"); + return false; + } + I2C.begin(400000); + + this->sendSyncBeacon(); + while (I2C.writeBusy) { + delay(250); + Serial.println("Writing!"); + } + this->_first_sync = 1; +} + +bool NoiseSensorClass::begin(uint8_t tick_count) { + this->_tick_count = tick_count; + this->begin(); +} + +bool NoiseSensorClass::begin(uint8_t tick_count, uint8_t tick_duration) { + this->_tick_count = tick_count; + this->_tick_duration = tick_duration; + this->begin(); +} + +bool NoiseSensorClass::begin(uint8_t tick_count, uint8_t tick_duration, + uint8_t tx_offset) { + this->_tick_count = tick_count; + this->_tick_duration = tick_duration; + this->_tx_offset = tx_offset; + this->begin(); +} + +void NoiseSensorClass::setIds(const char* sensebox_ids[], const char* sensor_ids[]) { + for (int i = 0; i < PACKET_COUNT; i++) { + char tmp[24] = {0}; + _nodes[i]->sensebox_id = sensebox_ids[i]; + _nodes[i]->sensor_id = sensor_ids[i]; + } +} + + +bool NoiseSensorClass::beaconReady() { +// if (this->_time_client->getEpochTime() - this->_last_sync_time == + if (now() - _last_sync_time == + (this->_tick_count * this->_tick_duration)) { + Serial.println("---->Beacon ready"); + return true; + } + return false; +} + +bool NoiseSensorClass::requestReady() { + // 1 second as tolerance + if (!this->_first_sync && + //this->_time_client->getEpochTime() - this->_last_sync_time == + now() - _last_sync_time == + (5 * this->_tx_offset * this->_tick_duration + 1)) { + Serial.println("---->Request ready"); + return true; + } + return false; +} + +bool NoiseSensorClass::read() { + Serial.println("Starting read"); + for (int pkt = 0; pkt < PACKET_COUNT; pkt++) { + // Calculate the time of the sync message that triggered this packet. + uint32_t time_offset = (_tick_count * _tick_duration) + 1; // TODO Move + this->_nodes[pkt]->_sync_message_ts = this->_last_sync_time - time_offset; + Serial.print("Last sync time: "); + Serial.println(_last_sync_time); + Serial.print("Offset: "); + Serial.println(time_offset); + Serial.print("TS: "); + Serial.println(_nodes[pkt]->_sync_message_ts); + Serial.print(minute(_nodes[pkt]->_sync_message_ts)); + Serial.print(":"); + Serial.println(second(_nodes[pkt]->_sync_message_ts)); + /* + if (_nodes[pkt]->missing_answers > 0) { + _nodes[pkt]->_sync_message_ts -= time_offset * _nodes[pkt]->missing_answers; + } + */ + I2C.readBytes(this->_i2c_address, (uint8_t *) this->_nodes[pkt]->_measurements, this->_PACKET_SIZE * 2); + } + + uint32_t read_start_time = millis(); + while (I2C.readBusy) { + Serial.println("Reading!"); + delay(1); + + // Restart I²C if no response for more than a second. + if (millis() - read_start_time >= 1000) { + Serial.println("Restarting I²C"); + I2C.end(); + I2C.begin(400000); + return false; + } + } + // Skip 0, it has no battery. + for (int pkt = 1; pkt < PACKET_COUNT; pkt++) { + // TODO Move this to a sane place (own function). + if (_nodes[pkt]->_measurements[_PACKET_SIZE - 1]) { + Serial.print("Battery low on device"); + Serial.println(pkt); + } + } + + Serial.println("Reading done."); + return true; +} + +void NoiseSensorClass::printMeasurements(uint8_t node_id) { + uint32_t ts = this->_nodes[node_id]->_sync_message_ts; + char printbuf[32]; + sprintf(printbuf, "- ID %u - ", node_id); + Serial.println(printbuf); + + for (int i = 2; i < (this->_PACKET_SIZE - BATTERY_FIELD_SIZE); i++) { + sprintf(printbuf, "%02i:%02i:%02i -> ", hour(ts), minute(ts), second(ts)); + Serial.print(printbuf); + + Serial.println(this->_nodes[node_id]->_measurements[i]); + ts += this->_tick_duration; + + } +} + +bool NoiseSensorClass::sendSyncBeacon() { + //_last_sync_time = _time_client->getEpochTime(); + uint8_t setup_values[3]; + + setup_values[0] = this->_tick_count; + setup_values[1] = this->_tick_duration; + setup_values[2] = this->_tx_offset; + + Serial.println("Sent beacon"); + I2C.writeBytes(this->_i2c_address, 0x01, (uint8_t *)setup_values, + sizeof(setup_values)); + this->_first_sync = 0; + _last_sync_time = now(); + Serial.println(_last_sync_time); + Serial.print(minute(_last_sync_time)); + Serial.print(":"); + Serial.println(second(_last_sync_time)); + + while (I2C.writeBusy) { + delay(250); + Serial.println("Writing!"); + } +} + +String NoiseSensorClass::buildSenseBoxJSON(uint8_t device_id) { + // _time_client->update(); + NoiseSensorNode* node = _nodes[device_id]; + uint32_t ts = node->_sync_message_ts; + + // Each measurement line is max. 94 chars long + 2 curly brackets. + uint16_t bufsize = _tick_count * 94 + 2; + char json_buf[bufsize] = {0}; + char sensor_id[25] = {0}; + node->sensor_id.toCharArray(sensor_id, 25); + + // Build a json string in a very comprehensible way... + strcat(json_buf, "[\n"); + + for (int i = 2; i < (_PACKET_SIZE - BATTERY_FIELD_SIZE); i++) { + char tmpbuf[1024] = {0}; + char timestamp[24] = {0}; + float m = node->_measurements[i] / 10.0; // From fixed point to float. + + if (m >= 29 && m <= 120) { + sprintf(timestamp, "%i-%02i-%02iT%02i:%02i:%02iZ", year(ts), month(ts), + day(ts), hour(ts), minute(ts), second(ts)); + + sprintf(tmpbuf, "{\"sensor\":\"%s\", \"value\":\"%.1f\", \"createdAt\":\"%s\"}", sensor_id, m, timestamp); + + // No trailing comma on the last entry. + if (i != _PACKET_SIZE - BATTERY_FIELD_SIZE - 1) { + strcat(tmpbuf, ","); + } + strcat(json_buf, tmpbuf); + strcat(json_buf, "\n"); + + } + + ts += this->_tick_duration; + } + + /* + if (strlen(json_buf) > 2) { + node->missing_answers = 0; + Serial.println("Missing answers reset."); + } else { + node->missing_answers++; + Serial.print("Missing answers: "); + Serial.println(node->missing_answers); + } + */ + + strcat(json_buf, "]"); + + return json_buf; +} + +String NoiseSensorClass::buildHTTPHeader(uint8_t device_id, const char* server, uint16_t content_length) { + char sb_id[25] = {0}; + _nodes[device_id]->sensebox_id.toCharArray(sb_id, 25); + + char uploadbuf[2048] = {0}; + sprintf(uploadbuf, "POST /boxes/%s/data HTTP/1.1\n" + "Host: %s\n" + "content-type: application/json\n" + "Connection: close\n" + "Content-Length: %u", sb_id, server, content_length); + + return String(uploadbuf); +} + + +// Private functions + +bool NoiseSensorClass::setupNTPClient() { + uint32_t start_time = millis(); + while (timeStatus() != timeSet) { + if (millis() - start_time >= 200) { + return false; + } + } + return true; + //setSyncProvider( + + + //WiFiUDP ntpUDP; + //this->_time_client = new NTPClient(ntpUDP); + //this->_time_client->begin(); + + //if (this->_time_client->update()) { return true; } + +} + +NoiseSensorClass NoiseSensor; diff --git a/Arduino/NoiseSensor.h b/Arduino/NoiseSensor.h new file mode 100644 index 0000000..f6f2702 --- /dev/null +++ b/Arduino/NoiseSensor.h @@ -0,0 +1,90 @@ +// vim: noai:sw=2:tw=88 + +#include "I2C_DMAC.h" +#include "NoiseSensorNode.h" +//#include + +#include + +#define LORA_HEADER_LENGTH 2 +#define BATTERY_FIELD_SIZE 1 +#define PACKET_COUNT 5 + +class NoiseSensorClass { + private: + //NTPClient* _time_client; + uint32_t _last_sync_time; + uint8_t _tick_count = 20; + uint8_t _tick_duration = 2; // in seconds + uint8_t _tx_offset = 1; + uint8_t _i2c_address = 0x3C; + bool _first_sync = true; + + // These aren't _real_ const but they shouldn't be redefined nonetheless. + uint16_t _PACKET_SIZE; + uint16_t _REQUEST_SIZE; + + NoiseSensorNode* _nodes[]; + + uint8_t getDeviceID(); + bool setupNTPClient(); + + public: + /** + * Begin the measurement cycle by sending a sync message. + * + * @return true on success, false on failure + */ + bool begin(); + + /** + * Call begin() with a custom tick count. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count); + + /** + * Call begin() with a custom tick count and tick duration. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count, uint8_t tick_duration); + + /** + * Call begin() with a custom tick count, tick duration and tx_offset. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count, uint8_t tick_duration, uint8_t tx_offset); + + void setIds(const char* sensebox_ids[], const char* sensor_ids[]); + String buildHTTPHeader(uint8_t device_id, const char* server, uint16_t content_length); + + bool beaconReady(); + bool requestReady(); + + /** + * Read the newly available data from the slave. + * + * @return true on success, false on failure + */ + bool read(); + + /** + * Print the measurements in a pretty format. + */ + void printMeasurements(uint8_t device_id); + + /** + * Send the sync message to the slave. + * + * @return true on success, false on failure + */ + bool sendSyncBeacon(); + + String buildSenseBoxJSON(uint8_t device_id); + +}; + +extern NoiseSensorClass NoiseSensor; diff --git a/Arduino/NoiseSensorNode.cpp b/Arduino/NoiseSensorNode.cpp new file mode 100644 index 0000000..b3b1382 --- /dev/null +++ b/Arduino/NoiseSensorNode.cpp @@ -0,0 +1,11 @@ +// vim: noai:sw=2:tw=88 + +#include "NoiseSensorNode.h" +#include "NoiseSensor.h" + +NoiseSensorNode::NoiseSensorNode() {}; + +NoiseSensorNode::NoiseSensorNode(uint8_t node_id, uint8_t size) { + this->_node_id = node_id; + this->_measurements = new uint16_t[size]; +} diff --git a/Arduino/NoiseSensorNode.h b/Arduino/NoiseSensorNode.h new file mode 100644 index 0000000..308ec49 --- /dev/null +++ b/Arduino/NoiseSensorNode.h @@ -0,0 +1,21 @@ +// vim: noai:sw=2:tw=88 + +#pragma once + +#include + +class NoiseSensorNode { + private: + uint8_t _node_id; + + public: + uint32_t _sync_message_ts; + uint16_t* _measurements; // For a dynamic array later. + String sensebox_id; + String sensor_id; + uint8_t missing_answers = 0; + bool _battery_low = 0; + + NoiseSensorNode(); + NoiseSensorNode(uint8_t node_id, uint8_t size); +}; diff --git a/Arduino/noisesensor_arduino.ino b/Arduino/noisesensor_arduino.ino new file mode 100644 index 0000000..bf475a0 --- /dev/null +++ b/Arduino/noisesensor_arduino.ino @@ -0,0 +1,225 @@ +#include "NoiseSensor.h" + +#include // For WiFi101 +#include +#include +#include + +// ---> Fill in your WiFi name (SSID) and password here. <--- +#define SECRET_SSID "CHANGEME" +#define SECRET_PASSWORD "CHANGEME" + +// Keep these values like this unless you know what you're doing. +const char server[] = "api.opensensemap.org"; +const uint8_t tick_count = 60; +const bool verbose = 1; + +// ---> Fill this in yourself. <--- +// SenseBox IDs from the web interface in order 0, 1, 2, 3, 4. Master is ID 0! +const char *sensebox_ids[] = {"CHANGEME", "", "CHANGEME", "CHANGEME", ""}; +// Sensor IDs from the web interface in the same order as above. +const char *sensor_ids[] = {"CHANGEME", "", "CHANGEME", "CHANGEME", ""}; + +// ================= + +WiFiClient client; +uint32_t last_reading; + +WiFiUDP udp; + +static const char ntpServerName[] = "de.pool.ntp.org"; +const int timeZone = 0; // Central European Time + +void setup() { + sleep(2000); + Serial.begin(115200); + Serial.println("\nStarting wifi connection..."); + + + // Retry WiFi connection until it succeeds. + do { + WiFi.begin(SECRET_SSID, SECRET_PASSWORD); + uint32_t wifi_start_time = millis(); + while (WiFi.status() != WL_CONNECTED) { + senseBoxIO.statusRed(); + delay(500); + Serial.print("."); + senseBoxIO.statusGreen(); + if (millis() - wifi_start_time >= 5000) { + Serial.println("Couldn't connect to Wifi. Try again."); + WiFi.end(); + break; + } + } + } + while (WiFi.status() != WL_CONNECTED); + + + Serial.print("Connected to AP "); + Serial.println(SECRET_SSID); + + udp.begin(8888); + setSyncProvider(getNtpTime); + setSyncInterval(300); + + // Let the sensors know we're ready and start measuring. + if (!NoiseSensor.begin(tick_count)) { + Serial.println("Something didn't work. Please restart the system."); + while (1) { + senseBoxIO.statusRed(); + delay(100); + senseBoxIO.statusNone(); + delay(100); + } + } + NoiseSensor.setIds(sensebox_ids, sensor_ids); + + sleep(1000); +} + +void loop() { + // Shows the openSenseMap's response. + if (!verbose) { + while (client.available()) { + char c = client.read(); + Serial.write(c); + } + } + + // This check is clumsy but will be replaced by a proper timer interrupt in the future. + if (NoiseSensor.beaconReady()) { + senseBoxIO.statusRed(); + NoiseSensor.sendSyncBeacon(); + senseBoxIO.statusGreen(); + } + + // This check is clumsy but will be replaced by a proper timer interrupt in the future. + if (millis() - last_reading >= 2000 && NoiseSensor.requestReady()) { + senseBoxIO.statusRed(); + + Serial.println(); + NoiseSensor.read(); + + // ---> Choose which sensor's data you want to upload by calling upload(device_id). + upload(0, server); + upload(2, server); + upload(3, server); + senseBoxIO.statusGreen(); + + // Record the last upload time to make sure we don't read and upload multiple times per second. + last_reading = millis(); + } +} + +void upload(uint8_t device_id, const char* server) { + Serial.println("Starting upload"); + + if (client.connected()) { + client.stop(); + sleep(500); + } + + // If there's a successful connection: + if (client.connectSSL(server, 443)) { + String uploadbuf; + String json = NoiseSensor.buildSenseBoxJSON(device_id); + uint16_t content_length = json.length(); + + uploadbuf = NoiseSensor.buildHTTPHeader(device_id, server, content_length); + + client.println(uploadbuf); + if (verbose) { Serial.println(uploadbuf); } + + client.println(); + if (verbose) { Serial.println(); } + + // For some reason that nobody knows, client.print has an arbitrary input string limit. + // Therefore, we need to split it at some arbitrary length and provide chunks. + for (uint16_t idx = 0; idx < json.length(); idx += 1000) { + client.print(json.substring(idx, idx + 1000)); + if (verbose) { Serial.print(json.substring(idx, idx + 1000)); } + } + + client.println(); + if (verbose) { Serial.println(); } + + Serial.println("done!"); + + } else { + // If we couldn't make a connection: + Serial.println("connection failed. Restart System."); + sleep(1000); + } + + Serial.println("Upload done"); +} + +void sleep(unsigned long ms) { // ms: duration + unsigned long start = millis(); // start: timestamp + for (;;) { + unsigned long now = millis(); // now: timestamp + unsigned long elapsed = now - start; // elapsed: duration + if (elapsed >= ms) // comparing durations: OK + return; + } +} + + +const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message +byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets + +time_t getNtpTime() +{ + IPAddress ntpServerIP;// = IPAddress(176, 9 , 42, 91); // NTP server's ip address + + while (udp.parsePacket() > 0) ; // discard any previously received packets + WiFi.hostByName(ntpServerName, ntpServerIP); + + if (verbose) { + Serial.println("Transmit NTP Request"); + Serial.print(ntpServerName); + Serial.print(": "); + Serial.println(ntpServerIP); + } + sendNTPpacket(ntpServerIP); + uint32_t beginWait = millis(); + while (millis() - beginWait < 1500) { + int size = udp.parsePacket(); + if (size >= NTP_PACKET_SIZE) { + if (verbose) { Serial.println("Receive NTP Response"); } + udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer + unsigned long secsSince1900; + // convert four bytes starting at location 40 to a long integer + secsSince1900 = (unsigned long)packetBuffer[40] << 24; + secsSince1900 |= (unsigned long)packetBuffer[41] << 16; + secsSince1900 |= (unsigned long)packetBuffer[42] << 8; + secsSince1900 |= (unsigned long)packetBuffer[43]; + return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; + } + } + Serial.println("No NTP Response :-("); + return 0; // return 0 if unable to get the time +} + +// send an NTP request to the time server at the given address +void sendNTPpacket(IPAddress &address) +{ + // set all bytes in the buffer to 0 + memset(packetBuffer, 0, NTP_PACKET_SIZE); + // Initialize values needed to form NTP request + // (see URL above for details on the packets) + packetBuffer[0] = 0b11100011; // LI, Version, Mode + packetBuffer[1] = 0; // Stratum, or type of clock + packetBuffer[2] = 6; // Polling Interval + packetBuffer[3] = 0xEC; // Peer Clock Precision + // 8 bytes of zero for Root Delay & Root Dispersion + packetBuffer[12] = 49; + packetBuffer[13] = 0x4E; + packetBuffer[14] = 49; + packetBuffer[15] = 52; + // all NTP fields have been given values, now + // you can send a packet requesting a timestamp: + udp.beginPacket(address, 123); //NTP requests are to port 123 + udp.write(packetBuffer, NTP_PACKET_SIZE); + udp.endPacket(); +} diff --git a/ESP32/NoiseSensor.cpp b/ESP32/NoiseSensor.cpp new file mode 100644 index 0000000..b756d50 --- /dev/null +++ b/ESP32/NoiseSensor.cpp @@ -0,0 +1,268 @@ +// vim: noai:sw=2:tw=88 + +#include "NoiseSensor.h" +#include "time.h" +#include +#include + +ESP32Time t; + +bool NoiseSensorClass::begin() { + _PACKET_SIZE = ( _tick_count + LORA_HEADER_LENGTH + BATTERY_FIELD_SIZE ); + _REQUEST_SIZE = ( PACKET_COUNT * _PACKET_SIZE * 2 ); // * 2, it's 16 bit. + + for (int i = 0; i < PACKET_COUNT; i++) { + _nodes[i] = new NoiseSensorNode(i, _PACKET_SIZE * 2); + } + + if (this->_tick_duration < 2) { + Serial.println("--> Using a tick duration of less than two seconds. This " + "will fail unless you've increased the MCUs SYSCLK!"); + } + + if (this->_tick_count >= 100) { + Serial.println( + "--> Using a tick count of more than 100 gets dangerously close to the " + "LoRa transmission limitations and is not supported as of now."); + } + + + if (!setupNTPClient()) { + Serial.println("!!!-------------------------!!!"); + Serial.println("NTPClient couldn't be started."); + Serial.println("!!!-------------------------!!!"); + return false; + } + + Wire.begin(21, 22, 400000); + sendSyncBeacon(); + this->_first_sync = 1; +} + +bool NoiseSensorClass::begin(uint8_t tick_count) { + this->_tick_count = tick_count; + this->begin(); +} + +bool NoiseSensorClass::begin(uint8_t tick_count, uint8_t tick_duration) { + this->_tick_count = tick_count; + this->_tick_duration = tick_duration; + this->begin(); +} + +bool NoiseSensorClass::begin(uint8_t tick_count, uint8_t tick_duration, + uint8_t tx_offset) { + this->_tick_count = tick_count; + this->_tick_duration = tick_duration; + this->_tx_offset = tx_offset; + this->begin(); +} + +void NoiseSensorClass::setIds(const char* sensebox_ids[], const char* sensor_ids[]) { + for (int i = 0; i < PACKET_COUNT; i++) { + char tmp[24] = {0}; + _nodes[i]->sensebox_id = sensebox_ids[i]; + _nodes[i]->sensor_id = sensor_ids[i]; + } +} + + +bool NoiseSensorClass::beaconReady() { + if (t.getEpoch() - _last_sync_time == + (this->_tick_count * this->_tick_duration)) { + Serial.println("---->Beacon ready"); + return true; + } + return false; +} + +bool NoiseSensorClass::requestReady() { + // 1 second as tolerance + if (!this->_first_sync && + t.getEpoch() - _last_sync_time == + (5 * this->_tx_offset * this->_tick_duration + 1)) { + Serial.println("---->Request ready"); + return true; + } + return false; +} + +bool NoiseSensorClass::read() { + for (int pkt = 0; pkt < PACKET_COUNT; pkt++) { + NoiseSensorNode* node = _nodes[pkt]; + // Calculate the time of the sync message that triggered this packet. + uint32_t time_offset = (_tick_count * _tick_duration); // TODO Move + node->_sync_message_ts = this->_last_sync_time - time_offset; + /* + Serial.print("Last sync time: "); + Serial.println(_last_sync_time); + Serial.print("Offset: "); + Serial.println(time_offset); + Serial.print("TS: "); + Serial.println(_nodes[pkt]->_sync_message_ts); + Serial.print(minute(_nodes[pkt]->_sync_message_ts)); + Serial.print(":"); + Serial.println(second(_nodes[pkt]->_sync_message_ts)); + */ + /* + if (node->missing_answers > 0) { + node->_sync_message_ts -= time_offset * node->missing_answers; + } + */ + Wire.requestFrom(_i2c_address, _PACKET_SIZE * 2); + char req_buf[_PACKET_SIZE * 2] = {0}; + uint8_t count = 0; + + while (Wire.available()) { + char c = Wire.read(); + req_buf[count] = c; + count++; + } + + // TODO There's probably a neater way than doing these low level operations. + for (int i = 0; i < _PACKET_SIZE * 2; i+=2) { + uint16_t n = req_buf[i+1] << 8; + n |= req_buf[i]; + node->_measurements[i/2] = n; + Serial.print(n); + Serial.print(" "); + } + + Serial.println(); + } + + // Skip 0, it has no battery. + for (int pkt = 1; pkt < PACKET_COUNT; pkt++) { + // TODO Move this to a sane place (own function). + if (_nodes[pkt]->_measurements[_PACKET_SIZE - 1]) { + Serial.print("Battery low on device"); + Serial.println(pkt); + } + } + + Serial.println("Reading done."); + return true; +} + +void NoiseSensorClass::printMeasurements(uint8_t node_id) { + uint32_t ts = this->_nodes[node_id]->_sync_message_ts; + char printbuf[32]; + sprintf(printbuf, "- ID %u - ", node_id); + Serial.println(printbuf); + + for (int i = 2; i < (this->_PACKET_SIZE - BATTERY_FIELD_SIZE); i++) { + // TODO Timestamps + + Serial.println(this->_nodes[node_id]->_measurements[i]); + ts += this->_tick_duration; + } +} + +bool NoiseSensorClass::sendSyncBeacon() { + uint8_t setup_values[4]; + + setup_values[0] = 1; + setup_values[1] = this->_tick_count; + setup_values[2] = this->_tick_duration; + setup_values[3] = this->_tx_offset; + + Serial.println("Sent beacon"); + + Wire.beginTransmission(_i2c_address); + Wire.write((uint8_t *) setup_values, sizeof(setup_values)); + Wire.endTransmission(); + + this->_first_sync = 0; + _last_sync_time = t.getEpoch(); + Serial.println(_last_sync_time); + +} + +String NoiseSensorClass::buildSenseBoxJSON(uint8_t device_id) { + NoiseSensorNode* node = _nodes[device_id]; + uint32_t ts = node->_sync_message_ts; + + // Each measurement line is max. 94 chars long + 2 curly brackets. + uint16_t bufsize = _tick_count * 94 + 2; + char json_buf[bufsize] = {0}; + char sensor_id[25] = {0}; + node->sensor_id.toCharArray(sensor_id, 25); + + // Build a json string in a very comprehensible way... + strcat(json_buf, "[\n"); + + for (int i = 2; i < (_PACKET_SIZE - BATTERY_FIELD_SIZE); i++) { + char tmpbuf[1024] = {0}; + char timestamp[24] = {0}; + float m = node->_measurements[i] / 10.0; // From fixed point to float. + + if (m >= 29 && m <= 120) { + sprintf(timestamp, "%i-%02i-%02iT%02i:%02i:%02iZ", year(ts), month(ts), + day(ts), hour(ts), minute(ts), second(ts)); + + sprintf(tmpbuf, "{\"sensor\":\"%s\", \"value\":\"%.1f\", \"createdAt\":\"%s\"}", sensor_id, m, timestamp); + + // No trailing comma on the last entry. + if (i != _PACKET_SIZE - BATTERY_FIELD_SIZE - 1) { + strcat(tmpbuf, ","); + } + strcat(json_buf, tmpbuf); + strcat(json_buf, "\n"); + } + + ts += this->_tick_duration; + } + + + /* + if (strlen(json_buf) > 2) { + node->missing_answers = 0; + Serial.println("Missing answers reset."); + } else { + node->missing_answers++; + Serial.print("Missing answers: "); + Serial.println(node->missing_answers); + } + */ + + strcat(json_buf, "]"); + + return json_buf; +} + +String NoiseSensorClass::buildHTTPHeader(uint8_t device_id, const char* server, uint16_t content_length) { + char sb_id[25] = {0}; + _nodes[device_id]->sensebox_id.toCharArray(sb_id, 25); + + char uploadbuf[2048] = {0}; + sprintf(uploadbuf, "POST /boxes/%s/data HTTP/1.1\n" + "Host: %s\n" + "content-type: application/json\n" + "Connection: close\n" + "Content-Length: %u", sb_id, server, content_length); + + return String(uploadbuf); +} + +// Private functions + +bool NoiseSensorClass::setupNTPClient() { + configTime(0, 0, "de.pool.ntp.org"); + + // Allow processing of the package. + delay(1000); + + struct tm timeinfo; + if(!getLocalTime(&timeinfo)){ + Serial.println("Failed to obtain time"); + return false; + } + char timestamp[24]; + uint32_t ts = t.getEpoch(); + sprintf(timestamp, "%i-%02i-%02iT%02i:%02i:%02iZ", year(ts), month(ts), + day(ts), hour(ts), minute(ts), second(ts)); + Serial.println(timestamp); + return true; +} + +NoiseSensorClass NoiseSensor; diff --git a/ESP32/NoiseSensor.h b/ESP32/NoiseSensor.h new file mode 100644 index 0000000..a8d1ccd --- /dev/null +++ b/ESP32/NoiseSensor.h @@ -0,0 +1,91 @@ +// vim: noai:sw=2:tw=88 + +#include +#include "NoiseSensorNode.h" + +#include + +#define LORA_HEADER_LENGTH 2 +#define BATTERY_FIELD_SIZE 1 +#define PACKET_COUNT 5 + +class NoiseSensorClass { + private: + //NTPClient* _time_client; + uint32_t _last_sync_time; + uint8_t _tick_count = 20; + uint8_t _tick_duration = 2; // in seconds + uint8_t _tx_offset = 1; + uint8_t _i2c_address = 0x3C; + bool _first_sync = true; + + // These aren't _real_ const but they shouldn't be redefined nonetheless. + uint16_t _PACKET_SIZE; + uint16_t _REQUEST_SIZE; + + NoiseSensorNode* _nodes[]; + + uint8_t getDeviceID(); + bool setupNTPClient(); + uint32_t getEpoch(); + + + public: + /** + * Begin the measurement cycle by sending a sync message. + * + * @return true on success, false on failure + */ + bool begin(); + + /** + * Call begin() with a custom tick count. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count); + + /** + * Call begin() with a custom tick count and tick duration. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count, uint8_t tick_duration); + + /** + * Call begin() with a custom tick count, tick duration and tx_offset. + * + * @return true on success, false on failure + */ + bool begin(uint8_t tick_count, uint8_t tick_duration, uint8_t tx_offset); + + void setIds(const char* sensebox_ids[], const char* sensor_ids[]); + String buildHTTPHeader(uint8_t device_id, const char* server, uint16_t content_length); + + bool beaconReady(); + bool requestReady(); + + /** + * Read the newly available data from the slave. + * + * @return true on success, false on failure + */ + bool read(); + + /** + * Print the measurements in a pretty format. + */ + void printMeasurements(uint8_t device_id); + + /** + * Send the sync message to the slave. + * + * @return true on success, false on failure + */ + bool sendSyncBeacon(); + + String buildSenseBoxJSON(uint8_t device_id); + +}; + +extern NoiseSensorClass NoiseSensor; diff --git a/ESP32/NoiseSensorNode.cpp b/ESP32/NoiseSensorNode.cpp new file mode 100644 index 0000000..b3b1382 --- /dev/null +++ b/ESP32/NoiseSensorNode.cpp @@ -0,0 +1,11 @@ +// vim: noai:sw=2:tw=88 + +#include "NoiseSensorNode.h" +#include "NoiseSensor.h" + +NoiseSensorNode::NoiseSensorNode() {}; + +NoiseSensorNode::NoiseSensorNode(uint8_t node_id, uint8_t size) { + this->_node_id = node_id; + this->_measurements = new uint16_t[size]; +} diff --git a/ESP32/NoiseSensorNode.h b/ESP32/NoiseSensorNode.h new file mode 100644 index 0000000..ab06101 --- /dev/null +++ b/ESP32/NoiseSensorNode.h @@ -0,0 +1,21 @@ +// vim: noai:sw=2:tw=88 + +#pragma once + +#include + +class NoiseSensorNode { + private: + uint8_t _node_id; + + public: + uint32_t _sync_message_ts; + uint16_t* _measurements; // For a dynamic array later. + String sensebox_id; + String sensor_id; + uint8_t missing_answers; + bool _battery_low = 0; + + NoiseSensorNode(); + NoiseSensorNode(uint8_t node_id, uint8_t size); +}; diff --git a/ESP32/noisesensor_esp32.ino b/ESP32/noisesensor_esp32.ino new file mode 100644 index 0000000..52ac789 --- /dev/null +++ b/ESP32/noisesensor_esp32.ino @@ -0,0 +1,140 @@ +#include "NoiseSensor.h" + +#include // For WiFi, I think +#include +#include "time.h" + +#define SNTP_UPDATE_DELAY 300000 + +// ---> Fill in your WiFi name (SSID) and password here. <--- +#define SECRET_SSID "CHANGEME" +#define SECRET_PASSWORD "CHANGEME" + +// Keep these values like this unless you know what you're doing. +const char server[] = "api.opensensemap.org"; +const uint8_t tick_count = 30; +const bool verbose = 1; + +// ---> Fill this in yourself. <--- +// SenseBox IDs from the web interface in order 0, 1, 2, 3, 4. Master is ID 0! +const char *sensebox_ids[] = {"CHANGEME", "", "CHANGEME", "", ""}; +// Sensor IDs from the web interface in the same order as above. +const char *sensor_ids[] = {"CHANGEME", "", "CHANGEME", "", ""}; + +// ================= + +WiFiClientSecure client; +uint32_t last_reading; + +WiFiUDP udp; + +void setup() { + delay(2000); + Serial.begin(115200); + Serial.println("\nStarting wifi connection..."); + + + // Retry WiFi connection until it succeeds. + do { + WiFi.begin(SECRET_SSID, SECRET_PASSWORD); + uint32_t wifi_start_time = millis(); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + if (millis() - wifi_start_time >= 5000) { + Serial.println("\nCouldn't connect to Wifi. Try again."); + //WiFi.end(); + break; + } + } + } + while (WiFi.status() != WL_CONNECTED); + + Serial.print("\nConnected to AP "); + Serial.println(SECRET_SSID); + client.setInsecure(); + + // Let the sensors know we're ready and start measuring. + if (!NoiseSensor.begin(tick_count)) { + Serial.println("Something didn't work. Please restart the system."); + while (1) { + Serial.println("Error, please restart."); + } + } + NoiseSensor.setIds(sensebox_ids, sensor_ids); + + delay(1000); +} + +void loop() { + // Shows the openSenseMap's response. + if (verbose) { + while (client.available()) { + char c = client.read(); + Serial.write(c); + } + } + + // This check is clumsy but will be replaced by a proper timer interrupt in the future. + if (NoiseSensor.beaconReady()) { + NoiseSensor.sendSyncBeacon(); + } + + // This check is clumsy but will be replaced by a proper timer interrupt in the future. + if (millis() - last_reading >= 2000 && NoiseSensor.requestReady()) { + + Serial.println(); + NoiseSensor.read(); + + // ---> Choose which sensor's data you want to upload by calling upload(device_id). + upload(0, server); + upload(2, server); + + // Record the last upload time to make sure we don't read and upload multiple times per second. + last_reading = millis(); + } +} + +void upload(uint8_t device_id, const char* server) { + Serial.println("Start upload"); + + if (client.connected()) { + client.stop(); + delay(500); + } + + // If there's a successful connection: + if (client.connect(server, 443)) { + String uploadbuf; + String json = NoiseSensor.buildSenseBoxJSON(device_id); + uint16_t content_length = json.length(); + Serial.println("connecting..."); + + uploadbuf = NoiseSensor.buildHTTPHeader(device_id, server, content_length); + + client.println(uploadbuf); + if (verbose) { Serial.println(uploadbuf); } + + client.println(); + if (verbose) { Serial.println(); } + + // For some reason that nobody knows, client.print has an arbitrary input string limit. + // Therefore, we need to split it at some arbitrary length and provide chunks. + for (uint16_t idx = 0; idx < json.length(); idx += 1000) { + client.print(json.substring(idx, idx + 1000)); + if (verbose) { Serial.print(json.substring(idx, idx + 1000)); } + } + + client.println(); + if (verbose) { Serial.println(); } + + Serial.println("done!"); + + } else { + // If we couldn't make a connection: + Serial.println("connection failed. Restart System."); + delay(1000); + } + + Serial.println("Upload done"); +}