Monitoring Environmental Data with the BlueberryE UnoEVS

This tutorial shows how to use the UnoEVS sensor board to measure environmental data. We will use the UnoEVS in combination with a BlueberryE Uno335 board. In this use case, the Uno335 acts as a master which triggers all communication between the two boards. The Uno335 triggers the measurements on the UnoEVS and requests the measurement data. The UnoEVS controls the measurements on the sensors, performs calculations to transform the sensor data to physical values and sends the data to the master board. All communication between the two boards is done via the Serial Peripheral Interface (SPI). The Uno335 is the SPI master. The UnoEVS acts as SPI slave.

Content

  • The UnoEVS
  • Example circuit
  • Example software

The UnoEVS

Among others, the UnoEVS has the following parts on the board:

  • ATmega328P controller running at 3.3V.
  • BME280 (pressure, temperature, humidity) sensor connected to the ATmega via the Two Wire Interface (I2C).
    This sensor provides calibrated values for pressure, temperature and humidity.
  • LTR303ALS01 (ambient light) sensor connected to the ATmega via I2C.
    In order to cover the large wavelength range (approx. 380nm - 780nm) of visible light, this sensor has two channels: channel 0 with a sensitivity maximum at 450nm wavelength and channel 1 with a maximum at 770nm. The output of this channels are numbers which are proportional to the light intensity. The absolute values depend on the gain settings of the sensor.
  • ML8511 (UV) sensor connected to the ATmega Analog to Digital Converter (ADC).
    The ML8511 measures the UV intensity for a wavelength range of 280nm - 420nm. It has the maximum sensitivity at 370nm. Using the ADC output, an estimation for the UV index can be calculated.
  • TSSP58038 IR receiver.
    Will not be covered by this tutorial.
  • VSMB10940 IR sender.
    Will not be covered by this tutorial.
  • ISP header.
    Used for burning progams onto the Atmega328P.
The UnoEVS needs a 5V power supply. The SPI pins are operated at 3.3V.

Example Circuit

In this tutorial, the Uno335 provides the 5V and GND power supply to the UnoEVS. Both boards communicate via SPI. As the UnoEVS expects 3.3V at the SPI pins, we have to switch the SPI on the Uno335 to 3.3V using special jumper settings.

Jumper settings on the Uno335
Jumper settings for 3.3V SPI on the Uno335

After setting the jumpers, the UnoEVS can be connected to the Uno335. Just connect the SPI pins _SS (10), MOSI (11), MISO (12), SCK (13) and the 5V and GND pins.

UnoEVS connected to an Uno335
UnoEVS connected to an Uno335


Alternatively, you can mount the UnoEVS on top of the Uno335.

UnoEVS / Uno335 sandwich
UnoEVS / Uno335 sandwich

Example software

The software consists of two parts: One sketch for the Uno335 and code for the UnoEVS. For programming the Uno335, the Arduino IDE can be used directly. For programming the UnoEVS, you will need an AVR ISP programmer. For code development, we recommend to use also an IDE like Eclipse or AtmelStudio.

Sketch for the Uno335 (SPI master)

