Infineon PAS CO2 Sensor ESP32 MQTT Publisher

Introduction

Hardware

Programming

>>>>>>Main Program

>>>>>>Communication With Sensor Via UART

Conclusion

Introduction

I recently came across this interesting CO2 sensor that uses photoacoustic spectroscopy and is not too expensive. The evaluation board that I purchased was only 30 USD. It has both an I2C interface and an UART interface. I decided to use the UART interface to communicate directly with the device registers to control and operate it. I found that programming this device was a bit more involved than the programming and commissioning of the Sensiron SCD30 CO2 sensor that uses NDIR (non-dispersive infrared) to measure CO2 concentration. That project was done earlier and used the Raspberry Pi Zero W to operate the sensor and to do an MQTT publish to the cloud. Other sensors earlier deployed using ESP32 and ESP8266 platforms were the CCS811, Plantower PMS1003, Honeywell HPMA115S0, Shinyei PPD42NS, and BME280. All of these had available C code drivers and/or command sets that made programming much easier.

In what follows is a description of programming and operating this sensor. The hardware set up is strictly a breadboard using an assembled ESP32 custom board of my design that I have used in other projects.

Hardware

The Infineon website has a complete set of documentation that includes the device hardware and its specifications. The image below shows the breadboard that was put together for this project.

CO2 Sensor with ESP32 Board

Out of sight to the left is a plug-in 12 VDC charger that supplies the sensor’s heater power along with 5.0 VDC for the ESP32 board through the LM340 regulator. 3.3 VDC is generated on the ESP32 board and is supplied to the sensor. Also out of sight to the left is the UART connection to the ESP32 board from the laptop running Ubuntu 16.04 on a Oracle VM. Documentation guidelines specify that the sensor should not be subjected to heat sources that would cause it to operate at elevated temperatures, and the lay-out shown follows this guideline. The LM340 as well as the ESP32 board are the principal heat sources and are sufficiently apart from the sensor board.

The sensor board itself is shown in the next image.

XSENIV PAS CO2 Evaluation Board

The programming guide recommends that start up should be 3.3 VDC applied first followed by 12 VDC. Shut down is recommended to be the reverse of this sequence. This is easily done with the breadboard, but in a complete stand-alone design these sequences create considerable complication. So far this recommendation has been ignored and no apparent consequences have been experienced. Also, no de-noise coupling capacitors on the 12 VDC have been used but these are easily added in a completed design.

Programming

I do all Espressif programming using their SDKs. I never use Arduino hardware nor software libraries. I find them unnecessary. The particular ESP-IDF version used in this project is v3.2-dirty. In the SDK there are three example programs the code of which was adapted and used in this project. The directories are:

  • esp-idf/examples/peripherals/uart_echo/
  • esp-idf/examples/protocols/mqtt/tcp/
  • esp-idf/examples/protocols/sntp/

The last module was used in order to date and time stamp all published MQTT data. The program uses freertos and is structured essentially as three tasks: a wifi task, an MQTT client task for publishing, and finally a UART task for operating the sensor and grabbing data which is sent to the MQTT client task via an event handler. The MQTT client sends to a broker running in my personal LAN. Therefore, SSL/TLS was not used.

I did not find any existing C/C++ drivers for this sensor, so the project became more involved since I needed to code my own by carefully reading the documentation provided by Infineon.

Main Program

Below I show the main program, app_main.c and discuss some relevant pieces of it.

#include "driver/uart.h"
#include "driver/gpio.h"
#include "soc/timer_group_reg.h"  //needed for watchdog timer management
#include "soc/timer_group_struct.h"
#include "driver/periph_ctrl.h"
#include "driver/timer.h"
#include <stdint.h>
#include <stdbool.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_sleep.h"
#include "lwip/err.h"
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event_loop.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "lwip/netdb.h"
#include "mqtt_client.h"
#include "lwip/apps/sntp.h"
#include <time.h>
#include <sys/time.h>
#include "uart_PAS_CO2.h"

#define ECHO_TEST_TXD  (GPIO_NUM_17)
#define ECHO_TEST_RXD  (GPIO_NUM_16)
#define ECHO_TEST_RTS  (UART_PIN_NO_CHANGE)
#define ECHO_TEST_CTS  (UART_PIN_NO_CHANGE)
#define BUF_SIZE (1024)


#define TASK_STACK_DEPTH 3072  //2048 will result in stack overflow
#define PUB_MSG_LEN  130
#define TOPIC "Esp/XENSIV_PAS_CO2"
#define SNTP_SERVERS "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"
#define CONFIG_WIFI_SSID "mynetwork"
#define CONFIG_WIFI_PASSWORD "mypassword"

static const char *TAG = "MQTT_EXAMPLE";

static EventGroupHandle_t wifi_event_group;
const static int CONNECTED_BIT = BIT0;
size_t nbytes;
uint8_t i=0;
static char strftime_buf[64];
int result_ppm;
char ip_str[50];
ip_addr_t myaddr;
struct timeval tv_now;
static char buf[256];
char buff[256];
//static char msg[PUB_MSG_LEN];


