Reading co2 and voc values from SGP30 with buspirate?

I apologize if this basic for most of you, but understanding some of these things would be really helpful for me.
in reading the datasheet, i think i may need to be writing a checksum, and that is why it doesnt work:

My attempt was to use this:

I2C> [0xB0 0x2003] D:5 [0xB0 0x2008] D:11 [0xB1 r:2]

I2C START
TX: 0xB0 ACK 0x03 ACK
I2C STOP
Delay: 25ms
I2C START
TX: 0xB0 ACK 0x08 ACK
I2C STOP
Delay: 11ms
I2C START
TX: 0xB1 ACK
RX: 0x00 ACK (I2C timeout)0x00 NACK (I2C timeout)

The idea being that i send the sgp30_iaq_init , wait the proper time, and then send sgp30_measure_iaq and try to read back the first 2 bytes (before the checksum)

The scan looks like this
I2C> scan
I2C address search:
0x00 (0x00 W)
0x58 (0xB0 W) (0xB1 R)

Found 3 addresses, 1 W/R pairs.


The concerning part of the datasheet is “In write direction it is mandatory to transmit the checksum, since the SGP30 only accepts data if it is followed by the correct checksum”

any advice would be helpful to my learning.

You get ACKs, that’s good because you know the chip is listening. I suspect you’re onto something here.

Does the datasheet have example with precalculated checksums for standard commands? I’ve used other Sensiron chips and there is usually a table of precalculated checksums to ease situations like this.

1 Like

Unfortunately I don’t think so. It has this in regards to checksum calculation


and a section on measurement communication in general:

I am going to read the datasheet more closely after work, and if you have any ideas id love to hear them, but after reading that paragraph im beginning to think this isnt a measurement i can read easily such as your device demo examples for temp/humidity.

They have arduino libraries for reading the SGP30. i could probably check the code there as well

This would be a really interesting thing to add. There is already some code that does a very similar checksum, I don’t recall were offhand, but it’s in the code base.

A CRC calculator command would be easy. A new form of syntax like [ 0xb0 (0x55 0xaa:crc-method)] could also he handy, but would involve moderate changes to the syntax compiler.

I believe though you can find a calculator online to break through this current blockage. I’d google the formula.

1 Like

Didn’t find a calculator that seemed to do 0xBEEF = 0x92, but this for the sht30 (probably where I saw the precalculated values) shows how it’s done:

i think this is right

[relo@killengn sgp30]$ python3 sgp30_crc.py 0xBEEF
0x92
[relo@killengn sgp30]$ python3 sgp30_crc.py 0x2003
0xe
[relo@killengn sgp30]$ python3 sgp30_crc.py 0x2008
0xe4
[relo@killengn sgp30]$ cat sgp30_crc.py

import sys

def crc8(data: int) -> int:
    crc = 0xFF
    polynom = 0x31
    data_bytes = data.to_bytes(2, 'big')

    for byte in data_bytes:
        crc ^= byte
        for _ in range(8):
            if crc & 0x80:
                crc = ((crc << 1) ^ polynom) & 0xFF
            else:
                crc <<= 1
    crc ^= 0x00
    return crc

data = int(sys.argv[1], 16)

print(hex(crc8(data)))

maybe like you were saying, there could be CRC calculation functions in buspirate, that take the polynomial and other values given in datasheets

1 Like

It would be interesting. I just grab code when I need to do this, but it would be nice to dig in a bit and make something nice.

I don’t believe the RP2040 has a CRC engine. Some PICs and STM32s do. Maybe it could be done in the PIO? It would be nice to have a template for how to handle the config options.

2 Likes

i feel like this is definitely doable from buspirate with what we learned, but im clearly still missing something. this is the code for raw measurement from arduino lib:

boolean Adafruit_SGP30::IAQmeasureRaw(void) {
  uint8_t command[2];
  command[0] = 0x20;
  command[1] = 0x50;
  uint16_t reply[2];
  if (!readWordFromCommand(command, 2, 25, reply, 2))
    return false;
  rawEthanol = reply[1];
  rawH2 = reply[0];
  return true;
}