The Uno335 acts as the SPI master. It controls all the communication by sending commands to the UnoEVS and receiving responses from it. The UnoEVS is the SPI slave. Depending on the message, the UnoEVS triggers measurements from the sensors or provides measurement data to the Uno335. Both, the SPI master and slave know the following commands:

  • cmdBmeStart: Triggers the measurements of the BME280 (pressure, temperature, humidity) sensor.
  • cmdBmeGetTemperature: Triggers the transfer of the temperature data from the UnoEVS to the Uno335. As the temperature is a 32 bit value, four SPI transactions are needed. The most significant byte is transfered first. The 32bit value represents the temperature in degrees Celsius * 100.
  • cmdBmeGetPressure: Triggers the transfer of the pressure data from the UnoEVS to the Uno335. As the pressure is a 32 bit value, four SPI transactions are needed. The most significant byte is transfered first. The 32bit value represents the pressure in hPa * 100.
  • cmdBmeGetHumidity: Triggers the transfer of the humidity data from the UnoEVS to the Uno335. As the humidity is a 32 bit value, four SPI transactions are needed. The most significant byte is transfered first. The 32bit value represents the relative humidity in % * 1024.
  • cmdLtrStart: Triggers the measurements of the LTR303ALS01 (ambient light) sensor.
  • cmdLtrGetCh0: Triggers the transfer of the channel 0 (sensitivity max. at 450nm) data from the UnoEVS to the Uno335. As this is a 16 bit value, two SPI transactions are needed. The most significant byte is transfered first.
  • cmdLtrGetCh1: Triggers the transfer of the channel 1 (sensitivity max. at 770nm) data from the UnoEVS to the Uno335. As this is a 16 bit value, two SPI transactions are needed. The most significant byte is transfered first.
  • cmdMl8511Start: Triggers the measurements of the ML8511 (UV) sensor.
  • cmdMl8511GetValue: Triggers the transfer of the data from the UnoEVS to the Uno335. As this is a 16 bit value, two SPI transactions are needed. The most significant byte is transfered first.
  • cmdSleep: Receiving this command token, the UnoEVS will set the ATmega328P to sleep. A signal change on the _SS pin is used as wake up signal.

Here is the sketch for the Uno335. You can download it from Github here.


/**
 * Trigger measurements on an UnoEVS and receive data from it.
 *
 * v0.01 created 19. Oct. 2016
 * by Engelbert Mittermeier (BlueberryE GmbH)
 */

// The following two functions are used for SPI communication
// This can easily be replaced by the SPI.h library

// initialize the SPI as master
void my_Spi_init(){
  pinMode(SS, OUTPUT);
  SPCR |= _BV(MSTR);
  SPCR |= _BV(SPE);
  SPCR |= _BV(SPR1);
  SPCR |= _BV(SPR0);
  pinMode(SCK, OUTPUT);
  pinMode(MOSI, OUTPUT);
}

// used for data transfer (read and write)
uint8_t my_Spi_transfer(uint8_t inData){
  SPDR = inData;
    while (!(SPSR & _BV(SPIF))) ; // wait
    return SPDR;
}

// Define commands:

// set the UnoEVS to sleep mode
const uint8_t cmdSleep = 0xF0;                

// commands for the BME280 (Temperature, Pressure, Humidity):
// trigger the measurements
const uint8_t cmdBmeStart = 0x10;
// get the temperature value
const uint8_t cmdBmeGetTemperature = 0x11;
// get the pressure value
const uint8_t cmdBmeGetPressure = 0x12;
// get the humidity value
const uint8_t cmdBmeGetHumidity = 0x13;

// commands for the LTR303ALS01 (visible light):
// trigger the measurements
const uint8_t cmdLtrStart = 0x20;
// get the value from channel 0
const uint8_t cmdLtrGetCh0 = 0x21;
// get the value from channel 0
const uint8_t cmdLtrGetCh1 = 0x22;

// commands for the ML8511 (UV):
// trigger the measurements
const uint8_t cmdMl8511Start = 0x30;
// get the value from the sensor
const uint8_t cmdMl8511GetValue = 0x31;


// Define variables taking the measurement data:
// variables for the bme280
uint8_t bmeTemperature[4]; // the temperature value consists of 4 bytes
uint8_t bmePressure[4];    // the pressure value consists of 4 bytes
uint8_t bmeHumidity[4];    // the humidity value consists of 4 bytes

// variables for the ltr303als01 (visible light)
uint8_t ltrVisibleCh0[2];  // the value from channel 0 consists of 2 bytes
uint8_t ltrVisibleCh1[2];  // the value from channel 1 consists of 2 bytes

// variable for the ml8511 (uv)
uint8_t ml8511Value[2];    // the value from the ml8511

// waiting times: time for wake up, sensor measurement time, 
// calculation time, ...
const uint16_t waitTime = 10;
//10ms is too short -> for the ml8511 use 200ms before reading the data
const uint16_t waitTimeMl8511 = 200; 

void setup() {
  Serial.begin(9600);
  digitalWrite(SS, HIGH);
  my_Spi_init();
  digitalWrite(SS, LOW);
  my_Spi_transfer(cmdSleep);
  digitalWrite(SS, HIGH);
  Serial.println("Setup completed");
}