void user_task(void *pvParameters)
{  //we pass the mqtt client handle to this task
    esp_mqtt_client_handle_t client = pvParameters;
    /* Configure parameters of an UART driver,
     * communication pins and install the driver */
    uart_config_t uart_config = {
        .baud_rate = 9600,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
    };
    uart_param_config(UART_NUM_2, &uart_config);
    uart_set_pin(UART_NUM_2, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS);
    uart_driver_install(UART_NUM_2, BUF_SIZE * 2, 0, 0, NULL, 0);

    // Configure a temporary buffer for the incoming data
    uint8_t *data = (uint8_t *) malloc(BUF_SIZE);
	//The rate of measurement is set to 10 seconds
	printf("WRITE MEAS RATE............................\n");
    write_meas_rate( (uint8_t*) data, (uint8_t) 0x0A);
	//The pressure reference is set to 1013 millibars(standard sea level pressure)
	//write PRES_REF_H and PRES_REF_L
    printf("WRITE PRESS REF............................\n");
    write_press_ref((uint8_t*) data, 0x03F5);
	//Assuming the sensor is in a fresh air environment
    //do a forced calibration sequence using 400 ppm as reference.
	//The following sequence follows that described in the application note.
    set_idle( data);
    printf("WRITE CALIBRATION REF............................\n");
    write_calib_ref((uint8_t*) data, 0x0190);//400 PPM
    set_forced( data);
    for( int i=0;i<10;i++){
	//read 10 ppm values 10 second meas rate
	while ( read_device_ready( data) == 0 ){
	//loop until device ready with new data
	}	
	read_PPM_CO2((uint8_t*) data );
    }
    //set idle
    set_idle( data);
    //do SENS_RST=0xCF
    set_sens_rst( data, 0xCF);
    //set continous
    set_continuous( data);
	//now begin the measurement loop
while (1)
    {
        setenv("TZ", "PST8DPT,M3.2.0,M11.1.0", 1);
//    setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
        tzset();

//two ways of presenting local time
        time_t now;
        struct tm timeinfo;
        time(&now);
        localtime_r(&now, &timeinfo);
        strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
        printf("local time is: %s\n", strftime_buf);
//        time_t ts = time(NULL);
		time_t ts;
		time(&ts);
        struct tm *tm = localtime(&ts);
		printf("localtime: %02d/%02d/%02d-%02d:%02d:%02d\n", tm->tm_mon+1, tm->tm_mday, tm->tm_year+1900, tm->tm_hour, tm->tm_min, tm->tm_sec);
        nbytes = 0;  //size of message
        ESP_LOGI(TAG, "Free heap size: %d\n", esp_get_free_heap_size());//used for debugging
        printf("topic = %s\n", TOPIC);
		//get data
        for( int i=0;i<3;i++){
            //read 3 ppm values 10 second meas rate which will consume 30 seconds
            while ( read_device_ready( data) == 0 ){
            //loop until device ready with new data
            }       
            result_ppm =  read_PPM_CO2((uint8_t*) data );
        }
		//get size of message to be sent
		nbytes = snprintf(NULL, 0, "CO2 in PPM = %d ppm,  %s\n", result_ppm, strftime_buf);
		//now put message in the buffer
		snprintf(buff, nbytes, "CO2 in PPM = %d ppm,  %s\n", result_ppm, strftime_buf);	
        if (strlen(buff) >= 256 ){
                memcpy(buf, buff, 256);
        }else{
                memcpy(buf, buff, sizeof(buff));
        }

        printf("buf=%s\n", buf);
        esp_mqtt_client_publish(client, TOPIC, buf, nbytes, 1, 0);
    }
}

void startSNTP()
{
    ESP_LOGI(TAG, "Initializing SNTP");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_init();
    // Set timezone to Eastern Standard Time and print local time
    // setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
    // tzset();

    setenv("TZ", "PST8DPT,M3.2.0,M11.1.0", 1);
    tzset();	
}


static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event)
{
    esp_mqtt_client_handle_t client = event->client;
    int msg_id;
    // your_context_t *context = event->context;
    switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
            msg_id = esp_mqtt_client_publish(client, TOPIC, "data_3", 0, 1, 0);
            ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);

            msg_id = esp_mqtt_client_subscribe(client, TOPIC, 0);
            ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);

            msg_id = esp_mqtt_client_subscribe(client, TOPIC, 1);
            ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);

            msg_id = esp_mqtt_client_unsubscribe(client, TOPIC);
            ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
            break;
        case MQTT_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
            break;

        case MQTT_EVENT_SUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
            msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
            ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
            break;
        case MQTT_EVENT_UNSUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
            break;
        case MQTT_EVENT_PUBLISHED:
            ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
            break;
        case MQTT_EVENT_DATA:
            ESP_LOGI(TAG, "MQTT_EVENT_DATA");
            printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
            printf("DATA=%.*s\r\n", event->data_len, event->data);
            break;
        case MQTT_EVENT_ERROR:
            ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
            break;
        default:
            ESP_LOGI(TAG, "Other event id:%d", event->event_id);
            break;
    }
    return ESP_OK;
}

static esp_err_t wifi_event_handler(void *ctx, system_event_t *event)
{
    switch (event->event_id) {
        case SYSTEM_EVENT_STA_START:
            esp_wifi_connect();
            break;
        case SYSTEM_EVENT_STA_GOT_IP:
            xEventGroupSetBits(wifi_event_group, CONNECTED_BIT);

            break;
        case SYSTEM_EVENT_STA_DISCONNECTED:
            esp_wifi_connect();
            xEventGroupClearBits(wifi_event_group, CONNECTED_BIT);
            break;
        default:
            break;
    }
    return ESP_OK;
}

