A challenge on Bit mapping

I have a CAN message as below - it contains the status of 5 solenoids in Byte 0 and Byte 1. As I have shown a solenoid that is ON is represented by a bit combination of 01. 00 is OFF. 10 or 11 is fault. So I just have to check for 01 or 00 and then if both are not matching declare fault.

0x18FF0308 = 55 01 00 00 00 00 00 00

First swap Byte 0 and Byte 1 and then form a 16 bit integer.

0x0155 = 00000001 01010101 ( All Solenoids ON )

One way is to do is a bit wise mask then AND for each solenoid and then decide . For instance for Solenoid 1 the mask will be 0x0001 and for Solenoid 2 it will be 0x0004 and so on.

Any other simple way ??

Is there an actual question in the above word salad?

masking the bits as you indicate seems to be the simplest way
as the CAN message is 8 bytes why not transmit the solenoid states in 5 separate bytes?

a bit field array

struct RELAYS
{
uint16_t RELAY0:  2;
uint16_t RELAY1:  2;
…
uint16_t RELAY4:  2;
};

Yes would have been way simpler to do that … but the balance 6 bytes are reserved for some other info to be sent in future. So all this circus.

Instead of applying a six separate bitmasks you could do it in a loop. But the way depends on the purpose. How do you plan to use the solenoid states after extraction them from the message?

in which format do you store your CAN payload ( 55 01 00 00 00 00 00 00) ? is that in a byte array ?

if it's in an array, let's says a 8 byte array, then you could write a function

// Returns a byte where:
// bits 0-4 : solenoid ON/OFF (1 = ON, 0 = OFF)
// bits 5-7 : number of solenoids in fault
uint8_t getSolenoidStatus(const uint8_t* payload) {
  uint16_t val = (payload[1] << 8) | payload[0];
  uint8_t result = 0;
  uint8_t faultCount = 0;

  for (int i = 0; i < 5; i++) {
    uint8_t status = (val >> (i * 2)) & 0x03;
    if (status == 0x01)
      bitSet(result, i);
    else if (status != 0x00)
      faultCount++;
  }

  result |= (faultCount << 5);
  return result;
}

OK - using a loop

void setup() {
  Serial.begin(115200);
  delay(2000);
  //byte data[]={0x55, 0x01, 0, 0, 0, 0, 0, 0};
  byte data[]={0x4B, 0x01, 0, 0, 0, 0, 0, 0};   // 0x4B = 01001011
  for(int i=0;i<4;i++) {
    byte test = (data[0] >> (i*2))& 0x3;  // select 2 bits to test
    switch(test) {
      case 0: Serial.printf("switch %d, test %0x OFF\n", i, test); break;
      case 1: Serial.printf("switch %d, test %0x ON\n", 0, test); break;
      default: Serial.printf("switch %d, test %0x ERROR\n", i, test); break;    
    }
  }
}

void loop() {}

on an ESP32 displays

switch 0, test 3 ERROR
switch 1, test 2 ERROR
switch 2, test 0 OFF
switch 0, test 1 ON

i like the power of bit fields and structures to handle packetized data


