November 4th, 2019 at 11:04 PM
I recently completed an online course on embedded applications using C.
To tell you the truth, it wasn't worth the few hours I invested in it. I more or less scanned through all the material, and most of it was intro stuff I already knew about optimization, like bit masking using logical/bitwise operators, compiler optimizations, const vs. #define, etc. Nothing particularly difficult to understand as long as you have a good foundation in computer science, ie. understanding how a CPU actually works.
But there was one thing that I did learn, and have wondered about before.
Take some data structure for example. Using C pseudocode, we can assume something that looks like this:
And inside that data structure, you've got a bunch of boolean values (ie. true/false)
Well, note how in this specific use-case, I have eight values in this one structure. Now, Boolean values are either 0 or 1 traditionally, so how large is this data structure?
Well, 0/1 imply bits, so you might think 8 bits, or one byte.
Wrong.
In C, by default, a boolean value takes up one byte minimum, and since most other primitive data types (ie. int, double/float, char, etc.) can also be larger than one byte, we can say that the size can be much greater.
So the data structure given above isn't one byte, but it's eight bytes.
Now, this distresses me. If they're bools, they should only technically be 0 or 1 (or 0 and not 0) so we should be able to cut program size costs from an extra eight bytes to a single byte if we were to modify/read individual bit values.
And sure enough, you can mask/read/write bit values pretty easily, if you used a macro function like this:
But as you can see, using macro functions everywhere can start to make things really unclear, especially when you start to combine them.
Instead, in this course, they covered a cool concept called a 'bit field' which does exactly this, but without the messy code.
Let's go back to our first struct of bools. Instead, we'll rewrite it:
So the format is slightly familiar yet still different.
Realistically, any type can be used here, but I'm using single-byte ints for the sake of looking cool. What's important is the colon and size of the value in bits. Yes, you can have more than one, say three bits, and so when that field is read, you may have a value from 0-7 (or a three-bit value, 2^3)
Now, we can take another step further. What if you want to set/read a single bit rather than a specified group that you already made in the struct?
Well, you mush them all together. To do that, we use a union (or a structure that is split up and shares memory.)
So in short, that's how you turn a structure of eight bytes into a structure of eight bits instead, without making the code completely unreadable.
It feels a little more object oriented this way, but overall, it's pretty well optimized.
To tell you the truth, it wasn't worth the few hours I invested in it. I more or less scanned through all the material, and most of it was intro stuff I already knew about optimization, like bit masking using logical/bitwise operators, compiler optimizations, const vs. #define, etc. Nothing particularly difficult to understand as long as you have a good foundation in computer science, ie. understanding how a CPU actually works.
But there was one thing that I did learn, and have wondered about before.
Take some data structure for example. Using C pseudocode, we can assume something that looks like this:
Code:
struct {
//some code
}myBools;
And inside that data structure, you've got a bunch of boolean values (ie. true/false)
Code:
struct {
bool var1;
bool var2;
bool var3;
bool var4;
bool var5;
bool var6;
bool var7;
bool var8;
}myBools;
Well, note how in this specific use-case, I have eight values in this one structure. Now, Boolean values are either 0 or 1 traditionally, so how large is this data structure?
Well, 0/1 imply bits, so you might think 8 bits, or one byte.
Wrong.
In C, by default, a boolean value takes up one byte minimum, and since most other primitive data types (ie. int, double/float, char, etc.) can also be larger than one byte, we can say that the size can be much greater.
So the data structure given above isn't one byte, but it's eight bytes.
Now, this distresses me. If they're bools, they should only technically be 0 or 1 (or 0 and not 0) so we should be able to cut program size costs from an extra eight bytes to a single byte if we were to modify/read individual bit values.
And sure enough, you can mask/read/write bit values pretty easily, if you used a macro function like this:
Code:
#define MASK(x) (1<<x)
int main(){
const char toRead = 0b01001100; //Some 1 byte value defined in binary
uint_8 readVal; //Whatever the value of what we read will be, one byte in size
//read a single byte from a character value
readVal = (toRead >> 3) & MASK(3);
//returns 1
//toggle a single bit:
toRead ^= MASK(3);
//returns 0b01000100
//to set a bit (back)
toRead |= MASK(3);
//returns back to 0b01001100
return 0;
}
But as you can see, using macro functions everywhere can start to make things really unclear, especially when you start to combine them.
Instead, in this course, they covered a cool concept called a 'bit field' which does exactly this, but without the messy code.
Let's go back to our first struct of bools. Instead, we'll rewrite it:
Code:
struct {
uint_8 var1 : 1;
uint_8 var2 : 1;
uint_8 var3 : 1;
uint_8 var4 : 1;
uint_8 var5 : 1;
uint_8 var6 : 1;
uint_8 var7 : 1;
uint_8 var8 : 1;
}myBools;
So the format is slightly familiar yet still different.
Code:
type Name : bitSize;
Realistically, any type can be used here, but I'm using single-byte ints for the sake of looking cool. What's important is the colon and size of the value in bits. Yes, you can have more than one, say three bits, and so when that field is read, you may have a value from 0-7 (or a three-bit value, 2^3)
Now, we can take another step further. What if you want to set/read a single bit rather than a specified group that you already made in the struct?
Well, you mush them all together. To do that, we use a union (or a structure that is split up and shares memory.)
Code:
union{
uint_8 full;
struct{
uint_8 bit0 : 1;
uint_8 bit1 : 1;
uint_8 bit2 : 1;
uint_8 bit3 : 1;
uint_8 bit4 : 1;
uint_8 bit5 : 1;
uint_8 bit6 : 1;
uint_8 bit7 : 1;
}bits;
struct{
uint_8 bit0 : 3;
uint_8 bit1 : 2;
uint_8 bit2 : 1;
uint_8 bit3 : 1;
uint_8 bit4 : 1;
}fields;
}bitField;
//Examples
int main(){
bitField a = 0b01001100; //Same number as above.
uint_8 readVal;
//To read a single byte
readVal = a.bits.bit3;
//returns 1
//To read the first three bits in the field:
readVal = a.fields.bit0;
//Returns 4
//To set a bit:
a.bits.bit3 = 0;
//To toggle a bit:
a.bits.bit3 ^= 1;
//To read the value of the whole byte:
readVal = a.full;
//returns 0b01001100, or 76, or 0x4C
return 0;
}
So in short, that's how you turn a structure of eight bytes into a structure of eight bits instead, without making the code completely unreadable.
It feels a little more object oriented this way, but overall, it's pretty well optimized.