static void wifi_init(void)
{
    tcpip_adapter_init();
    wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, NULL));
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = CONFIG_WIFI_SSID,
            .password = CONFIG_WIFI_PASSWORD,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    ESP_LOGI(TAG, "start the WIFI SSID:[%s]", CONFIG_WIFI_SSID);
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_LOGI(TAG, "Waiting for wifi");
    xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT, false, true, portMAX_DELAY);
}

static void mqtt_app_start(void)
{
    esp_mqtt_client_config_t mqtt_cfg = {
//        .uri = CONFIG_BROKER_URL,
	.uri = "mqtt://192.168.0.136",
        .event_handle = mqtt_event_handler,
        // .user_context = (void *)your_context
    };

    mqtt_cfg.uri = "mqtt://192.168.0.136";
    printf("URI = %s\n", mqtt_cfg.uri);
    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_start(client);
    xTaskCreate(user_task, "user_task", TASK_STACK_DEPTH, client, 3, NULL);
}

void app_main()
{
    ESP_LOGI(TAG, "[APP] Startup..");
    ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
    ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());

    esp_log_level_set("*", ESP_LOG_INFO);
    esp_log_level_set("MQTT_CLIENT", ESP_LOG_VERBOSE);
    esp_log_level_set("TRANSPORT_TCP", ESP_LOG_VERBOSE);
    esp_log_level_set("TRANSPORT_SSL", ESP_LOG_VERBOSE);
    esp_log_level_set("TRANSPORT", ESP_LOG_VERBOSE);
    esp_log_level_set("OUTBOX", ESP_LOG_VERBOSE);

    nvs_flash_init();
    wifi_init();
    vTaskDelay(2500 / portTICK_PERIOD_MS);
    startSNTP();
    mqtt_app_start();
}

Most of the work is done in the user_task. After configuring the UART the measurement rate of the sensors is set to 10 seconds, a change from the default of 60 seconds. The 10 second rate is needed to implement the forced calibration protocol which is performed after setting the pressure reference. Standard pressure under standard conditions of temperature at mean sea level is 1013 millibars, so this value was used. The documentation states that the default is 1015 millibars, so skipping this step would have little consequence. If deployment of this sensor were to include a pressure sensor, then one would regularly update the pressure reference.

Similar to the SCD30 CO2 sensor, this sensor does Automatic Baseline Offset Correction (ABOC) , which is a slow process that takes many days given that some of that time the sensor sees fresh air. The CO2 concentration in fresh air is very close to 400 PPM. The offset value is maintained in a pair of registers, CAL_REF_H and CAL_REF_L. However, if one needs to or wants to calibrate the offset after commissioning the sensor, there is the forced calibration protocol that is shown in the code above, which must be followed in order for the calibration to take effect. (see documentation Infineon-AN_FCS_ABOC_XENSIV_PASCO2_2-ApplicationNotes-v01_00-EN.pdf).

In the while loop every third measurement is used to create an MQTT message, thus sending data every 30 seconds. Together with current time the message is created, put into a buffer, and passed to the MQTT task via esp_mqtt_client_publish. The messages received by an MQTT subscriber are shown below:

CO2 in PPM = 386 ppm,  Tue Dec 21 11:40:45 2021
CO2 in PPM = 389 ppm,  Tue Dec 21 11:41:15 2021
CO2 in PPM = 388 ppm,  Tue Dec 21 11:41:44 2021
CO2 in PPM = 385 ppm,  Tue Dec 21 11:42:13 2021
CO2 in PPM = 382 ppm,  Tue Dec 21 11:42:43 2021
CO2 in PPM = 378 ppm,  Tue Dec 21 11:43:12 2021

Communication With Sensor Via UART

Communication (commands, reading, writing registers) is put into its own file, uart_PAS_CO2.c. External programs interact with the sensor only through those functions found in uart_PAS_CO2.h. This include file is shown below:

/*
uart_PAS_CO2.h
*/

/*num is an ASCII byte and returns the decoded integer byte*/
uint8_t asc_to_uint8( uint8_t num);

/*num is integer byte and returns the encoded ASCII byte*/
uint8_t uint8_to_asc( uint8_t num);

/*highbyte and lowbyte represent integer values.  Concatentaion of the two is then cast as an integer and returned*/
int concat( uint8_t highbyte, uint8_t lowbyte);

/*Returns a short integer value constructed of the four LSB positions of highbyte put into the four MSB positions 
   of lowbyte*/ 
uint8_t make_byte_value( uint8_t highbyte, uint8_t lowbyte);


/*All functions below are called as the primary interface to external users.
   *data is the buffer used to pass or return data*/
void read_sensor_status(uint8_t *data);

void read_meas_cfg(uint8_t *data);

void read_meas_status(uint8_t *data);

void set_meas_cfg(uint8_t *data, uint8_t config );

int read_PPM_CO2(uint8_t *data);//returns the PPM of CO2

void write_meas_rate(uint8_t *data , uint8_t value);//value is one byte value in seconds, 0x05 to 0xFF

