Implementing the MICS-VZ-89T gas sensor on Intel Edison i2c

On my current project I have the requirement to monitor indoor air quality. What is of interest are the levels of Volatile Organic Compounds (VOCs) and CO2.  There are specific thresholds that we are looking for that when exceeded should trigger an action. For VOCs it is when the concentration is greater that 0.9 ppm. For CO2 it is when the concentration is 1000 ppm above ambient out side C02 — which is generally around 400 ppm. The links above out line the dangers of these indoor pollutants. When the threshold is reached we want to start the ventilation system and optionally message a user.

When I need a sensor my first choice is to find one that implements i2c. In this case I found a good candidate for the job in the SGX Sensortech MICS-VZ-89T. The VZ89 product is a small board with a MICS SMD device integrated with an i2c controller. The board comes in both 5 volt (VZ89) and 3.3 volt (VZ89T) versions that are are easy to implement using a logic level shifter with the Edison on a mini breakout board. (For an example of using a logic level shifter you can see my article Intel Edison and I2C sensors with XDK.)

There was no driver that I could find for implementing the board with my setup so I had to roll my own. This wasn’t too hard, but I did have to break out the logic analyzer to get it right. If we examine the MICS-VZ-89T I2C Specification page  we see that the device only has two commands. These are Set ppmC02 and Get VZ89 Status. According to the MICS-VZ-89T Data Sheet the device comes calibrated from the factory so we don’t need to implement Set ppmC02. That leaves us Get VZ89 Status. The code that follows here is available in an XDK project on git hub.

To read the status we have to perform a two step process. First we write a command byte of 0x09 to register address 0x70. We follow this by writing two data bytes to the same register. I write 0x00 twice.

We then read 6 bytes immediately after writing the command byte. The bytes are decoded as follows:

Data byte 1 = CO2-equivalent value. 
Data byte 2 = VOC_SHORT value. 
Data byte 3 = VOC_LONG value 
Data byte 4 = Raw sensor 1st byte (LSB). 
Data byte 5 = Raw sensor 2nd byte 
Data byte 6 = Raw sensor 3rd byte (MSB).

To implement the functionality I created the following class in a node module:

//Import mraa 
var mraa = require('mraa');

//Constructor -- set defaults and populate tx_buf
function VZ89(bus , address){

 this.bus = new mraa.I2c(bus || 1); 
 this.bus.address(address || 0x70); 
 
 this.tx_buf = new Buffer(3); 
 this.tx_buf[0] = 0x09;
 this.tx_buf[1] = 0x00;
 this.tx_buf[2] = 0x00; 
 
}

//Add a function to get the device readings. 
VZ89.prototype.getReadings = function() {
 this.bus.frequency(mraa.I2C_STD);
 this.bus.write(this.tx_buf); 
 return this.bus.read(6);
 
};
//Export as a node module 
module.exports = VZ89;

The MRAA library is used to access the i2c bus, so it is imported at the top or the file. There is a constructor that optionally takes a bus number and a devices address. The VZ89 is addressed at 0x70 so we don’t really need to change that. If the Edison mini-breakout is used we have a choice of busses. I have set this to bus 6 as I am using the Edison Arduino for this example.

A class function is added to implement named getReadings to perform the measurements and return data from the device. In this case the buffer is passed back to the calling program for use.

To use the class the code in the server file would look like this:

//Import our sensor file from the file system
var Sensor = require('./VZ89.js'); 
//Create an instance of the sensor object. 
var sensor = new Sensor();
//Create a var for the receive buffer.
var rx_buf; 

//Call the readBuf function every minute. 
setInterval(readBuf , 60000); 

//A function to read the sensor data, perform data conversions and display on the console every minute.
function readBuf(){

    rx_buf = sensor.getReadings();
 
    console.log("Co2_equ: " + ((rx_buf[0] - 13) * (1600/229) + 400) + " ppm"); 
    console.log("VOC_short: " + (rx_buf[1])); 
    console.log("tVOC: " + (rx_buf[2] * (1000/229)) + " ppb"); 
    console.log("Resistor Value: " + 10 * (rx_buf[3] + (256 * rx_buf[4]) + (65536 * rx_buf[5])) + " ohms"); 
}

