1. Memory and Bits - RIT-Launch-Initiative/Liftoff-Project GitHub Wiki
Welcome to the fun (and slightly painful side) of low level programming. Memory! All data in your program is put in memory somewhere.
A pointer is a variable that stores an address of another variable. A reference also stores the address of an object, but the difference is that it can't be NULL, so the memory it references must be valid and the reference cannot change once initialized.
Stack memory is just like the stack (LIFO) data structure. When a function declares a variable, memory is pushed onto the stack and once it exits, memory is popped out of the stack from the top. If you were ever curious about where the term stack overflow comes from, usually when you have a recursive function that never ends, its because you run out of memory on the stack that it overflows into other sections of memory.
On the opposite end of the memory spectrum, is the heap which is a pool of memory that you personally allocate and free yourself. In higher level languages like Python and Java, this is automatically handled for you. When allocating memory, you are given a pointer to that memory where you can store your values and can be used anywhere in a program. Unlike stack memory which gets destroyed once you exit a function, heap memory is only destroyed once you tell it to be destroyed. If you do not properly manage your memory and fail to free memory you no longer use it will lead to less available memory causing system degradation. In embedded systems, you will actively avoid using this type of memory due to memory constraints and non deterministic behavior. The time spent allocating memory is unpredictable and unexpected failure to allocate the memory is possible.
To allocate memory, use malloc function in C and the new keyword in C++.
C Example:
// Buffer is a term used to describe a group of memory for storing temporary data
// Use sizeof to avoid hardcoding a size for your variable. Multiply by 10 to fit space for 10 chars**
char string_ex[10] = malloc(sizeof(char) * 10)); // Allocate a char buffer size of 10
free(string_ex); // De-allocate memory to avoid a memory leak
C++ Example:
char string_ex[10] = new char[10];
delete string_ex;
Example Usage:
#include <memory.h>
#include <stdint.h>
const int buffer_size = 10;
// size_t is an unsigned integer type and its largest value depends on the max address number of the system
void fill_buffer(int *buff, size_t len) {
for (int i = 0; i < len; i++ {
*(buff + i) = i; // Equivalent to doing buff[i] = i; buff + i gets the next address to store the value in
}
}
int main(int argc, char **argv) {
// NOTE: Use malloc or new depending on language. This is just for example purposes
int *c_buffer = malloc(sizeof(int) * buffer_size);
int *cpp_buffer = new int[10];
// Notice there is no * included in the argument
fill_buffer(c_buffer, buffer_size);
// Clean up after yourself!
free(c_buffer)
delete cpp_buffer;
return 0;
}
Unlike stack and heap memory, the lifetime of static memory lasts until your program ends. Memory is allocated at compile time and the location is fixed. The downside of static
memory is that it represents global state and can be modified anywhere within the same file (depending on context). You can use the static keyword in front of the type of a variable. NOTE: The static
keyword can mean different things under different contexts, and I suggest reading up on it.
All variables have a memory size. The most common units used to refer to memory sizes are bits and bytes. 8 bits make up a byte. The size of an integer can vary based on the hardware. Usually a 32-bit operating system means an integer will be 32 bits or 4 bytes and a 64-bit operating systems has an integer the size of 64 bits or 8 bytes. The size of an integer defines the possible range of numbers it can store as shown by the table below.
To resolve portability issues in embedded systems, we use the stdint header and specifically define the size of the variables we use. The most common size we use is an unsigned 8 bit integer (uint8_t), the smallest possible unit.
Endianness is how a sequence of data is stored in computer hardware. The two types are little and big endian. Big endian is what humans are accustomed to reading which is where the leftmost bit is the most significant bit. In contrast, little endian is more efficient for computers to read and is where the leftmost bit is the least significant bit. Beware the difference between byte endianness and bit endianness. A system can store bits as big endian, but store a group of bytes as little endian.
Each bit in a number has a position that represents a value for a base 2 number (1, 2, 4, 8, 16, 32, 64, 128, ...). Bit numbering also depends on endianness, so in big endian, the rightmost position will always be 0. In little endian, the leftmost position will always be 0. Each position represents a number which is 2^n where n is the bit position. So a binary 1 at position 0 in a number represents 1 while a binary 1 at position 7 represents 128.
A single bit can represent things like state in hardware. Bit manipulation is useful for toggling bits to turn on things in hardware or reading the status of hardware. For example, from the Cortex M4 manual, you can read from a 32 bit hardware register. The 16th bit will tell you the status since you last read the bit (the hardware can tell when you access the bit and automatically clear it). For the first three bits you can toggle them between 1 and 0.
Here is a table of all possible bit operations you can do. The AND operation is useful for clearing a bit to 0. The OR operation is useful for setting a bit. Bitwise NOT is useful for flipping the bit completely. The shift operators allow you to specify which bits to set specifically.
Note: A common term used in computing is masking. In the context of bit manipulation, masking is when you extract and/or clear bits from a value. For example, if you wanted to grab the 3 rightmost bits from the hardware register you can apply a mask of 0b111, and AND it with the value.
uint8_t example = 0b11111111;
example = example & (0 << 4); // Clear the 4th bit. example equals 0b11101111
uint8_t example = 0b11101111;
example = example | (1 << 4); // Turn on the 4th bit. example equals 0b11111111
uint8_t example = 0b10101010;
example = ~example; // example == 0b01010101
- Since it'll be easier to follow through, just go through each function in the main file of the memory folder.