1
\$\begingroup\$

I have a MH-Z19 CO2 sensor, according the data-sheet I can obtain PPM values via UART and PWM. The difference between both method is the limit in the CO2 concentration, where UART I should have readings between 0-5000ppm, and via PWM I should have readings between 0-2000ppm.

I just wanted to know if I can have the same readings using both methods, but after tried I'm realized that I'm having different values.

I'm using the following code to test:

#include <SoftwareSerial.h>
SoftwareSerial mySerial(A0, A1); // RX, TX

byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
char response[9]; 

#define pwmPin 10
int prevVal = LOW;
long th, tl, h, l, ppm, ppm2 = 0.0;



void setup() {
    Serial.begin(9600); 
    mySerial.begin(9600); 
    pinMode(pwmPin, INPUT);
}

void loop(){
    mySerial.write(cmd,9);
    mySerial.readBytes(response, 9);
    int responseHigh = (int) response[2];
    int responseLow = (int) response[3];
    ppm = (256*responseHigh)+responseLow;


    //CO2 via pwm
    do {
        th = pulseIn(pwmPin, HIGH, 1004000) / 1000.0;
        tl = 1004 - th;
        ppm2 = 2000 * (th-2)/(th+tl-4);
    } while (ppm2 < 0.0);

    Serial.println(ppm);
    Serial.println(ppm2);
    Serial.println("-----------");
    delay(5000);
}

The readings that I'm getting are the foolowing:

643
356
-----------
643
356
-----------

Any idea what could be going on here? Should I suspect that the sensor is damaged? Or, I'm doing something wrong?

Thanks and best regards.

BTW: The test is done with an Arduino Pro Mini ATMega 328, 3.3v, 8Mhz. The sensor is being powered by 5v from a NodeMCU (lolin).

\$\endgroup\$
2
\$\begingroup\$
char response[9]; 

On many systems including an Arduino, an unspecified char is a signed type.

int responseHigh = (int) response[2];
int responseLow = (int) response[3];
ppm = (256*responseHigh)+responseLow;

Therefore when you cast it to an int, any value value with the highest bit set will be considered negative and sign extended to a negative value in the full integer width. So for example an 8-bit char of 0x80 becomes a 16-bit int 0xff81. This is probably not what you want, as it will actually reduce rather than increase the value set by the upper bits.

Instead, since your values are unsigned you should either declare your array as unsigned char or cast the values to an unsigned char before you implicitly or explicitly cast them to a wider type.

Note that even if the overall value were signed, you would probably still want to be interpreting the lower 8 bit as an unsigned modifier of the signed upper 8 bits.

However, it is not immediately apparent that this change will resolve the discrepancy in your readings - it may actually increase it.

th = pulseIn(pwmPin, HIGH, 1004000) / 1000.0;

There does not seem to be any sound reason to use a floating point constant when dividing an integer type to produce an integer result.

You should probably print out your PWM times and see if they make sense, compared to say scope measurements.

Also ideally you would have some other reference sensor you could use to determine which value is more credible.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the unsigned data-type tip. It was very helpful, now I think I figure out what happened. I will post it as an answer. \$\endgroup\$ – crosvera Oct 9 '16 at 21:37
1
\$\begingroup\$

First thanks to @ChrisStratton for his correction on data-types.

I have changed my code to the following:

#include <SoftwareSerial.h>
#define pwmPin 10
SoftwareSerial mySerial(A0, A1); // RX, TX

byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
unsigned char response[9]; 
unsigned long th, tl,ppm, ppm2, ppm3 = 0;



void setup() {
  Serial.begin(9600); 
  mySerial.begin(9600); 
  pinMode(pwmPin, INPUT);
}

void loop() {
  mySerial.write(cmd,9);
  mySerial.readBytes(response, 9);
  unsigned int responseHigh = (unsigned int) response[2];
  unsigned int responseLow = (unsigned int) response[3];
  ppm = (256*responseHigh)+responseLow;


  //CO2 via pwm
  do {
    th = pulseIn(pwmPin, HIGH, 1004000) / 1000;
    tl = 1004 - th;
    ppm2 = 2000 * (th-2)/(th+tl-4);
    ppm3 = 5000 * (th-2)/(th+tl-4);
  } while (th == 0);

  Serial.println(ppm);
  Serial.println(th);
  Serial.println(ppm2);
  Serial.println(ppm3);
  Serial.println("-----------");
  delay(5000);
}

Besides the change in the datatypes, I have added a second way to calculate the CO2 concentration using PWM (ppm3 in the code), now using 5000ppm as concentration limit, using this I'm getting readings very close to those I got from UART.

I'm getting the following readings:

459  <- ppm (UART)
93   <- Milliseconds UART is HIGH
182  <- ppm2 (PWM) with 2000ppm as limit
455  <- ppm3 (PWM) with 5000ppm as limit
-----------
460
93
182
455
-----------
460
93
182
455
-----------

It seems the datasheet, only reefers the use of PWM with 0-2000ppm range as an example, and does not exclude the 0-5000 range.

\$\endgroup\$
1
\$\begingroup\$

The UART readings at least make sense. Normal CO2 concentrations at open spaces should be around 400ppm, and a room may well be around 650ppm.

While calculating ppm2, since you defined tl=1004-th I think that (th+tl-4) will always be 1000, so you can use ppm2=2(th-2)

I'm quite sure that there are two versions of this sensor. One reading up to 2000ppm and the other up to 5000ppm. Also, I think there is a B version of the sensor that uses the 0x99 command to change the range.

It would be a good idea to calibrate the sensor. Do that in open air with nobody close.

Also, use the checksum to check for posible response errors.

Just my 2 cents.

\$\endgroup\$
1
\$\begingroup\$

The data sheet states, that timing accuracy is 5%. Modifying the code to measure the actual PWM interval yields exactly the same result as UART reading.

 void setup() {
  Serial.begin(9600); 
  mySerial.begin(9600); 
  pinMode(pwmPin, INPUT);
  th = pulseIn(pwmPin, HIGH, 3000000); // use microseconds
  tl = pulseIn(pwmPin, LOW, 3000000);
  tpwm = th + tl; // actual pulse width
  Serial.print("PWM-time: ");
  Serial.print(tpwm);
  Serial.println(" us");
  p1 = tpwm/502; // start pulse width
  p2 = tpwm/251; // start and end pulse width combined
}

and the calculation

  th = pulseIn(pwmPin, HIGH, 3000000);
  ppm3 = 5000 * (th-p1)/(tpwm-p2);

If You have a 0-2000 ppm version, multiply by 2000 instead of 5000. You could also measure pulse widths in every round, if the timing is variable over time.

\$\endgroup\$
-1
\$\begingroup\$

When you connect sensor to Arduino notice to connect TX of sensor with RX of Arduino and RX of sensor with TX of Arduino.

My 2 cents.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.