The details of converting the rx_buf data to usable values are in the data sheet.  The ones we are most interested in are Co2 equivalent (rx_buf[0]) and total VOC (rx_buf[2]).

As you can see, the MICS-VZ89T is a pretty easy to use device once you know how. There are only a couple of gotchas to be aware of. First, the device can only be polled once a second. I find if I try to get the readings faster than that the device will return nulls. Secondly, care must be taken when handling the device. It contains organic material that is susceptible to solvents.

One comment

  1. Kavindran says:

    Hi Marc,

    it is impressive that you have dealt with the sensor so smoothly. I am using the same sensor talking to PIC micro controller (16f877a), but i am having some trouble with it. This is the driver function which i wrote for reading the data from the sensor

    void readmem(unsigned int reg, unsigned int buff[], unsigned int size)
    {
    unsigned int i =0;
    i2c_start();
    i2c_write(VZ89_ADDR | WRITE); ////// E0
    i2c_write(reg); /////09
    i2c_delay(); //////1ms
    i2c_rep_start(); /////
    i2c_write(VZ89_ADDR | READ); /////E1
    for(i=0; i<size; i++) ////size=6
    {
    if(i==size-1)
    {
    buff[i] = i2c_readNak();
    }
    else
    {
    buff[i] = i2c_readAck();
    }
    }
    i2c_stop();
    }
    bit read_main()
    {
    float co2;
    unsigned int reactivity;
    float tvoc;
    unsigned int data[6];
    readmem(VZ89_GETSTATUS, rawData, 6);
    if (data[0] < 13 || data[1] < 13 || data[2] < 13)
    {
    sendstringserially("d[0]= ");
    USART_INT(data[0]);
    sendstringserially("| d[1]= ");
    USART_INT(data[1]);
    sendstringserially("| d[2]= ");
    USART_INT(data[2]);
    sendstringserially("\n\r");
    __delay_ms(100); ///// 1hz; variation in every 2 readings, apparently.
    return 0;
    }
    sendstringserially("d[0]= ");
    USART_INT(data[0]);
    sendstringserially("| d[1]= ");
    USART_INT(data[1]);
    sendstringserially("| d[2]= ");
    USART_INT(data[2]);
    co2 = (float)(data[0] – 13) * (1600.0 / 229) + 400; // ppm: 400 .. 2000
    reactivity = data[1];
    tvoc = (float)(data[2] – 13) * (1000.0/229); // ppb: 0 .. 1000
    sendstringserially("| CO2= ");
    USART_INT(co2);
    sendstringserially("PPM");
    sendstringserially("| Reactivity=");
    USART_INT(reactivity);
    sendstringserially("| VOC= ");
    USART_INT(tvoc);
    sendstringserially("PPB");
    sendstringserially("\n\r");
    __delay_ms(100);
    //unsigned int32 resistor = 10 * (data[3] +256 * data[4] + 65536 * data[5]);
    return 1;
    }

    and my respective driver function looks like this:

    #define I2C_WRITE 0
    #define I2C_READ 1
    #define clk1 49 ///100khz
    void i2c_init(void);
    unsigned int i2c_write(unsigned char data);
    void i2c_wait(void);
    void i2c_start(void);
    void i2c_rep_start(void);
    void i2c_stop(void);
    void i2c_Address(unsigned char address, unsigned char mode);
    unsigned int i2c_Read(unsigned char ack);
    unsigned int i2c_readNak();
    unsigned int i2c_readAck();
    void i2c_delay();

    void i2c_init(void)
    {
    TRISC3=1; // SDA
    TRISC4=1; // SCL, Change to 0 for testing
    SSPCON = 0b00101000; // I2C enabled, Master mode
    SSPCON2 = 0x00; // I2C Master mode, clock = FOSC/(4 * (SSPADD + 1))
    SSPADD = clk1; // 100Khz @ 20Mhz Fosc
    SSPSTAT = 0b11000000; // Slew rate disabled
    }
    unsigned int i2c_write(unsigned char data)
    {
    int ack_s;
    i2c_wait();
    SSPBUF = data;
    ack_s=ACKSTAT;
    return ack_s;
    }
    void i2c_wait(void)
    {
    while ( ( SSPCON2 & 0x1F ) || ( SSPSTAT & 0x04 ) );
    }

    void i2c_start(void)
    {
    i2c_wait();
    SEN=1;
    }
    void i2c_rep_start(void)
    {
    i2c_wait();
    RSEN=1;
    }
    void i2c_stop(void)
    {
    i2c_wait();
    PEN=1;
    }

    unsigned int i2c_address(unsigned char address, unsigned char mode)
    {
    int w_ack;
    unsigned char l_address;
    l_address=address<<1;
    l_address+=mode;
    i2c_wait();
    SSPBUF = l_address;
    w_ack=ACKSTAT;
    return w_ack;
    }

    unsigned int i2c_read(unsigned int ack)
    {
    // Read data from slave
    // ack should be 1 if there is going to be more data read
    // ack should be 0 if this is the last byte of data read
    unsigned int i2cReadData;
    i2c_wait();
    RCEN=1;
    i2c_wait();
    i2cReadData = SSPBUF;
    i2c_wait();
    if ( ack )
    ACKDT=0; // Ack
    else
    ACKDT=1; // NAck
    ACKEN=1; // send acknowledge sequence
    return(i2cReadData );
    }
    unsigned int i2c_readNak()
    {
    unsigned int rnack;
    rnack=i2c_read(0);
    return rnack;
    }
    unsigned int i2c_readAck()
    {
    unsigned int rack;
    rack=i2c_read(0);
    return rack;
    }
    void i2c_delay()
    {
    __delay_ms(1);
    }

    And the readings sometimes look like this(when I use the logic level shifter, because my controller is a 5v device):

    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 0

    and sometimes looks like this(without the level shifter):

    d[0]= 146| d[1]= 146| d[2]= 146| CO2= 1329PPM| Reactivity=146| VOC= 580PPB
    d[0]= 146| d[1]= 146| d[2]= 146| CO2= 1329PPM| Reactivity=146| VOC= 580PPB
    d[0]= 146| d[1]= 146| d[2]= 146| CO2= 1329PPM| Reactivity=146| VOC= 580PPB
    d[0]= 146| d[1]= 146| d[2]= 146| CO2= 1329PPM| Reactivity=146| VOC= 580PPB
    d[0]= 146| d[1]= 146| d[2]= 146| CO2= 1329PPM| Reactivity=146| VOC= 580PPB
    d[0]= 0| d[1]= 42| d[2]= 85
    d[0]= 168| d[1]= 21| d[2]= 136| CO2= 1482PPM| Reactivity=21| VOC= 537PPB
    d[0]= 130| d[1]= 20| d[2]= 2
    d[0]= 130| d[1]= 80| d[2]= 0
    d[0]= 17| d[1]= 170| d[2]= 85| CO2= 427PPM| Reactivity=170| VOC= 314PPB
    d[0]= 85| d[1]= 162| d[2]= 4
    d[0]= 4| d[1]= 162| d[2]= 85
    d[0]= 170| d[1]= 0| d[2]= 0
    d[0]= 0| d[1]= 0| d[2]= 65
    d[0]= 21| d[1]= 170| d[2]= 68| CO2= 455PPM| Reactivity=170| VOC= 240PPB
    d[0]= 168| d[1]= 85| d[2]= 170| CO2= 1482PPM| Reactivity=85| VOC= 685PPB
    d[0]= 160| d[1]= 0| d[2]= 8
    d[0]= 0| d[1]= 0| d[2]= 1
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 130| d[1]= 21| d[2]= 130| CO2= 1217PPM| Reactivity=21| VOC= 510PPB
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 170| d[1]= 85| d[2]= 168| CO2= 1496PPM| Reactivity=85| VOC= 676PPB
    d[0]= 170| d[1]= 85| d[2]= 170| CO2= 1496PPM| Reactivity=85| VOC= 685PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB
    d[0]= 255| d[1]= 255| d[2]= 255| CO2= 2090PPM| Reactivity=255| VOC= 1056PPB

    Just tell me where am i wrong

    P.S.: Sorry for the long reply : )

Leave a Reply