2

I am trying to make write a code for i2c communication with a device (ADS1115). The communication uses different arrays of bits for commands and sending data back, in which most bits or group of bits have different meanings. So, I did the natural thing that came to my mind and wrote enum classes like this:

enum class latching_comparator : bool
{
    non_latching= false,    //default
    latching    = true
};
enum class comparator_polarity : bool
{
    low = false,        //default
    high= true
};

I know I can derive my enum class from uint8_t and uint16_t for 8 and 16 bit entities but I don't need them. What I need instead are 2 and 3 bit entities. However, I can't do this:

typedef std::bitset<2> twobits;
enum class comperator : twobits
{
...
}

Is there a way I can group bits like this and make it easy to write further codes using this? Is there a way to combine and get back bits/groups of bits this way?

Suggestions for any other method of doing this is also welcome.

  • why a third enum? consider `std::pair` – 463035818_is_not_an_ai Mar 19 '23 at 19:20
  • @463035818_is_not_a_number I am really sorry for the confusion. The comparator enum is independent of the last two enums, it's not a combination of the two. There are in total 5 types of 1 bit configurations, a 2-bit configuration and 3 types of 3-bit configurations, in total serialized to 16 bits. – Aparajita Roy Mar 19 '23 at 19:31
  • 2
    *"What I need instead are 2 and 3 bit entities."* -- just checking: you do realize that even a 1-bit entity occupies an entire byte of memory, right (with some special exceptions, such as bit fields if the compiler decides to pack them)? What do you hope to gain by having a 2-bit entity instead of having a `uint8_t` where you ignore 6 of the bits? – JaMiT Mar 19 '23 at 19:44
  • The platform dependent old school solution is bit-fields. Although part of C since day 0, they are very unpopular due to underspecification and platform dependece. – Red.Wave Mar 19 '23 at 19:50
  • @JaMiT Yes, while reading up about bitset, I found out about it just yesterday. But as long as it behaves the way I want, I don't care much. The behavior I want is to properly name (why I used enum) the individual bits so as not to open up the documentation of the device every 2 seconds and a way to combine them. – Aparajita Roy Mar 20 '23 at 04:51

1 Answers1

0

You can use bit fields in a struct to accomplish this. Depending on what you are doing, you may need to use #pragma pack which will fix alignment issues.

enum class latching_comparator : bool
{
    non_latching= false,    //default
    latching    = true,
};

enum class comparator_polarity : bool
{
    low = false,        //default
    high= true,
};

#pragma pack(push, 1)  
typedef struct
{
    comparator_polarity polarity        : 1;
    latching_comparator latching        : 1;
    uint8_t test                        : 3;
} comparator_t;
#pragma pack(pop)

This says that each enum in the struct uses only 1 bit. The test member uses 3 bits, which could be a 3 bit value or flag member, just as an example. This will only use one byte for both enums. Technically it only uses 5 bits, but 8 bit arch technically can't go lower, so you always use at least 8. If you use 9 bits in the struct, it will use 2 bytes since you move into another byte for bit 9.

std::cout << sizeof(comparitor_t) << std::endl;

Will print 1 because it only uses one byte to accomplish this.

I didn't read the documentation on the hardware you're interfacing with, but it would be best to design the entire struct around the registers you're configuring. The bit sizes will vary for each configuration in that register, but generally I write one struct for each register, manipulate the struct, then i just output the bytes of the struct straight into the communication interface, no need to hassle with bit shifting etc.

Edit: I'm feeling generous today, so here is some of the code to work with the config register of that chip (reading or writing). The register has more configurations in it, so you still need to finish it, but it gives you a direct example of what I'm talking about.

typedef enum
{
    ADS115_CONFIG_COMP_QUE_ASSERT_ONE           = 0b00,
    ADS115_CONFIG_COMP_QUE_ASSERT_TWO           = 0b01,
    ADS115_CONFIG_COMP_QUE_ASSERT_FOUR          = 0b10,
    ADS115_CONFIG_COMP_QUE_ASSERT_DISABLE       = 0b11
} ads115_config_comp_que_t;

typedef enum
{
    ADS115_CONFIG_COMP_LAT_NONLATCHING          = 0b0,
    ADS115_CONFIG_COMP_LAT_LATCHING             = 0b1
} ads115_config_comp_lat_t;

typedef enum
{
    ADS115_CONFIG_COMP_POL_ACTIVE_LOW           = 0b0,
    ADS115_CONFIG_COMP_POL_ACTIVE_HIGH          = 0b1,
} ads115_config_comp_pol_t;