void read_meas_rate(uint8_t *data);

void write_press_ref( uint8_t *data,  uint16_t value);//value is two byte value in hPa

void write_calib_ref( uint8_t *data, uint16_t value);//value is two byte value in PPM
  
void set_idle( uint8_t *data);//sensor idled

void set_continuous( uint8_t *data);//ABOC enabled and continous read

void set_forced( uint8_t *data);//Forced calibration is set and continuous read

int read_device_ready( uint8_t *data);//returns 1 if new data is ready

void set_sens_rst(uint8_t *data, uint8_t config );//soft reset-see register map

One should note that almost all details of the sensor registers are hidden from the user. The interface functions take care of the details as well as pass or receive values needed by the user. Now shown below is the code in uart_PAS_CO2.c.

/*
These functions provide complete communication via UART with the Infineon 
XENSIV PAS CO2 sensor.  The register map description is found in an 
Infineon application note with a related title.  Some considerations:

1.  All registers require ASCII bytes reading and writing.  E.G., unsigned short integer 0x0B --> 0x42(hex) or 66(decimal)

2.  For this purpose command, address, and data registers contain uint8_t variables, which are unsigned short integers (=bytes).

3   The read/write commands always have two bytes of data that together constitute a value of one byte.  The first data byte
    is an ascii byte that represents the high 4 bits of the value and the second data byte represents the low 4 bits of the value.
    E.G., 0x46 0x42 --> 0x0F 0x0B --> (0000 1111)  (0000 1011) --> (0000 1111)<<4 (0000 1011) --> (1111 0000) (0000 1011) -->
    (1111 1011) --> 0xFB = 251.  This process must be reversed when a byte value must be written to a register.

4.  The main task requires memory size to be increased to 2048:  xTaskCreate(echo_task, "uart_echo_task", 2048, NULL, 10, NULL);

5.  At the beginning the measurement configuration needs some changes from its default values:
    uint8_t meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_EN | MEAS_CFG_OPMODE_CONTINUOUS;
    (Grounding pin PWM_DIS is required but also required is setting MEAS_CFG_PWMOUT_EN to zero) 
6.   Default rate was measured to be 60 seconds.
     After changing the measure rate for it to take effect one needs to put sensor(meas_cfg) in IDLE mode, then 
     back to CONTINOUS mode.

7.  A small delay is needed sometimes between a write command and a read command on the same register.  Otherwise the read 
    command comes back with no bytes being read.

*/


#include <stdio.h>
#include <string.h>
#include "driver/uart.h"

#define BUF_SIZE (1024)

uint8_t data1;
uint8_t data2;
#define CMD_WRITE_RATEH 1
#define CMD_WRITE_RATEL 2
#define CMD_WRITE_CONFIG 3
#define CMD_WRITE_MEAS_STATUS 4
#define CMD_WRITE_INT_CFG 5
#define CMD_WRITE_ALARM_THRESH 6
#define CMD_WRITE_ALARM_THRESL 7
#define CMD_WRITE_PRESS_REFH 8
#define CMD_WRITE_PRESS_REFL 9
#define CMD_WRITE_CALIB_REFH 10
#define CMD_WRITE_CALIB_REFL 11
#define CMD_WRITE_SCRATCHPAD 12
#define CMD_WRITE_SENS_RST 13


uint8_t cmd_read_product_id[5] = { 0x52, 0x2C, 0x30, 0x30, 0x0A };
uint8_t cmd_read_sens_status[5] = { 0x52, 0x2C, 0x30, 0x31, 0x0A };
uint8_t cmd_read_rate_high[5] = { 0x52, 0x2C, 0x30, 0x32, 0x0A };
uint8_t cmd_read_rate_low[5] = { 0x52, 0x2C, 0x30, 0x33, 0x0A };
uint8_t cmd_read_config[5] = { 0x52, 0x2C, 0x30, 0x34, 0x0A };
uint8_t cmd_read_co2ppm_high[5] = { 0x52, 0x2C, 0x30, 0x35, 0x0A };
uint8_t cmd_read_co2ppm_low[5] = { 0x52, 0x2C, 0x30, 0x36, 0x0A };
uint8_t cmd_read_meas_status[5] = { 0x52, 0x2C, 0x30, 0x37, 0x0A };
uint8_t cmd_read_interrupt_cfg[5] = { 0x52, 0x2C, 0x30, 0x38, 0x0A };
uint8_t cmd_read_alarm_thresH[5] = { 0x52, 0x2C, 0x30, 0x39, 0x0A };
uint8_t cmd_read_alarm_thresL[5] = { 0x52, 0x2C, 0x30, 0x41, 0x0A };
uint8_t cmd_read_press_refH[5] = { 0x52, 0x2C, 0x30, 0x42, 0x0A };
uint8_t cmd_read_press_refL[5] = { 0x52, 0x2C, 0x30, 0x43, 0x0A };
uint8_t cmd_read_calib_refH[5] = { 0x52, 0x2C, 0x30, 0x44, 0x0A };
uint8_t cmd_read_calib_refL[5] = { 0x52, 0x2C, 0x30, 0x45, 0x0A };
uint8_t cmd_read_scratchpad[5] = { 0x52, 0x2C, 0x30, 0x46, 0x0A };