void loop() {

    // Trigger BME280 meaurements and get data:

    digitalWrite(SS, LOW); // select the UnoEVS + wake up
    delay(waitTime);

    // trigger the bme280 measurements
    my_Spi_transfer(cmdBmeStart);
    delay(waitTime);

    // fetch the bme280 data from the EVS
    my_Spi_transfer(cmdBmeGetTemperature);
    for (unsigned int i = 0; i < sizeof(bmeTemperature); i++)
        bmeTemperature[i] = my_Spi_transfer(0xFF);
    my_Spi_transfer(cmdBmeGetPressure);
    for (unsigned int i = 0; i < sizeof(bmeTemperature); i++)
        bmePressure[i] = my_Spi_transfer(0xFF);
    my_Spi_transfer(cmdBmeGetHumidity);
    for (unsigned int i = 0; i < bmeTemperature); i++)
        bmeHumidity[i] = my_Spi_transfer(0xFF);

    my_Spi_transfer(cmdSleep); // set the UnoEVS to sleep

    digitalWrite(SS, HIGH); 

    // print out the data or do some other stuff:
    Serial.print("T = "); 
    Serial.print(convertArray(bmeTemperature, 4, 1.0 / 100.0));
    Serial.println("degC");
    Serial.print("P = ");
    Serial.print(convertArray(bmePressure, 4, 1.0 / 100.0));
    Serial.println("hPa");
    Serial.print("H = ");
    Serial.print(convertArray(bmeHumidity, 4, 1.0 / 1024.0));
    Serial.println("%");

    // Trigger LTR303ALS01 (visible light) meaurements and get data:

    digitalWrite(SS, LOW); // select the UnoEVS + wake up
    delay(waitTime);

    // trigger the ltr303als01 measurements
    my_Spi_transfer(cmdLtrStart);
    delay(waitTime);

    // fetch the ltr303als01 data from the EVS
    my_Spi_transfer(cmdLtrGetCh0);
    for (unsigned int i = 0; i < sizeof(ltrVisibleCh0); i++) 
        ltrVisibleCh0[i] = my_Spi_transfer(0xFF);
    my_Spi_transfer(cmdLtrGetCh1);
    for (unsigned int i = 0; i < sizeof(ltrVisibleCh1); i++)
        ltrVisibleCh1[i] = my_Spi_transfer(0xFF);

    my_Spi_transfer(cmdSleep); // set the UnoEVS to sleep

    digitalWrite(SS, HIGH); 

    // print out the data or do some other stuff:
    Serial.print("CH0 = "); 
    Serial.println(convertArray(ltrVisibleCh0, 2, 1.0));
    Serial.print("CH1 = ");
    Serial.println(convertArray(ltrVisibleCh1, 2, 1.0));

    // Trigger ML8511 (UV) meaurements and get data:

    digitalWrite(SS, LOW); // select the UnoEVS + wake up
    delay(waitTime);

    // trigger the ml8511 measurements
    my_Spi_transfer(cmdMl8511Start);
    delay(waitTimeMl8511);

    // fetch the ml8511 data from the EVS
    my_Spi_transfer(cmdMl8511GetValue);
    for (unsigned int i = 0; i < sizeof(ml8511Value); i++)
        ml8511Value[i] = my_Spi_transfer(0xFF);

    my_Spi_transfer(cmdSleep); // set the UnoEVS to sleep

    digitalWrite(SS, HIGH); 

    // print out the data or do some other stuff:
    Serial.print("UV = ");
    Serial.println(convertArray(ml8511Value, 2, 1.0));

    Serial.println("--------------------------------");
    delay(2000); // wait 2 s before the next cycle
}

/**
 * Converts an array containing raw measurement data to one value.
 * @param measValues[] the raw data with sequence from high to low
 * @param dataCount the number of values in measValues[]
 * @param scale a value used for scaling the resulting data.
 * 
*/
float convertArray(uint8_t measValues[], uint8_t dataCount, float scale){
    uint32_t tempValue = 0x00;
    for (unsigned int i = 0; i < dataCount; i++)
        tempValue = tempValue | ((uint32_t) measValues[i] << ( (dataCount - 1 - i) * 8));
    return tempValue * scale;
}    