/*!
 *   @brief  Request baseline calibration values for both CO2 and TVOC IAQ
 *           calculations. Places results in parameter memory locaitons.
 *   @param  eco2_base
 *           A pointer to a uint16_t which we will save the calibration
 *           value to
 *   @param  tvoc_base
 *           A pointer to a uint16_t which we will save the calibration value to
 *   @return True if command completed successfully, false if something went
 *           wrong!
 */

Should i be writing each byte individually with the CRC afterwards? with small delay in between? i even tried with the iaqinit in the arduino code

I2C> [0xB0 0x36] D:5 [0xB0 0x82] D:5 [0xB0 0x71] D:5 [0xB0 0x20] D:5 [0xb0 0x50] D:5 [0xB0 0x23] D:23 [0xB1 r:6]

I2C START
TX: 0xB0 ACK 0x36 ACK
I2C STOP
Delay: 5ms
I2C START
TX: 0xB0 ACK 0x82 ACK
I2C STOP
Delay: 5ms
I2C START
TX: 0xB0 ACK 0x71 ACK
I2C STOP
Delay: 5ms
I2C START
TX: 0xB0 ACK 0x20 ACK
I2C STOP
Delay: 5ms
I2C START
TX: 0xB0 ACK 0x50 ACK
I2C STOP
Delay: 5ms
I2C START
TX: 0xB0 ACK 0x23 ACK
I2C STOP
Delay: 23ms
I2C START
TX: 0xB1 ACK
RX: 0x00 ACK 0x00 ACK 0x81 ACK 0xFF ACK 0xFF ACK 0xFF NACK
I2C STOP

but i always get
RX: 0x00 ACK 0x00 ACK 0x81 ACK 0xFF ACK 0xFF ACK 0xFF NACK

maybe this is notable? its saying it wants and ACK after every byte from the microcontroller?

Each byte must be acknowledged by the microcontroller with an ACK condition for the sensor to
continue sending data. If the sensor does not receive an ACK from the master after any byte of data, it will not continue sending
data.

i know you have better things to do. maybe someone else will see the thread and will know off-hand. might make a good example of doing a slightly more complicated sensor reading

1 Like

I don’t think the CRC is needed for commands.

Ok. I see a few things now.

I2C> [0xB0 0x2003] D:5 [0xB0 0x2008] D:11 [0xB1 r:2]

I2C START
TX: 0xB0 ACK 0x03 ACK <--missing 0x20
I2C STOP

The 0x2003 is skipping bytes because the SPI peripheral is in 8 bit mode and the underlying libraries aren’t handling multiple bytes. Separate that out.

[0xB0 0x20 0x03] D:20 [0xB0 0x20 0x08] D:20 [0xB1 r:3]

Try this.

  • 16 bit 0x20xx values split into 8 bit values
  • Increased delay a bit over what is in the table above
  • Read all three bytes, including CRC to ensure proper end of read operation
1 Like

dude! check out what i missed>

The sensor responds with 2 data bytes (MSB first) and 1 CRC byte for each of the two preprocessed
air quality signals in the order CO2eq (ppm) and TVOC (ppb). For the first 15s after the “sgp30_iaq_init” command the sensor is
in an initialization phase during which a “sgp30_measure_iaq” command returns fixed values of 400 ppm CO2eq and 0 ppb
TVOC.

so we cant send the sgp30_iaq_init before every measurement command, or it just resets the 15s of giving that static value. thats why we were seeing those same values over and over.

If i send the init and then wait

I2C> [0xB0 0x20 0x03 0x0E] D:1000

I2C START
TX: 0xB0 ACK 0x20 ACK 0x03 ACK 0x0E ACK
I2C STOP
Delay: 1000ms

I see the mA’s start to fluctuate but still get :slight_smile:

I2C> [0xB0 0x20 0x08 0xE4] D:23 [0xB1 r:6]

I2C START
TX: 0xB0 ACK 0x20 ACK 0x08 ACK 0xE4 ACK
I2C STOP
Delay: 23ms
I2C START
TX: 0xB1 ACK
RX: 0x01 ACK 0x90 ACK 0x4C ACK 0x00 ACK 0x00 ACK 0x81 NACK
I2C STOP