#define CMD_PROD (cmd_read_product_id)
#define CMD_STATUS (cmd_read_sens_status)
#define CMD_RATEH (cmd_read_rate_high)
#define CMD_RATEL (cmd_read_rate_low)
#define CMD_CFG (cmd_read_config)
#define CMD_CO2PPMH (cmd_read_co2ppm_high)
#define CMD_CO2PPML (cmd_read_co2ppm_low)
#define CMD_MEAS_STS (cmd_read_meas_status)
#define CMD_INT_CFG (cmd_read_interrupt_cfg)
#define CMD_ALARM_THH (cmd_read_alarm_thresH)
#define CMD_ALARM_THL (cmd_read_alarm_thresL)
#define CMD_PRES_REFH (cmd_read_press_refH)
#define CMD_PRES_REFL (cmd_read_press_refL)
#define CMD_CALIB_REFH (cmd_read_calib_refH)
#define CMD_CALIB_REFL (cmd_read_calib_refL)
#define CMD_SCRATCH (cmd_read_scratchpad)

#define MEAS_CFG_PWMOUT_EN (uint8_t) 0x20 //0010 0000 bit5=1 
#define MEAS_CFG_PWMOUT_DIS (uint8_t) 0x00 //0000 0000 bit5=0
#define MEAS_CFG_PWMMODE_SINGLE (uint8_t) 0x10 //0001 0000 bit4=1
#define MEAS_CFG_PWMMODE_PULSETRAIN (uint8_t) 0x00 //0000 0000 bit4=0
#define MEAS_CFG_BOC_EN (uint8_t) 0x04 //0000 0100 bit3:2=01
#define MEAS_CFG_BOC_DIS (uint8_t) 0x00 //0000 0000 bit3:2=00
#define MEAS_CFG_BOC_FORCED (uint8_t) 0x08 //0000 1000 bit3:2=10
#define MEAS_CFG_OPMODE_IDLE (uint8_t) 0x00 //0000 0000 bit1:0=00
#define MEAS_CFG_OPMODE_SINGLESHOT (uint8_t) 0x01 //0000 0001 bit1:0=01
#define MEAS_CFG_OPMODE_CONTINUOUS (uint8_t) 0x02 //0000 0010 bit1:=10

uint8_t asc_to_uint8( uint8_t num){
	uint8_t value = num;
	printf("value in hex and uint8_t:  %02x  %02u \n", value, value);
	if( ((uint8_t) 48 <= value) && (value <= (uint8_t) 57)){ value = (value - (uint8_t) 48);}
	if( ((uint8_t) 65 <= value) && (value <= (uint8_t) 70)){ value = (value - (uint8_t) 55);}
	return value;
}

uint8_t uint8_to_asc( uint8_t num){
	uint8_t value = num;
	if( value <= 0x09){ value = value+0x30;}else{ value = value+0x37;}
	return value;
}

int concat( uint8_t highbyte, uint8_t lowbyte){
	int combine = highbyte << 8 | lowbyte;
	return combine;
}

uint8_t make_byte_value( uint8_t highbyte, uint8_t lowbyte){
	uint8_t combine = (highbyte<<4) + ( lowbyte & 0x0F);
	return combine;
}

void command_string( uint8_t cmd, char *name ){
	
        switch (cmd) {
                case CMD_WRITE_RATEH:
                //do
		strcpy( name, "Write Rate High");
                break;
                case CMD_WRITE_RATEL:
                //do
		strcpy( name, "Write Rate Low");
                break;
                case CMD_WRITE_CONFIG:
                //do
		strcpy( name , "Write Meas Config");
                break;
                case CMD_WRITE_MEAS_STATUS:
                //do
		strcpy ( name , "Write Meas Status");
                break;
                case CMD_WRITE_INT_CFG:
                //do
		strcpy( name , "Write Interrupt Config");
                break;
                case CMD_WRITE_ALARM_THRESH:
                //do
		strcpy( name , "Write Alarm Thres High");
                break;
                case CMD_WRITE_ALARM_THRESL:
                //do
		strcpy ( name , "Write Alarm Thres Low");
                break;
                case CMD_WRITE_PRESS_REFH:
                //do
		strcpy( name , "Write Pressure Ref High");
                break;
                case CMD_WRITE_PRESS_REFL:
                //do
		strcpy( name , "Write Pressure Ref Low");
                break;
                case CMD_WRITE_CALIB_REFH:
                //do
		strcpy( name , "Write Calib Ref High");
                break;
                case CMD_WRITE_CALIB_REFL:
                //do
		strcpy( name , "Write Calib Ref Low");
                break;
                case CMD_WRITE_SCRATCHPAD:
                //do
		strcpy( name , "Write Scratchpad");
                break;
		case CMD_WRITE_SENS_RST:
		strcpy( name , "Write sensor reset");
		break;
                default :
                        ;
        }
}