```cpp
struct RELAYS {
  uint16_t RELAY0 : 2;
  uint16_t RELAY1 : 2;
  uint16_t RELAY2 : 2;
  uint16_t RELAY3 : 2;
  uint16_t RELAY4 : 2;
};

struct PACKET {
  RELAYS RELAY;
  byte STUFF[6];
};

PACKET pkt;

void setup() {
  Serial.begin(115200);
  //byte data[]={0x55, 0x01, 0, 0, 0, 0, 0, 0};
  byte data[] = { 0x54, 0x01, 11, 22, 33, 44, 77, 66 };
  memcpy((uint8_t*)&pkt, data, 8);
  Serial.println();
  Serial.printf("\nRelay-0\t%d\tStuff-0\t[%d]", pkt.RELAY.RELAY0, pkt.STUFF[0]);
  Serial.printf("\nRelay-1\t%d", pkt.RELAY.RELAY1);
  Serial.printf("\nRelay-2\t%d", pkt.RELAY.RELAY2);
  Serial.printf("\nRelay-3\t%d", pkt.RELAY.RELAY3);
  Serial.printf("\nRelay-4\t%d\tStuff-4\t[%d]", pkt.RELAY.RELAY4, pkt.STUFF[4]);
  Serial.printf("\n\t\tStuff-5\t[%d]", pkt.STUFF[5]);
}

void loop() {
  // put your main code here, to run repeatedly:
}
ESP-ROM:esp32c3-api1-20210207

Relay-0 0 Stuff-0 [11]

Relay-1 1

Relay-2 1

Relay-3 1

Relay-4 1 Stuff-4 [77]

Stuff-5 [66]

Did you mean union ?

(With the associated challenges in C++)

no , its a struc

Ok

Still, you are in undefined behavior territory according to the C++ standard. Bitfield layout and padding are implementation-defined, meaning the compiler decides how bits are arranged in memory, and this arrangement can vary across platforms or even compiler versions.

That being said, in practice, most compilers such as GCC and Clang on little-endian architectures produce the deterministic results you showed, so the code appears to work, but it is not portable and not guaranteed by the language.

i can see that being a major issue when the single specific implementation the code has been designed for, tested and proved to work on suddenly changes its platform and recompiles itself without user intervention.

i have never found a platform that I could not got this method to work on[eventually]

might need a bit of padding, attribute packing or order juggling , endianness conversion if one must.

I guess for hobbyists it’s a personal decision. I know many companies where falling into Undefined Behavior is a no no.

Personally I’d rather stay « clean » when it’s easy to do so. The simple for loop I posted earlier is an example of how to extract some information from the payload easily.

but it comes into its own if the data is modified and sent on

struct RELAYS {
  uint16_t RELAY0 : 2;
  uint16_t RELAY1 : 2;
  uint16_t RELAY2 : 2;
  uint16_t RELAY3 : 2;
  uint16_t RELAY4 : 2;
};
//__attribute__ ((packed))
struct PACKET {
  RELAYS RELAY;
  byte STUFF[6];
};

PACKET pkt;

void setup() {
  Serial.begin(115200);
  //byte data[]={0x55, 0x01, 0, 0, 0, 0, 0, 0};
  byte data[] = { 0x54, 0x01, 11, 22, 33, 44, 77, 66 };
  memcpy((uint8_t*)&pkt, data, 8);
  Serial.println("ready");
  Serial.printf("\nRelay-0\t%d\tStuff-0\t[%d]", pkt.RELAY.RELAY0, pkt.STUFF[0]);
  Serial.printf("\nRelay-1\t%d", pkt.RELAY.RELAY1);
  Serial.printf("\nRelay-2\t%d", pkt.RELAY.RELAY2);
  Serial.printf("\nRelay-3\t%d", pkt.RELAY.RELAY3);
  Serial.printf("\nRelay-4\t%d\tStuff-4\t[%d]", pkt.RELAY.RELAY4, pkt.STUFF[4]);
  Serial.printf("\n\t\tStuff-5\t[%d]", pkt.STUFF[5]);
  pkt.RELAY.RELAY0 = 3;
  pkt.STUFF[5] = 77;
  pkt.STUFF[4] = 66;
  memcpy(data, (uint8_t*)&pkt, 8);
  Serial.printf("\ndata[0x%02x,0x%02x,%d,%d,%d,%d,%d,%d]", data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
}

void loop() {
  // put your main code here, to run repeatedly:
}
ESP-ROM:esp32c3-api1-20210207
ready

Relay-0	0	Stuff-0	[11]
Relay-1	1
Relay-2	1
Relay-3	1
Relay-4	1	Stuff-4	[77]
		Stuff-5	[66]
data[0x57,0x01,11,22,33,44,66,77]

I don't understand what that means ...

i mean the data can be easily created or modified using easy to understand meaningful names ready to be transmitted or retransmitted
eg

struct RELAYS {
  uint16_t heater_fan : 2;
  uint16_t demister: 2;
  uint16_t washer : 2;
  uint16_t high_beam: 2;
  uint16_t reverse_lamp : 2;
};

I don't understand...

basically my function

// Returns a byte where:
// bits 0-4 : solenoid ON/OFF (1 = ON, 0 = OFF)
// bits 5-7 : number of solenoids in fault
uint8_t getSolenoidStatus(const uint8_t* payload) {
  uint16_t val = (payload[1] << 8) | payload[0];
  uint8_t result = 0;
  uint8_t faultCount = 0;

  for (int i = 0; i < 5; i++) {
    uint8_t status = (val >> (i * 2)) & 0x03;
    if (status == 0x01)
      bitSet(result, i);
    else if (status != 0x00)
      faultCount++;
  }

  result |= (faultCount << 5);
  return result;
}

does conceptually what you do here

I provide a result (as you do in pkt) which you can use then to extract and print information from. The information I extracted is the status — ON / OFF of each solenoid and the count of faulty states (so that if that count is not 0, you know you can't trust the status bits but you can't tell in one byte which one was wrong)

so if you get a payload from CAN, you pass it to the function and you keep the resulting bye as your system state.

indeed but you are assuming the data is only to be read. what if you want to create the data or modify it for transmission

i gave my last two posts more detail ,sorry

as an aside, I do this a lot. identical structures can exist on a 8bit pic chip and an esp32 for instance, if you are transferring data between them its so easy . the concept works for every c compiler I have come across xc8,xc16,xc32,gcc,mickroc, sourceboost etc. even python, node-red, javascript have similar methods too, it makes transfer of data between platforms simple in my view. if you know how the compiler allocates memory for your platform its not difficult