Introduction: IOT123 - I2C MAX9812 BRICK

About: The tension between novelty and familiarity...

The IOT123 BRICKS are DIY modular units that can be mashed up with other IOT123 BRICKS, to add functionality to a node or wearable. They are based on the inch square, double-sided protoboards with interconnected through holes.

A number of these BRICKS are expected to be on multiple nodes (Master MCUs - ESP8266 or ATTINY84) on a site. The MCU needs no prior knowledge of the sensors purpose or software needs. It scans for I2C nodes then requests a property dump (sensor data) from each slave. These BRICKs supply 5.0V, 3.3V and another AUX line which is customizable.

This I2C MAX9812 BRICK dumps 3 sound sensing properties:

  • audMin (0-1023) - lowest value inside the 50ms (20Hz) sample window
  • audMax (0-1023) - highest value inside the 50ms (20Hz) sample window
  • audDiff (0-50) - a value derived from the difference of aMin and aMax

Sound Levels using I2C are not expected to be used for continuous readings. Think machine noise levels, meeting/venue room occupancy or sound due to movement (water/friction).

The Keyes type sensor bricks will be abstracted first as they come with vitamins (extra components needed) included and are relatively cheep (I bought 37 for 10AUD). Other boards/circuits will be introduced to the I2C BRICKS.

The through-holes adjacent to the ATTINY85 have been left unused, to enable a pogo pin programmer while the DIP8 is soldered to the PCB.

A further abstraction, packaging the BRICKS in small cylinders that plug into a D1M WIFI BLOCK hub, pumping the values to a MQTT server, is being developed.