int write_cmd_PAS_CO2(uint8_t *data, uint8_t  cmd, uint8_t val  ){
	uint8_t command[8];
	int i = 0;
//	char name[100];
	char *name = (char *) malloc(100);
	command[0] = 0x57; command[1] = 0x2C, command[4] = 0x2c; command[7] = 0x0A;
        uint8_t high = val;
        uint8_t low = val;
	command_string( cmd, (char*) name);
	printf("\n");
	printf("Writing to %s  with value: \n", name );
	free(name);
	high = high >> 4; printf("high = %02x ", high);
	low = low & (uint8_t) 0x0f; printf("low = %02x ", low);
	high = uint8_to_asc( high);
	low = uint8_to_asc(low);
	printf("ascii: %02x %02x ", (uint8_t) high,  (uint8_t) low);
	printf("\n");
	switch (cmd) {
		case CMD_WRITE_RATEH:
		//do
		command[2] = 0x30; command[3] = 0x32; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_RATEL:
		//do
		command[2] = 0x30; command[3] = 0x33; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_CONFIG:
		//do
		command[2] = 0x30; command[3] = 0x34; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_MEAS_STATUS:
		//do
		command[2] = 0x30; command[3] = 0x37; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_INT_CFG:
		//do
		command[2] = 0x30; command[3] = 0x38; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_ALARM_THRESH:
		//do
		command[2] = 0x30; command[3] = 0x39; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_ALARM_THRESL:
		//do
		command[2] = 0x30; command[3] = 0x41; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_PRESS_REFH:
		//do
		command[2] = 0x30; command[3] = 0x42; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_PRESS_REFL:
		//do
		command[2] = 0x30; command[3] = 0x43; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_CALIB_REFH:
		//do
		command[2] = 0x30; command[3] = 0x44; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_CALIB_REFL:
		//do
		command[2] = 0x30; command[3] = 0x45; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_SCRATCHPAD:
		//do
		command[2] = 0x30; command[3] = 0x46; command[5] = high; command[6] = low;
		break;
		case CMD_WRITE_SENS_RST:
		command[2] = 0x31; command[3] = 0x00; command[5] = high; command[6] = low;
		break;
		default :
			;
	}
	
	do {printf("%02x ", command[i++]);}while(i < 8); i=0; printf("\n");
	uart_write_bytes(UART_NUM_2, (const char *) command, (uint8_t) 8);
        int len = uart_read_bytes(UART_NUM_2, data, BUF_SIZE, 200 / portTICK_RATE_MS);
        printf("num bytes read after writing command: %d\n", len); 
        if (len > 0){
           do {printf("%02x ", data[i++]);}while(len-- > 1);
           i = 0; len = 0;
        }	
	printf("\n");
	return 1;
}

int read_cmd_PAS_CO2(uint8_t *data, uint8_t *cmd, int nbytes  ){
	int i = 0;
	printf("\n");
	printf("Read the register\n");
        uart_write_bytes(UART_NUM_2, (const char *) cmd, nbytes);
        int len = uart_read_bytes(UART_NUM_2, data, BUF_SIZE, 200 / portTICK_RATE_MS);
        printf("num bytes read %d\n", len); 
        if (len > 0){
           do {printf("%02x ", data[i++]);}while(len-- > 1);
           i = 0; len = 0;
        }
	printf("\n");
	return 1;
}

/*The functions below provide the UART interface to calling programs*/

void read_sensor_status(uint8_t *data){
        uint8_t statusH;
        uint8_t statusL;
	int i = 0;
	printf("\n");
	printf("READ SENSOR STATUS.................\n"); 
        read_cmd_PAS_CO2((uint8_t*) data,  (uint8_t*) CMD_STATUS, (int) 5  );
        statusH = data[0]; statusL = data[1];
	printf("status values\n");
        printf("statusH: %02x, statusL: %02x ", statusH, statusL);
	printf("\n");
	if(statusH & (1<<3)){printf("SEN_RDY\n");}else{printf("NOT_SEN_RDY\n");}
	if(statusH & (1<<2)){printf("PWM_DIS_ST is high\n");}else{printf("PWM_DIS_ST is low\n");}
	if(statusH & (1<<1)){printf("ORTMP error\n");}else{printf("ORTMP no error\n");}
	if(statusH & (1)){printf("ORVS error\n");}else{printf("ORVS no error\n");}
	if(statusL & (1<<3)){printf("ICCER invalid cmd\n");}else{printf("ICCER cmd OK\n");}
	if(statusL & (1<<2)){printf("ORTMP_CLR\n");}else{printf("ORTMP_CLR is 0\n");}
	if(statusL & (1<<1)){printf("ORVS_CLR\n");}else{printf("ORVS_CLR is 0\n");}
	if(statusL & (1)){printf("ICCER_CLR\n");}else{printf("ICCER_CLR is 0\n");}
	//clear any error bits
	printf("clear any errors\n");
	uint8_t cmd[8] = { 0x57, 0x2C, 0x30, 0x31, 0x2C, 0x30, 0x37, 0x0A};
	uart_write_bytes(UART_NUM_2, (const char *) cmd, 8);
        int len = uart_read_bytes(UART_NUM_2, data, BUF_SIZE, 200 / portTICK_RATE_MS);
        printf("num bytes read after writing command: %d\n", len); 
        if (len > 0){
           do {printf("%02x ", data[i++]);}while(len-- > 1);
           i = 0; len = 0;
        }       
	printf("\n");  
}