C++ code for the unoEVS (SPI slave)

Here is the code for the UnoEVS. The main program (link to repository) and the libraries (link to repository) can be downloaded from Github. Include this code in your IDE and compile an executable. Alternatively, you can use this hex file (link to repository) and burn it onto the UnoEVS.


/**
 * BB_EVS.cpp - this is an example for the usage of the BlueberryE UnoEVS.
 * It shows how to control the UnoEVS with commands transferred via SPI.
 * Measurements are triggered from a master by "external" SPI commands. This
 * master may e.g. be an Uno335. The measurement results are transferred back
 * the master. All communication between master and UnoEVS is controlled by
 * the master. The UnoEVS triggers the measurements in the sensors of
 * the board and performs all necessary calculations to get physical values.
 *
 * In order to reduce the power consumption, the Atmega328P of the UnoEVS
 * is set to sleep between the measurements. A signal change on the
 * SPI slave select pin triggers the wake up of the controller.
 *
 *  Created on: Oct 18, 2016
 *      Author: E. Mittermeier, BlueberryE
 *  Released into the public domain.
 */

// as we use 3.3V, 8MHZ is a reasonable operation frequency for
// the Atmega328P
#define F_CPU 8000000UL

// some convenience definitions
#define redLedOn  PORTD |= (1 << PD7)
#define redLedOff PORTD &= ~(1 << PD7)
#define greenLedOn PORTB |= (1 << PB0)
#define greenLedOff PORTB &= ~(1  << PB0)

extern "C" {
    #include <avr/io.h>
    #include <stdint.h>
    #include <util/delay.h>
    #include <avr/power.h> 
    #include <avr/wdt.h>
    #include <avr/sleep.h>
    #include <avr/interrupt.h>
}

// include the libraries for the sensors
#include <BB_I2C.h>
#include <BB_BME280.h>
#include <BB_LTR303ALS01.h>
#include <BB_ML8511.h>

/**
 * Initiate the SPI settings. Note - the controller
 * of this board will act as SPI slave. SPI communication
 * does not use interrupts ("polling mode")
 */
void SPI_SlaveInit_woInt(void){

    volatile char dummy;

    // Set PB4(MISO) as output - all others are input
    DDRB |= (1 << PB4);

    //Enable SPI in Slave Mode
    SPCR  = (1 << SPE);

    dummy   = SPSR;   // Clear SPIF bit in SPSR
    dummy   = SPDR;
}

/**
 * Transfer data via SPI.
 * @param inData for write operations: this is the data which
 *               will be transferred from slave to master
 *               for read operations: no meaning
 * @return for read operations: the data which was transferred from
 *         master to slave
 *         for write operations: no meaning
 */
uint8_t SPI_transferData(uint8_t inData){
    SPDR = inData;
    while (!(SPSR & (1 << SPIF)));
    return SPDR;
}

// define an interrupt service routine which we will need to wake
// up the processor from sleep.
// Trigger will be a signal change at the SPI slave select pin.
ISR(PCINT0_vect){
}