which would be what the datasheet said about the 400ppm static value.
so

[relo@killengn sgp30]$ echo $(( (0x01 << 8) | 0x90 ))
400

and other is obviously 0. checksums are correct as well.
But then if i wait >15s check this out!

without alcohol swab near it

I2C> [0xB0 0x20 0x08 0xE4] D:23 [0xB1 r:6]

I2C START
TX: 0xB0 ACK 0x20 ACK 0x08 ACK 0xE4 ACK
I2C STOP
Delay: 23ms
I2C START
TX: 0xB1 ACK
RX: **0x01 ACK 0xCE ACK** 0x2D ACK 0x00 ACK 0x00 ACK 0x81 NACK
I2C STOP

which is

[relo@killengn sgp30]$ echo $(( (0x01 << 8) | 0xCE ))
462
^co2

and 0 for TVOC ( ACK 0x00 ACK 0x00 ACK 0x81 ) 0x81 being the CRC for 0x00.

[relo@killengn sgp30]$ python3 sgp30_crc.py 0x00                                0x81

now with soaked alcohol swabsuper close to the hot plate MOX sensor

I2C> [0xB0 0x20 0x08 0xE4] D:23 [0xB1 r:6]

I2C START
TX: 0xB0 ACK 0x20 ACK 0x08 ACK 0xE4 ACK
I2C STOP
Delay: 23ms
I2C START
TX: 0xB1 ACK
RX: 0xDF ACK 0xF2 ACK **0x3C ACK 0xEA ACK** 0x60 ACK 0xA2 NACK
I2C STOP
I2C>

which just so happens to be the friggin max reading for 60000ppb for the TVOC sensor

[relo@killengn sgp30]$ echo $(( (0xEA << 8) | 0x60 ))
60000

thank you so much. this was a great learning experience.

couple of bonus questions about buspirate cli i thought of when doing this
1.) is there a way to turn off verbosity of the transmission side, and only get the read? not super important but when running it tons of times when i might only want to show the command and what im trying to read, for copy and paste purposes.
2.) how can i print out values such as the mA. it shows it super pretty on the buspirate and in the CLI of course, but was just wondering if i could simply echo the current value of stuff
3.) can i do arithmetic and bitwise arithmetic in BP? like how i was using echo or printf in bash

thanks so much ian

  1. You can use v.1 etc to print voltage, but I had not considered current. I will add that the next time in in the syntax compiler.

  2. Yeah, absolutely . I’ve always wanted to, and with the new command system it’s possible. I can attach it to the = convert base command. Getting the order of operations and stuff correct might be a challenge. Maybe there’s some code out there we can use.

1 Like

Excellent! I’m so glad it’s working. That demo as proof is great!

1 Like

im getting a bit out of my league with this suggestion, but how i could imagine it

1.) First implement command line substitution/expansion in BP cli. IE everything in curly brackets is evaluated first, and its output inserted into the args. this could open up tons of future commands having novel use
2.) find a good starting point for math expressions including bitwise ops, and implement that as it’s own command called “cal” or something

then you could use that command inside of the brackets to include conversions and expressions on the fly within a command

1 Like

I imagine there’s a “right” (meaning doesn’t generate a ton of spaghetti code and variables all over) way to do order of operations. Both evaluating ()()(()) and PEMDAS. My gut suggests recursion and a sort of tree situation, but this is not something I have a ton of experience with.

If some simple math like 0x55|=8 or 0x5>>8 will suffice, that could be a workable starting point. Maybe someone who knows the proper solution will come along and help me work it out.

I think we may be able to use ideas/code from this: ccalc/src/evaluate.c at master · gkikola/ccalc · GitHub

So it has this get_token() and peek_token(). peek_token returns the type of the current token without consuming it. and then it has functions for each type of expression that enforce order of operations. its a little over my head but it doesnt use any libs buspirate firmware doesnt already use.

Each parse_* function runs one after the other, in order of correct order of operations. finding each occurrence of the operator
for example here is the multiplicative:

void parse_multiplicative_expression(parser *parse, value *result) {
  value left, right;

  parse_unary_expression(parse, result);
  left = *result;

  bool done = false;
  while (!done) {
    switch (peek_token(parse)) {
    default:
      done = true;
      break;

    case TOKEN_OP_TIMES:
      get_token(parse);
      parse_unary_expression(parse, &right);
      multiply(&left, &right, result);
      
      left = *result;
      break;

    case TOKEN_OP_DIVIDE:
      get_token(parse);
      parse_unary_expression(parse, &right);
      divide(&left, &right, result);
      
      left = *result;
      break;

    case TOKEN_OP_IDIVIDE:
      get_token(parse);
      parse_unary_expression(parse, &right);
      int_divide(&left, &right, result);

      left = *result;
      break;

    case TOKEN_OP_MOD:
      get_token(parse);
      parse_unary_expression(parse, &right);
      modulo(&left, &right, result);
      
      left = *result;
      break;
    }
  }
}

and then i think the last is parse_primary which handles the most basic units of expression such as variables/functions/sub expressions

void parse_primary(parser *parse, value *result) {
  get_token(parse);

  switch (parse->cur_token) {
  default:
    raise_error(ERROR_EXPR, "unexpected token '%s'", parse->str_value);
    break;

  case TOKEN_END:
    raise_error(ERROR_EXPR, "unexpected end of input");
    break;
    
  case TOKEN_IDENTIFIER:
    //is this a function or a constant?
    if (peek_token(parse) == TOKEN_LEFT_PAREN) {      
      int argc = 0;
      value argv[MAX_ARGUMENTS];
      char id[MAX_IDENTIFIER_LENGTH + 1];

      strncpy(id, parse->str_value, MAX_IDENTIFIER_LENGTH + 1);

      //eat parenthesis
      get_token(parse);

      //is there an argument list?
      if (peek_token(parse) == TOKEN_RIGHT_PAREN) {
	argc = 0;
      } else {
	bool done = false;
	while (!done) {
	  if (argc >= MAX_ARGUMENTS)
            raise_error(ERROR_EXPR, "too many arguments to function '%s'", id);
	  
          parse_conditional_expression(parse, &argv[argc]);
	  argc++;

	  switch (peek_token(parse)) {
          default:
	    done = true;
	    break;
          case TOKEN_OP_COMMA:
	    get_token(parse);
	    break;
	  }
	}
      }

      //read closing parenthesis
      get_token(parse);

      if (parse->cur_token != TOKEN_RIGHT_PAREN)
	raise_error(ERROR_EXPR, "unmatched parenthesis '('");

      //call the function
      call_function(id, result, argc, argv, parse->program_opts->degrees);
    } else {    
      get_constant(parse->str_value, result);
    }
    break;

  case TOKEN_LITERAL:
    *result = parse->numeric_value;
    break;

  case TOKEN_LEFT_PAREN:
    parse_expression(parse, result);

    //closing parenthesis should be next
    get_token(parse);

    if (parse->cur_token != TOKEN_RIGHT_PAREN)
      raise_error(ERROR_EXPR, "unmatched parenthesis '('");
    
    break;
  }
}
2 Likes

Good find! We use a similar get and peek system for data coming into the Bus Pirate, so this could literally be dropped into the firmware (if it wasn’t GPL).

This is a calculator without bitwise operators. It looks like it wouldn’t be hard to add. MIT license.

The real trick is the malloc and realloc, and how many may need to be safe for recursion :grimacing:

1 Like

Tell me if i am crazy here. I was checking out the btmills code and pondering the trig functions. would it be possible to make a DAC (digital to analog converter) add-on for buspirate, and do legit analog signal generation with DIO and math.

i have a eurorack modular synth. imagine if you could send CV with the buspirate. maybe just a novelty, but the general concept of more complex analog signal generation would be a huge feature.

I installed the RPi pico SDK and will try to start working on my C

Somewhere around here there’s a thread about cheap chinese made ADCs and DAC clones of the “go to” high speed Analog Inc (?) versions. DAC to 100MSPS or something. I have a schematic, but no board to test yet.

The issue I ran into is that we only have 8 IO pins, and one will be needed for the clock, so max 7 bits resolution.