int get_meas_cfg(uint8_t *data){
        uint8_t cfgH;
        uint8_t cfgL;
        printf("\n");
        printf("READ MEAS CFG..................\n");
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_CFG, (int) 5);
        printf("meas cfg values\n");
        cfgH = data[0]; cfgL = data[1];
	return make_byte_value( cfgH, cfgL);
}

void read_meas_cfg(uint8_t *data){
	uint8_t cfgH;
	uint8_t cfgL;
	printf("\n");
	printf("READ MEAS CFG..................\n");
	read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_CFG, (int) 5);
	printf("meas cfg values\n");
	cfgH = data[0]; cfgL = data[1];
	printf("cfgH: %02x, cfgL: %02x ", cfgH, cfgL); printf("\n");
	if(cfgH & (1<<1)){printf("PWM_OUTEN: PWM out is enabled\n");}else{printf("PWM_OUTEN: PWM out is disabled\n");}
	if(cfgH & (1)){printf("PWM_MODE: PWM mode is single pulse\n");}else{printf("PWM_MODE: PWM mode is pulse train\n");}
	uint8_t boc_cfg = cfgL; uint8_t op_mode = cfgL;
	boc_cfg = boc_cfg & 0x0D; //extract bits 2 and 3
	op_mode = op_mode & 0x03; //extract bits 0 and 1
	if(boc_cfg == 0x00){printf("BOC_CFG: auto baseline offset comp (ABOC) is disabled\n");}
	if(boc_cfg == 0x04){printf("BOC_CFG: ABOC enabled\n");}//0000 0100
	if(boc_cfg == 0x08){printf("BOC_CFG: Forced compensation\n");}//0000 1000
        if(op_mode == 0x00){printf("OP_MODE: Idle mode\n");}
        if(op_mode == 0x01){printf("OP_MODE: Single shot mode\n");}//0000 0001
        if(op_mode == 0x02){printf("OP_MODE: Continuous mode enabled\n");}//0000 0010

}

void read_meas_status(uint8_t *data){
        uint8_t meas_stsH;
        uint8_t meas_stsL;
	printf("\n");
	printf("READ MEAS STATUS.....................\n");
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_MEAS_STS, (int) 5);
        printf("meas status values\n");
        meas_stsH = data[0]; meas_stsL= data[1];
        printf("meas_stsH: %02x, meas_stsL: %02x ", meas_stsH, meas_stsL); printf("\n");
        if( data[0] & 0x01 ){printf("new data ready...........................................\n");}else{printf("no new data\n");}
        if( data[1] & 0x08 ){printf("INT pin sts bit: INT has been latched\n");}else{printf("INT pin sts bit: INT not latched\n");}
        if( data[1] & 0x04 ){printf("Alarm violation\n");}else{printf("No alarm violation\n");}
}

void set_meas_cfg(uint8_t *data, uint8_t config ){
//	uint8_t present_config = get_meas_cfg( (uint8_t*) data);
	write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_CONFIG, (uint8_t) config);

}

int read_PPM_CO2(uint8_t *data){
	int i=0;
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_CO2PPMH, (int) 5);
        printf("CO2_PPMH: high=%02x  low=%02x\n", asc_to_uint8( data[0]), asc_to_uint8( data[1]));
	uint8_t  highbyte = ((asc_to_uint8( data[0])<<4) + asc_to_uint8(data[1]));
        printf("CO2_PPMH: high byte = %02x \n", highbyte);
        for (i=0; i<8; i++){ data[i] = 0x00;}
        printf("\nRead CO2_PPML \n");
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_CO2PPML, (int) 5);
        printf("CO2_PPML: high=%02x  low=%02x\n", asc_to_uint8( data[0]), asc_to_uint8( data[1]));
        uint8_t  lowbyte = (asc_to_uint8( data[0])<<4) + asc_to_uint8(data[1]);
        printf("CO2_PPML: low byte = %02x \n", lowbyte);
        for (i=0; i<8; i++){ data[i] = 0x00;}
        printf("RESULT>>>>>>>>>>>>PPM_CO2:  %02d \n", concat( highbyte, lowbyte));
	return concat( highbyte, lowbyte);
}

//Invoking this function always disables the PWM output besides setting measure rate
void write_meas_rate(uint8_t *data , uint8_t value){
    int i=0;
    uint8_t meas_config;
    write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) /*CMD_WRITE_SCRATCHPAD*/ CMD_WRITE_RATEL, (uint8_t) value);
    for (i=0; i<8; i++){ data[i] = 0x00;}
//  to do
    vTaskDelay(40 / portTICK_PERIOD_MS);
    meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_EN | MEAS_CFG_OPMODE_IDLE;
    set_meas_cfg((uint8_t*) data, (uint8_t) meas_config);
    for (i=0; i<8; i++){ data[i] = 0x00;}
    vTaskDelay(40 / portTICK_PERIOD_MS);
    read_meas_cfg((uint8_t*)data);
    for (i=0; i<8; i++){ data[i] = 0x00;}
    vTaskDelay(40 / portTICK_PERIOD_MS);
    meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_EN | MEAS_CFG_OPMODE_CONTINUOUS; // MEAS_CFG_OPMODE_IDLE;
    set_meas_cfg((uint8_t*) data, (uint8_t) meas_config);
    for (i=0; i<8; i++){ data[i] = 0x00;}
    vTaskDelay(40 / portTICK_PERIOD_MS);
    read_meas_cfg((uint8_t*)data);
    for (i=0; i<8; i++){ data[i] = 0x00;}
}

