Little-Endian and Big-Endian Memory Storage
Each memory address stores 8 bits, so data must be accessed in 8-bit increments.
Data Types
Type Casting Does Not Change Variable Types or Values
Consider this example from an official ST function:
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
Although Flash memory can only write 16 bits at a time, it can handle 32-bit data through the following approach:
*(__IO uint16_t*)Address = (uint16_t)Data; // Store lower 16 bits at Address
uint32_t tmp = Address + 2; // Move to next address for upper 16 bits
*(__IO uint16_t*)tmp = Data >> 16; // Store upper 16 bits
The expression (uint16_t)Data places the value in a 16-bit temporary variable, effectively containing only the lower 16 bits. This temporary variable then stores these lower 16 bits at the specified address. Note that STM32 follows little-endian mode for memory writes, so lower bytes are stored first. When casting from a larger to a smaller data type, the excess higher bits are discarded. However, the original Data variable remains unchanged as a 32-bit value.
Similarly, Data >> 16 shifts the value right by 16 bits, effectively keeping only the upper 16 bits. When stored in memory at the temporary address, this completes the 32-bit storage process.
Let's demonstrate with a practical example:
int main()
{
OLED_Init();
uint32_t number = 0x00010001;
uint16_t lower_half = (uint16_t)number;
uint32_t temp = number;
uint8_t byte1, byte2, byte3, byte4;
byte1 = (0xff000000 & temp) >> 24;
byte2 = (0x00ff0000 & temp) >> 16;
byte3 = (0x0000ff00 & temp) >> 8;
byte4 = (0x000000ff & temp);
OLED_Printf(0, 0, OLED_6X8, "temp %d", temp);
OLED_Printf(0, 8, OLED_6X8, "byte1 %d", byte1);
OLED_Printf(0, 16, OLED_6X8, "byte2 %d", byte2);
OLED_Printf(0, 24, OLED_6X8, "byte3 %d", byte3);
OLED_Printf(0, 32, OLED_6X8, "byte4 %d", byte4);
OLED_Update();
return 0;
}
Type Casting Does Not Change Pointer Types or Pointed Value Types
int main()
{
OLED_Init();
uint32_t number = 0x00010001;
uint32_t* pNumber = &number;
uint16_t* pValue = (uint16_t*)pNumber;
uint8_t byte1, byte2, byte3, byte4;
byte1 = (0xff000000 & (*pNumber)) >> 24;
byte2 = (0x00ff0000 & (*pNumber)) >> 16;
byte3 = (0x0000ff00 & (*pNumber)) >> 8;
byte4 = (0x000000ff & (*pNumber));
OLED_Printf(0, 0, OLED_6X8, "temp %d", (*pNumber));
OLED_Printf(0, 8, OLED_6X8, "byte1 %d", byte1);
OLED_Printf(0, 16, OLED_6X8, "byte2 %d", byte2);
OLED_Printf(0, 24, OLED_6X8, "byte3 %d", byte3);
OLED_Printf(0, 32, OLED_6X8, "byte4 %d", byte4);
OLED_Update();
return 0;
}
Let's verify this with C++:
int main()
{
int a = 10;
int* pA = &a;
float* temp = (float*)pA;
cout << typeid(a).name() << endl;
cout << typeid(pA).name() << endl;
return 0;
}
Extracting Individual Bytes from Integers
We can define four uint8_t variables and extract each 8-bit segment from an integer:
int main()
{
OLED_Init();
int number = 261;
uint8_t byte1, byte2, byte3, byte4;
byte1 = (0xff000000 & number) >> 24;
byte2 = (0x00ff0000 & number) >> 16;
byte3 = (0x0000ff00 & number) >> 8;
byte4 = (0x000000ff & number);
OLED_Printf(0, 0, OLED_6X8, "number %d", number);
OLED_Printf(0, 8, OLED_6X8, "byte1 %d", byte1);
OLED_Printf(0, 16, OLED_6X8, "byte2 %d", byte2);
OLED_Printf(0, 24, OLED_6X8, "byte3 %d", byte3);
OLED_Printf(0, 32, OLED_6X8, "byte4 %d", byte4);
OLED_Update();
return 0;
}
Storing Data in Memory
*((__IO uint16_t*)Address); // For STM32, addresses are 32-bit, but this creates a 16-bit pointer to the address
Extracting Bytes from Floating-Point Numbers
Single-Precision Floating-Point Numbers
Single-precision floats are stored in memory according to IEEE754 standards. For example, 19.625 is represented as 01000001100111010000000000000000.
Floating-point numbers cannot participate in bitwise operations, so we need special handling using unions. A union allows a uint32_t variable and a float to share the same memory space. The uint32_t will then have the same binary representation as the float.
union float_test
{
float value;
uint32_t num;
} test;
int main()
{
test.value = 19.625;
OLED_Init();
uint8_t byte1, byte2, byte3, byte4;
byte1 = (0xff000000 & (test.num)) >> 24; // 65 decimal, 41 hex
byte2 = (0x00ff0000 & (test.num)) >> 16; // 157 decimal, 9D hex
byte3 = (0x0000ff00 & (test.num)) >> 8; // 0
byte4 = (0x000000ff & (test.num));
OLED_Printf(0, 0, OLED_6X8, "number %f", test.value);
OLED_Printf(0, 8, OLED_6X8, "byte1 %d", byte1);
OLED_Printf(0, 16, OLED_6X8, "byte2 %d", byte2);
OLED_Printf(0, 24, OLED_6X8, "byte3 %d", byte3);
OLED_Printf(0, 32, OLED_6X8, "byte4 %d", byte4);
OLED_Update();
return 0;
}
Project Problem 1: Writing to Flash Memory
How do we write floating-point and integer values to Flash memory? We can use FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data):
float value = 19.625;
MyFLASH_ErasePage(0x0800FC00);
MyFLASH_ProgramWord(0x0800FC00, value); // Core function is FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
OLED_Printf(35, 17, OLED_8X16, "%05.3f", MyFLASH_ReadFloat(0x0800FC00));
OLED_Update();
The issue is that the data type of the variable determines how it's stored in Flash. When passing a float to a uint32_t parameter, it gets stored as a uint32_t rather than a float.
We can solve this using a union:
union float_test
{
float value;
uint32_t binary;
} test;
int main(void)
{
MyI2C_Init();
OLED_Init();
test.value = 19.625;
MyFLASH_ErasePage(0x0800FC00);
MyFLASH_ProgramWord(0x0800FC00, test.binary);
OLED_Printf(35, 17, OLED_8X16, "%05.3f", MyFLASH_ReadFloat(0x0800FC00));
OLED_Update();
}
The principle is that value and binary share the same memory. When value stores 19.625 as a float, the binary representation remains unchanged when accessed as a uint32_t.
Alternatively, using pointers (a better approach):
int main()
{
float data = 19.625;
uint8_t* pData = (void*)&data;
OLED_Init();
OLED_Clear();
OLED_Printf(0, 0, OLED_8X16, "%d", *pData);
OLED_Printf(0, 16, OLED_8X16, "%d", *(pData + 1)); // Also pData[1]
OLED_Printf(0, 32, OLED_8X16, "%d", *(pData + 2));
OLED_Printf(0, 48, OLED_8X16, "%d", *(pData + 3));
OLED_Update();
return 0;
}
Reading from Flash Memory
Reading data from Flash depends on how it's interpreted and printed:
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t*)Address); // Retrieve data as uint32_t
}
float MyFLASH_ReadFloat(uint32_t Address)
{
return *((__IO float*)Address); // Retrieve data as float
}
OLED_Printf(35, 17, OLED_8X16, "%u", MyFLASH_ReadWord(0x0800FC00)); // Print as uint32_t
OLED_Printf(35, 17, OLED_8X16, "%06.3f", MyFLASH_ReadFloat(0x0800FC00)); // Print as float
In summary: when reading, printing, or writing to memory (including arrays), always be mindful of the data storage format. These methods also apply to EEPROM.
Project Problem 2: Working with EEPROM Arrays
When working with EEPROM, we can define a buffer array and write multiple values at once. How do we write float or int arrays to EEPROM, which stores data one byte at a time?
Here's a simplified approach:
void process_array(uint8_t* pArray) {
OLED_Printf(0, 0, OLED_6X8, "pArray[0] %d", pArray[0]);
OLED_Printf(0, 8, OLED_6X8, "pArray[1] %d", pArray[1]);
OLED_Printf(0, 16, OLED_6X8, "pArray[2] %d", pArray[2]);
OLED_Printf(0, 24, OLED_6X8, "pArray[3] %d", pArray[3]);
OLED_Printf(0, 32, OLED_6X8, "pArray[4] %d", pArray[4]);
OLED_Printf(0, 40, OLED_6X8, "pArray[5] %d", pArray[5]);
OLED_Printf(0, 48, OLED_6X8, "pArray[6] %d", pArray[6]);
OLED_Printf(0, 56, OLED_6X8, "pArray[7] %d", pArray[7]);
}
int main()
{
OLED_Init();
float arr[2] = {19.625, 65.875};
process_array((void*)arr);
OLED_Update();
return 0;
}
Note that writing arrays to memory follows little-endian storage. To read back the data:
int main()
{
OLED_Init();
uint8_t data_array[8] = {0, 0, 157, 65, 0, 192, 131, 66};
float* pArray = (void*)data_array;
OLED_Clear();
OLED_Printf(0, 0, OLED_6X8, "pArray[0] %05.3f", pArray[0]);
OLED_Printf(0, 8, OLED_6X8, "pArray[1] %05.3f", pArray[1]);
OLED_Update();
}
The key principle is that pointer interpretation depends only on the pointer type, not the original data type. This applies equally to integers and floating-point numbers.