Step 1: Materials and Tools

    There is a full Bill of Material and Sourcing list.

    1. CJMCU-9812 sensor (1)
    2. ATTINY85 20PU (1)
    3. 1" Double sided protoboard (1)
    4. Male Header 90º (3P, 3P)
    5. Male Header (2P, 2P)
    6. Jumper Shunt (1)
    7. Hookup wire (~7)
    8. Solder and Iron (1)
    9. Strong Cyanoachrylate Adhesive (1)

    Step 2: Meanderings

    In order to determine what supply voltage to support and the application of the sensor on an I2C bus, I ran the following scenarios. A sound test file from https://www.audiocheck.net/Audio/audiocheck.net_dynamiccheck.ogg was used. #1 & #2 sketch. #3 & #4 sketch. #5 & #6 sketches (UNO & ATTINY85). Using the Arduino IDE Serial Plotter.

    1. 5V VCC sensor on Standalone Arduino UNO
    2. 3V3 VCC sensor on Standalone Arduino UNO
    3. 5V VCC sensor on Standalone ATTINY85
    4. 3V3 VCC Sensor on Standalone ATTINY85
    5. 5V VCC sensor on I2C 5V VCC ATTINY85 Slave connected to Arduino UNO

    6. 3V3 VCC sensor on I2C 3V3 VCC ATTINY85 Slave connected to Arduino UNO

    #4 for some reason took ~15 seconds to become responsive where as #6 was responsive immediately.

    #1 - #4 had similar resolution as they were connected directly to the analog pin. #5 & #6 had less samples being reported as they where slowed by the I2C bus.

    The conclusion was there would be a limit to the applications the I2C sensor could be used for and that 5V/3V3 will be supported.

    Step 3: Prepare the ATTINY85

    NOTE: If intending to have Crouton integration, please use the library from here, and use the example installed "attiny_max9812".

    AttinyCore from the Boards Manager is needed. Burn bootloader "EEPROM Retained", "8mHZ Internal" (all config shown above).

    The GIST can be found here:

    https://gist.github.com/IOT-123/a6132924e753e993ed...

    You may find more details in these instructables:

    https://www.instructables.com/id/Programming-the-A...

    https://www.instructables.com/id/How-to-Program-AT...

    https://www.instructables.com/id/How-to-program-th...

    https://www.instructables.com/id/Programming-the-A...

    https://www.instructables.com/id/Programming-an-At...

    Best to test via breadboard before continuing.

    If you have existing ASSIMILATE SENSORS, make sure the slave address is different on a SENSOR/MCU Host combination i.e. all the Temperature sensors can have the same address as long as you only have one Temperature sensor on a MCU/node.

    Used for I2C BRICK.

    /*
    *
    * IOT123 ATTINY85 I2C SLAVE AUTO-JOIN LAYER FOR SENSOR: MAX9812
    *
    * Take readings on MAX9812 and send across wire on request via I2C in 3 segment 16byte packets
    * ID of PROPERTY (set in _properties)
    * VALUE of PROPERTY (set in getProperties)
    * MORE TO COME (0/1 0 = last property)
    *
    * Pins on ATTINY85
    * SDA PB0
    * SCL PB2
    * MAX9812 A3
    */
    #include<Wire.h>//SDA pin5/PB0, SCL pin7/PB2
    #definearraySize(x) (sizeof(x) / sizeof(x[0]))
    #defineNVC_NUM_STAGES3
    //------------------------------SENSOR SPECIFIC DECLARATIONS
    //#define _micSamples (1024*2)
    #define_micSamples (512)
    #defineADDRESS_SLAVE11
    #definePIN_SENSOR A3
    //--------------------------END SENSOR SPECIFIC DECLARATIONS
    #defineTIME_RESPONSE_MS0// will be last value sent to master + padding
    #if (TIME_RESPONSE_MS)
    unsignedlong startMillis;
    #endif
    structnvc
    {
    char Name[16];
    char Value[16];
    bool Continue;
    };
    //------------------------------SENSOR SPECIFIC METADATA
    //nvc _metas[8] = {
    // {"ASSIM_NAME", "MAX9812", true},
    // {"ASSIM_VERSION", "1", true},
    // {"ASSIM_ROLE", "SENSOR", true},
    // {"POWER_DOWN", "1", true},
    // {"PREPARES", "0", true},
    // {"RESPONSE_MS", "50", true},
    // {"MQTT_TOPIC", "MICROPHONE", true},
    // {"VCC_MV", "", false} // ALWAYS HAVE THIS LAST
    //};
    #defineMETA_COUNT9
    conststaticchar m1[] PROGMEM = "ASSIM_NAME";
    conststaticchar m2[] PROGMEM = "MAX9812";
    conststaticchar m3[] PROGMEM = "1";
    conststaticchar m4[] PROGMEM = "ASSIM_VERSION";
    conststaticchar m5[] PROGMEM = "1";
    conststaticchar m6[] PROGMEM = "1";
    conststaticchar m7[] PROGMEM = "ASSIM_ROLE";
    conststaticchar m8[] PROGMEM = "SENSOR";
    conststaticchar m9[] PROGMEM = "1";
    conststaticchar m10[] PROGMEM = "POWER_DOWN";
    conststaticchar m11[] PROGMEM = "1";
    conststaticchar m12[] PROGMEM = "1";
    conststaticchar m13[] PROGMEM = "PREPARE_MS";
    conststaticchar m14[] PROGMEM = "0";
    conststaticchar m15[] PROGMEM = "1";
    conststaticchar m16[] PROGMEM = "RESPONSE_MS";
    conststaticchar m17[] PROGMEM = "500";
    conststaticchar m18[] PROGMEM = "1";
    conststaticchar m19[] PROGMEM = "CLOCK_STRETCH";
    conststaticchar m20[] PROGMEM = "80000";
    conststaticchar m21[] PROGMEM = "1";
    conststaticchar m22[] PROGMEM = "MQTT_TOPIC";
    conststaticchar m23[] PROGMEM = "MICROPHONE";
    conststaticchar m24[] PROGMEM = "1";
    conststaticchar m25[] PROGMEM = "VCC_MV";
    conststaticchar m26[] PROGMEM = "";
    conststaticchar m27[] PROGMEM = "0";
    constchar* const _metas[] PROGMEM = { m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20, m21, m22, m23, m24, m25, m26, m27 };
    //--------------------------END SENSOR SPECIFIC METADATA
    //------------------------------SENSOR SPECIFIC PROPERTIES
    nvc _props[3] ={
    {"audMin (0-1023)", "", true},
    {"audMax (0-1023)", "", true},
    {"audDiff (0-50)", "", false}
    };
    //--------------------------END SENSOR SPECIFIC PROPERTIES
    volatileint _packetStage = 0;
    volatileint _propertyIndex = 0;
    volatilebool _metasConfirmed = false;
    volatileint _metaIndex = 0;
    uint16_t _vcc;
    voidsetup()
    {
    _vcc = getVcc();
    Wire.begin(ADDRESS_SLAVE);
    Wire.onReceive(receiveEvent);
    Wire.onRequest(requestEvent);
    }
    voidloop()
    {
    }
    voidreceiveEvent (int howMany)
    {
    byte buf[10];
    int i;
    for (i=0; i
    {
    buf[i] = Wire.read(); // receive byte as a character
    }
    if((buf[0] == 1) && (howMany == 1)){
    _metasConfirmed = true;
    _packetStage = 0;
    _propertyIndex = 0;
    }
    }
    voidrequestEvent() {
    char currentPacket[16];
    int propCount = 0;
    if (_metasConfirmed){
    // PROPERTIES
    if (_propertyIndex == 0){
    getProperties();
    }
    strcpy(currentPacket, nvcAsCharArray(_props[_propertyIndex], _packetStage));
    propCount = arraySize(_props);
    // METADATA
    }else{
    propCount = META_COUNT;
    if (_metaIndex == ((META_COUNT * 3) - 2)){// if last metadata (VCC), only runtime entry
    itoa(_vcc, currentPacket, 10);
    }else{ // just a normal metadata item
    //itoa(_metaIndex, currentPacket, 10);
    strcpy_P(currentPacket, (char*)pgm_read_word(&(_metas[_metaIndex])));
    }
    _metaIndex++;
    }
    Wire.write(currentPacket); // send metadate or sensor property
    _packetStage = _packetStage + 1;
    // go to next property if at last stage of current property
    if (_packetStage == NVC_NUM_STAGES){
    _packetStage = 0;
    _propertyIndex++;
    }
    // all properties processed?
    if (_propertyIndex == propCount){
    _propertyIndex = 0;
    // "0" should terminate requests to this slave
    }
    }
    //------------------------------SENSOR SPECIFIC PROPERTY READING
    voidgetProperties(){
    #if (TIME_RESPONSE_MS)
    startMillis = millis();
    #endif
    // read sensor
    unsignedint peakToPeak = 0; // peak-to-peak level
    unsignedint signalMax = 0;
    unsignedint signalMin = 1024;
    // collect data for 50 mS
    for (int i = 0; i < _micSamples; i++) // does not like millis!
    {
    int k = analogRead(PIN_SENSOR);
    signalMin = min(signalMin, k);
    signalMax = max(signalMax, k);
    }
    peakToPeak = signalMax - signalMin; // max - min = peak-peak amplitude
    // put values in table
    itoa(signalMin, _props[0].Value, 10);
    itoa(signalMax, _props[1].Value, 10);
    itoa(peakToPeak, _props[2].Value, 10);
    }
    //--------------------------END SENSOR SPECIFIC PROPERTY READING
    char* nvcAsCharArray(nvc nvc, int packetStage){
    switch (packetStage){
    case0:
    return nvc.Name;
    break;
    case1:
    #if (TIME_RESPONSE_MS)
    unsignedlong currentMillis;
    currentMillis = millis();
    char millis[16];
    itoa(currentMillis - startMillis, millis, 10);
    return millis;
    #endif
    return nvc.Value;
    break;
    case2:
    return nvc.Continue ? "1" : "0";
    break;
    default:
    char result[16];
    itoa(packetStage, result, 10);
    return result;
    }
    }
    // https://www.avrfreaks.net/forum/attiny85-vcc-measurement-skews-higher-vcc-voltages
    //5v = 6393, 6504
    //3.3V 3430
    uint16_tgetVcc() {
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
    ADMUX = _BV(MUX3) | _BV(MUX2);
    delay(2); // Wait for Vref to settle
    uint16_t result = 0;
    for (int x = 0; x < 32; x++){
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA,ADSC)); // measuring
    if (x >15){
    result += (int16_t)((int16_t)(ADC - result) / 2);
    }
    else{
    result = ADC;
    }
    }
    uint16_t voltage = 1125300 / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
    return voltage;
    }

    Step 4: Assemble the Circuit

    1. On the CJMCU-9812, attach the Electret Mic so that it is facing away from the 3 through-holes.
    2. On the front, insert the components ATTINY85 (1), 3P 90deg male headers (2)(3), 2P male headers (4)(5) using the glue if well ventilated, and solder off on the back.
    3. On the rear, trace a bare wire from SILVER1 to SILVER2 and solder.
    4. On the rear, trace a bare wire from SILVER3 to SILVER4 and solder.
    5. On the rear, trace a red wire from RED1 to RED2 and solder.
    6. On the rear, trace a red wire from RED3 to RED4 and solder.
    7. On the rear, trace a yellow wire from YELLOW1 to YELLOW2 and solder.
    8. On the rear, trace a blue wire from BLUE1 to BLUE2 and solder.
    9. On the rear, trace a green wire from GREEN1 to GREEN2 and solder.
    10. On the rear, trace a black wire from BLACK1 to BLACK2 and solder.
    11. On the rear, trace a black wire from BLACK3 to BLACK4 and solder.

    The sensor can now be connected directly via its pins to the PCB or via wires, to the points shown in the pin contract.

    Step 5: Testing

    A number of these BRICKS are expected to be on multiple nodes (MCUs - ESP8266 or ATTINY84) in an environment. This is a unit test: checks the UNO requests/responses until all the data has been dumped, then neglects the I2C slave.

    1. Upload the UNO code to your UNO test harness. Ensure ADDRESS_SLAVE matches the BRICK's I2C address.
    2. Connect the 3.3V or 5.0V on UNO to a VCC on BRICK.
    3. Ensure jumper for that pin is on.
    4. Connect the GND on UNO to GND on BRICK.
    5. Connect the A5 on UNO to SCL on BRICK.
    6. Connect the A4 on UNO to SDA on BRICK.
    7. Connect a 4K7 pull-up resistor from SDA to VCC.
    8. Connect a 4K7 pull-up resistor from SCL to VCC.
    9. Connect your UNO to your Dev PC with USB.
    10. Open the Arduino Console.
    11. Choose 9600 baud (restart the UNO and reopen the console if you have to).
    12. The output should be printed to the console once then the word sleep is repeated.

    I2C Master logging from I2C slave with plotter/metadata support.

    #include<Wire.h>
    #defineADDRESS_SLAVE10
    bool _outputPlotterOnly = false;
    bool _confirmedMetadata = false;
    int _packetSegment = 0;
    bool _i2cNodeProcessed = false;
    char _property[2][24] = {"name", "value"};
    voidsetup() {
    Wire.begin(); // join i2c bus (address optional for master)
    Serial.begin(9600); // start serial for output
    delay(1000);
    if (!_outputPlotterOnly){
    Serial.println("setup");
    Serial.println();
    }
    }
    voidloop() {
    if (_i2cNodeProcessed){
    if (!_confirmedMetadata){// let the slave know to start sending sensor data
    delay(1);
    Wire.beginTransmission(ADDRESS_SLAVE);
    Wire.write(1);
    Wire.endTransmission();
    delay(100);
    _confirmedMetadata = true;
    }
    _i2cNodeProcessed = false;
    if (!_outputPlotterOnly){
    Serial.println();
    }
    return;
    }
    Wire.requestFrom(ADDRESS_SLAVE, 16);
    _packetSegment++;
    char packet[16];
    intindex = 0;
    bool isContinueSegment = false;// continueSegment (the 3rd) 1=more, 0=last
    while (Wire.available()) { // slave may send less than requested
    char c = Wire.read();
    packet[index] = int(c) > -1 ? c : '';// replace invalid chars with spaces
    if (_packetSegment == 3){
    _packetSegment = 0;
    isContinueSegment = true;
    //Serial.println("-------------");
    //Serial.println(int(c));
    //Serial.println("-------------");
    if (int(c) == 48 || int(c) == 86){// 0 on last property
    _i2cNodeProcessed = true;
    // send values to MQTT
    break;
    }
    }
    index++;
    }
    if (!isContinueSegment){
    if (!_outputPlotterOnly){
    Serial.println(packet);
    }
    strcpy(_property[_packetSegment - 1], packet);// set local var with name/value
    }else{
    if (_outputPlotterOnly && _confirmedMetadata){
    if (_i2cNodeProcessed){
    Serial.println(_property[1]);
    }else{
    Serial.print(_property[1]);
    Serial.print("");
    }
    }
    }
    }

    Step 6: Next Steps

    The basic layout of the circuit and the I2C layer of the software is relate-able to many different sensors. The main thing to get right to start with, is the packet contract between master and slave.

    I have slated/started a (3D printed) packaged network of sensors that use this framework and will link to it as parts are published.