int main(void){
    // the processor works with 3.3V, thus we need 8 MHz
    clock_prescale_set(clock_div_2); // set MCU freq to 16/2 MHz

    // power saving
    wdt_disable();  // switch off watchdog

    // define ports
    PORTC = 0xFF;
    DDRD = (1 << PD6) | (1 << PD7);
    PORTD = (1 << PD1) | (1 << PD3) | (1 << PD4) | (1 << PD5);
    DDRB = (1 << PB0) | (1 << PB1);

    greenLedOff;
    redLedOff;

    // initialization

    SPI_SlaveInit_woInt();

    BB_I2C i2c;

    BB_BME280 bme(&i2c);
    // check the initialization
    if (bme.readChipId() != 0x60){
        redLedOn;
        while(1);
    }

    BB_LTR303ALS01 ltr(&i2c);
    // check the initialization
    if (ltr.readManufacturerId() != 0x05){
        greenLedOn;
        return false;
    }

    BB_ML8511 ml8511;

    // define variables and command codes

    uint8_t newCommand = 0xFF;

    const uint8_t cmdSleep = 0xF0;

    const uint8_t cmdBmeStart = 0x10;
    const uint8_t cmdBmeGetTemperature = 0x11;
    const uint8_t cmdBmeGetPressure = 0x12;
    const uint8_t cmdBmeGetHumidity = 0x13;

    int32_t bmeTemperature;
    uint32_t bmePressure;
    uint32_t bmeHumidity;

    const uint8_t cmdLtrStart = 0x20;
    const uint8_t cmdLtrGetCh0 = 0x21;
    const uint8_t cmdLtrGetCh1 = 0x22;
    uint16_t ltrCh0;
    uint16_t ltrCh1;

    const uint8_t cmdMl8511Start = 0x30;
    const uint8_t cmdMl8511GetValue = 0x31;
    uint16_t ml8511UvLevel;

    // initialize interrupt handling:

    // PB2 is input and is used as _SS
    // we will use it also for external interrupt to wake up the processor
    // Special function PCINT2: PB2 triggers a PCI0 interrupt request
    // disable interrupts during set up
    cli(); 
    // Pin Change Interrupt Control Register: activate PCI0
    PCICR |= (1 << PCIE0);
    // Pin Change Mask Register: Pin change on PB2 will trigger
    // a PCI0 interrupt request
    PCMSK0 |= (1 << PCINT2);
    sei(); // enable interrupts again

    while(1){
        newCommand = SPI_transferData(0xFF);
        switch(newCommand){
            case cmdBmeStart:
                // do the measurements
                bmeTemperature = bme.readTemperature();
                bmePressure = bme.readPressure();
                bmeHumidity = bme.readHumidity();
                cli();
            break;
            case cmdBmeGetTemperature:
                // send the temperature data (4 bytes)
                SPI_transferData((uint8_t) (bmeTemperature >> 24));
                SPI_transferData((uint8_t) (bmeTemperature >> 16));
                SPI_transferData((uint8_t) (bmeTemperature >> 8));
                SPI_transferData((uint8_t) bmeTemperature);
            break;
            case cmdBmeGetPressure:
                // send the temperature data (4 bytes)
                SPI_transferData((uint8_t) (bmePressure >> 24));
                SPI_transferData((uint8_t) (bmePressure >> 16));
                SPI_transferData((uint8_t) (bmePressure >> 8));
                SPI_transferData((uint8_t) bmePressure);
            break;
            case cmdBmeGetHumidity:
                // send the temperature data (4 bytes)
                SPI_transferData((uint8_t) (bmeHumidity >> 24));
                SPI_transferData((uint8_t) (bmeHumidity >> 16));
                SPI_transferData((uint8_t) (bmeHumidity >> 8));
                SPI_transferData((uint8_t) bmeHumidity);
            break;
            case cmdLtrStart:
                ltrCh0 = ltr.readChannel0();
                ltrCh1 = ltr.readChannel1();
                cli();
            break;
            case cmdLtrGetCh0:
                // send the visible light (2 bytes)
                SPI_transferData((uint8_t) (ltrCh0 >> 8));
                SPI_transferData((uint8_t) ltrCh0);
            break;
            case cmdLtrGetCh1:
                // send the visible light (2 bytes)
                SPI_transferData((uint8_t) (ltrCh1 >> 8));
                SPI_transferData((uint8_t) ltrCh1);
            break;
            case cmdMl8511Start:
                // do the measurements
                ml8511UvLevel = ml8511.readUvLevel(3);
                cli();
            break;
            case cmdMl8511GetValue:
                // send the uv level (2 bytes)
                SPI_transferData((uint8_t) (ml8511UvLevel >> 8));
                SPI_transferData((uint8_t) ml8511UvLevel);
            break;
            case cmdSleep:
                set_sleep_mode(SLEEP_MODE_PWR_DOWN);
                power_adc_disable();
                sleep_enable();
                sei();
                sleep_cpu();
                sleep_disable();
                if (PINB & (1 << PB2) ){
                    // SS is high: this was the wrong signal, thus sleep again
                    sleep_enable();
                    sleep_cpu();
                    sleep_disable();
                }
                power_adc_enable();
            break;
        }
    }
}