typedef enum
{
    ADS115_CONFIG_COMP_MODE_TRADITIONAL         = 0b0,
    ADS115_CONFIG_COMP_MODE_WINDOW              = 0b1
} ads115_config_comp_mode_t;

typedef enum
{
    ADS115_CONFIG_DR_8_SPS                      = 0b000,
    ADS115_CONFIG_DR_16_SPS                     = 0b001,
    ADS115_CONFIG_DR_32_SPS                     = 0b010,
    ADS115_CONFIG_DR_64_SPS                     = 0b011,
    ADS115_CONFIG_DR_128_SPS                    = 0b100,
    ADS115_CONFIG_DR_250_SPS                    = 0b101,
    ADS115_CONFIG_DR_475_SPS                    = 0b110,
    ADS115_CONFIG_DR_860_SPS                    = 0b111,
} ads115_config_dr_t;

typedef enum
{
    ADS115_CONFIG_MODE_CONTINUOUS               = 0b0,
    ADS115_CONFIG_MODE_ONESHOT                  = 0b1,
} ads115_config_mode_t;

#pragma pack(push, 1)
typedef struct
{
    ads115_config_comp_que_t comp_que           : 2;
    ads115_config_comp_lat_t comp_lat           : 1;
    ads115_config_comp_pol_t comp_pol           : 1;
    ads115_config_comp_mode_t comp_mode         : 1;
    ads115_config_dr_t dr                       : 3;
    ads115_config_mode_t mode                   : 1;
    // @TODO: other configurations
} ads115_reg_config_t;
#pragma pack(pop)
deviantgeek
  • 121
  • 3
  • Thank you very much I didn't know about bitfields and packing. – Aparajita Roy Mar 20 '23 at 09:29
  • One question: do I have to use typedef struct instead of just struct? I'm using c++. – Aparajita Roy Mar 20 '23 at 09:31
  • @AparajitaRoy You don't, just habit for me to do so. The bit fields still work so long as you define them in the struct. They do work in other places as well, but it's best to put them at the layer that needs it and not force everything in your application to adhere to it. Sometimes there is a performance impact when using them (compiler does the bit manipulations for you), so if you minimize the time those operations are necessary, it's basically no impact. Also, thank you! I couldn't remember the name of this concept, so I called it bit widths; but it's bit fields. – deviantgeek Mar 20 '23 at 21:41
  • @deviantgeek *"just habit for me to do so"* -- you might want to break that habit so your code is simpler. This answer is a good place to start practicing the more usual style, given that it is now slated to stand for all eternity as the answer to this question. (I.e., edit your answer to remove `typedef`.) Make it an example that you'd be proud of others using years from now. – JaMiT Mar 21 '23 at 01:19
  • @JaMiT i'm really not sure what you're trying to get at here. i see nothing wrong with typedef's and you've provided no explanation as to why they're "bad". I'm not going to remove the typedefs. I use them as habit because i can define them as a type and explicitly call out what member variables/parameters should be for clarity of the reader and myself. There are many advantages to doing this, too many for a mere comment. I'm not looking for praise or something to be proud of here, someone asked for help and i helped. If you can offer better advice, then do it instead of hiding in the comments – deviantgeek Mar 21 '23 at 12:30
  • *"i see nothing wrong with typedef's"* -- sorry, I thought you did when you wrote "just habit for me to do so". It's not a big problem, but having `typedef` adds to the size of your source code without contributing anything. Instead of `typedef struct { comparator_polarity polarity : 1; /* etc. */ } comparator_t;` you could more simply have `struct comparator_t { comparator_polarity polarity : 1; /* etc. */ };` for the same result (and similarly for an `enum`). Still defined as a type, just using the shorter C++ style instead of the more verbose C style. – JaMiT Mar 22 '23 at 00:23
  • @deviantgeek @JaMiT unfortunately packing didn't work for me. I have no way of seeing in which order the bits are being transmitted/built in every byte, bitset doesn't work. Some examples I found online makes an array of the 3 bytes as suggested in the datasheet, and uses `write(fd, char_iterator_begin, length_in_byte)`. I'm guessing the first byte's last bit is transferred first after the first byte, the second byte's last bit etc. So I have to again split the bytes and reorder them. For now I'm using bitmasking ( `byte_ = byte_ ^ ((byte_ ^ bits) & mask);` ) to prepare the bytes. – Aparajita Roy Mar 22 '23 at 05:40
  • @deviantgeek I also need to handle a hardware interrupt and found that it can only be done in kernel space. While reading up the documentation for broadcomm manual for this, I'm seeing that a lot of bit manipulation needs to be done in the kernel/drivers. I'm surprised that c/c++ doesn't offer a nicer way of doing this till now. – Aparajita Roy Mar 22 '23 at 05:59