void read_meas_rate(uint8_t *data){
        int i=0;
        printf("\nRead Meas Rate High \n");
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_RATEH, (int) 5);
	uint8_t high = asc_to_uint8(data[0]);
	uint8_t low = asc_to_uint8(data[1]);
	uint8_t highbyte = make_byte_value(high,low);
        for (i=0; i<8; i++){ data[i] = 0x00;}
        printf("\nRead Meas Rate Low \n");
        read_cmd_PAS_CO2((uint8_t*) data, (uint8_t*) CMD_RATEL, (int) 5);
        high = asc_to_uint8(data[0]);
        low = asc_to_uint8(data[1]);
        uint8_t lowbyte = make_byte_value(high,low);
	printf("RESULT>>>>>>>>>>>>meas rate in seconds: %d \n", (int) concat(highbyte, lowbyte));
        for (i=0; i<8; i++){ data[i] = 0x00;}   
	
}
void write_press_ref( uint8_t *data,  uint16_t value){
	int i=0;
	uint8_t highval = (uint8_t) (value>>8);
	uint8_t lowval = (uint8_t) (value & 0x00FF);
        write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_PRESS_REFH, highval);
	vTaskDelay(20 / portTICK_PERIOD_MS);
        read_cmd_PAS_CO2((uint8_t*) data,  (uint8_t*) CMD_PRES_REFH, (int) 5  );
	vTaskDelay(20 / portTICK_PERIOD_MS);
        write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_PRESS_REFL, lowval);
	vTaskDelay(20 / portTICK_PERIOD_MS);
        read_cmd_PAS_CO2((uint8_t*) data,  (uint8_t*) CMD_PRES_REFL, (int) 5  );
        printf("RESULT>>>>>>>>>>>>pressure ref in hPa: %d \n", (int) concat(highval, lowval));
	for (i=0; i<8; i++){ data[i] = 0x00;}
}

void write_calib_ref( uint8_t *data, uint16_t value){
        int i=0;
        uint8_t highval = (uint8_t) (value>>8);
        uint8_t lowval = (uint8_t) (value & 0x00FF);
        write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_CALIB_REFH, highval);
        vTaskDelay(20 / portTICK_PERIOD_MS);
        read_cmd_PAS_CO2((uint8_t*) data,  (uint8_t*) CMD_CALIB_REFH, (int) 5  );
        vTaskDelay(20 / portTICK_PERIOD_MS);
        write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_CALIB_REFL, lowval);
        vTaskDelay(20 / portTICK_PERIOD_MS);
        read_cmd_PAS_CO2((uint8_t*) data,  (uint8_t*) CMD_CALIB_REFL, (int) 5  );
		printf("RESULT>>>>>>>>>>>>calib ref in ppm: %d \n", (int) concat(highval, lowval));
        for (i=0; i<8; i++){ data[i] = 0x00;}
}

void set_idle( uint8_t *data){
    uint8_t    meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_EN | MEAS_CFG_OPMODE_IDLE;
    set_meas_cfg((uint8_t*) data, (uint8_t) meas_config);
    read_meas_cfg( data);
}

void set_continuous( uint8_t *data){
    uint8_t    meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_EN | MEAS_CFG_OPMODE_CONTINUOUS;
    set_meas_cfg((uint8_t*) data, (uint8_t) meas_config);
    read_meas_cfg( data);
}

void set_forced( uint8_t *data){
    uint8_t    meas_config = MEAS_CFG_PWMOUT_DIS | MEAS_CFG_BOC_FORCED | MEAS_CFG_OPMODE_CONTINUOUS;
    set_meas_cfg((uint8_t*) data, (uint8_t) meas_config);
    read_meas_cfg( data);
}

int read_device_ready( uint8_t *data){
    int ready;
    read_meas_status( data);
    if( data[0] & 0x01 ){ready = 1;}else{ready = 0;}
    return ready;
}

void set_sens_rst(uint8_t *data, uint8_t config ){
    write_cmd_PAS_CO2((uint8_t*) data, (uint8_t) CMD_WRITE_SENS_RST, (uint8_t) config);
}

The notes at the beginning are salient points. The whole communication protocol is made a bit more complicated given that all bytes are ASCII encoded and all register byte addresses and all data bytes must be ASCII encoded into two bytes, one ASCII byte representing the high nibble (MSB 4 bits) and one ASCII byte representing the low nibble (LSB 4 bits).

Notes 5 and 6 denote some idiosyncracies involved with this sensor. Beside grounding pin PWM_DIS one additionally needs to set the bit PWMOUT_EN to zero in the MEAS_CFG register. This is done at the beginning with the write_meas_rate() function. Also for the new measure rate to take effect (default is 60 seconds), in the MEAS_CFG register Idle mode must be set and then Continuous mode must be set. Otherwise the old measurement rate continues. The documentation refers to register bits such as in this case as being “sticky”!

Lastly, the functions print lots of information when executed. I find this very useful to help debugging.

Conclusion

Next step is to fashion the elements of the breadboard into a completed assembly. Then, weather permitting (no rain), some outdoor testing needs to be done and to see how the ABOC function works.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *