mirror of
https://github.com/Xevion/v2.xevion.dev.git
synced 2025-12-06 07:16:49 -06:00
195 lines
7.4 KiB
Markdown
195 lines
7.4 KiB
Markdown
---
|
|
layout: default
|
|
title: Restricted Memory & Data Framing Tricks
|
|
date: 2022-07-16 13:51:00 -0500
|
|
tags: c memory embedded ti msp430 union
|
|
_preview_description: Tips and tricks I learned about handling restricted memory while working on microcontrollers at my first internship
|
|
---
|
|
|
|
Working on microcontrollers is a far cry from the essentially unlimited memory and floating point operations available
|
|
in _Python_ and other high level languages. Here's what I have been learning at my _first_ internship...
|
|
|
|
## Restricted Memory
|
|
|
|
While working there, I was assigned to a project that required usage of a Texas Instruments MSP430 microcontroller.
|
|
While there were many quirks with working on MCUs like this (like having to ditch JetBrains & VSCode altogether!), the
|
|
biggest quirk isn't working with C: it's working without `malloc` altogether.
|
|
|
|
On low memory devices like this, memory is extremely limited and there is no certainty that your code will not leak memory.
|
|
Usage of `malloc` and other dynamic memory allocation methods are considered innately dangerous - while there is a chance
|
|
you will write perfect code that will properly allocate/deallocate, you can't be certain that your complex program
|
|
won't run out of memory on such a small footprint to work with.
|
|
|
|
Instead, variables are assigned either inside methods for short periods (and passed around), or they are assigned statically and globally.
|
|
It appears that the libraries I use personally prefer globally accessible variables, which 99% of the time, is very wrong - but in
|
|
Microcontroller land, global variables are your friend.
|
|
|
|
```c
|
|
#include <stdint.h>
|
|
|
|
uint8_t uid[4]; // A unique identifier
|
|
|
|
int main(void) {
|
|
UID_generate(uid, 4); // Pass the pointer, the function writes to it (and does not return it)
|
|
UART_putUid(uid, 4); // Write the UID to UART
|
|
}
|
|
```
|
|
```c
|
|
void UID_generate(uint8_t uid, int length) {
|
|
uint8_t i = 0;
|
|
while(i < length)
|
|
uid[i++] = RANDOM_char();
|
|
}
|
|
|
|
void UART_putUid(uint8_t* uid, int length) {
|
|
uint8_t i = 0;
|
|
while(i < length)
|
|
UART_putChar(uid[i++]);
|
|
}
|
|
|
|
void UART_putChar(uint8_t value) {
|
|
while(!(UCB0IFG & 0x1));
|
|
UCB0TXBUF = value;
|
|
}
|
|
```
|
|
|
|
<center>
|
|
<i>
|
|
<small>
|
|
UART is a serial communication technology we use to send characters & text to the COM terminal.
|
|
For more information, click <a href="https://www.youtube.com/watch?v=VBRUyLcqXV4">here</a>.
|
|
</small>
|
|
</i>
|
|
</center>
|
|
|
|
There's not much more to this - don't use `malloc`, stick to the _stack_ for actively executing methods and use _global variables_
|
|
when you need to go into low power mode while maintaining state.
|
|
|
|
Overall, this doesn't hinder ones ability to write working code - the features are still there, but the way you access
|
|
methods, store data & manipulate is re-organized - sometimes at the detriment to quality & refactoring efforts.
|
|
|
|
## Data Framing Tricks
|
|
|
|
While at my internship, I used my MSP430 microcontroller to communicate with various devices over UART and SPI. I also sent
|
|
commands to a ISO15693 NFC wafers. All of these interfaces are extremely low level and the best documentation I have
|
|
is often just a PDF and some random code scattered across the internet. There is no library to speak of, usually.
|
|
|
|
Communicating at a low level like this requires reading and writing individual bytes of data into _frames_, or arrays
|
|
of bytes with a well-defined structure.
|
|
|
|
<center>
|
|
<b>ISO15693 Write Single Block (Addressed)</b> <a href="http://www.ti.com/lit/an/sloa141/sloa141.pdf" style="color: #90bcff">source</a>
|
|
</center>
|
|
|
|
[![ISO15693 Write Single Block Diagram][iso15693-diagram]][iso15693-diagram-edn]
|
|
|
|
Traditionally, commands are built statically all at once in a mostly hardcoded manner:
|
|
|
|
```c
|
|
uint8_t offset = 0;
|
|
ui8TRFBuffer[offset++] = 0x61;
|
|
ui8TRFBuffer[offset++] = 0x21;
|
|
ui8TRFBuffer[offset++] = 0xA7;
|
|
ui8TRFBuffer[offset++] = 0x3E;
|
|
ui8TRFBuffer[offset++] = 0xFF;
|
|
ui8TRFBuffer[offset++] = 0x58;
|
|
// ... You get the idea
|
|
```
|
|
|
|
Instead, what if we could format this into a `struct` that we could pass around on the stack with a pointer?
|
|
|
|
```c
|
|
struct AddressedSingleBlockWrite {
|
|
uint8_t Flag;
|
|
uint8_t Command;
|
|
uint8_t Address[8];
|
|
uint8_t Block;
|
|
uint8_t Data[4];
|
|
};
|
|
|
|
int main() {
|
|
struct AddressedSingleBlockWrite command;
|
|
command.Flag = 0x20 | 0x40;
|
|
command.Command = 0x21;
|
|
uint8_t address[8] = {0xA7, 0x3E, 0xFF, 0x58, 0x21, 0x32, 0x10, 0xFE};
|
|
memcpy(&command.Address, &address, sizeof(command.Address));
|
|
command.Block = 0x05;
|
|
uint8_t data[4] = {0x11, 0x22, 0x33, 0x44};
|
|
memcpy(&command.Data, &data, sizeof(command.Data));
|
|
}
|
|
```
|
|
|
|
|
|
Now we have a defined structure in our source code and we can move and manipulate various parts of our command
|
|
structures without having to deal with hardcoded offsets. Still though, if we want to copy this command structure into
|
|
the buffer, we have to individually copy each part of the command - which will break the second we modify its structure.
|
|
|
|
There's a fantastic solution for it: **Unions**.
|
|
|
|
```c
|
|
union ASBWUnion {
|
|
uint8_t data[15];
|
|
struct AddressedSingleBlockWrite marshalled;
|
|
};
|
|
```
|
|
|
|
```c
|
|
union ASBWUnion demarshalled;
|
|
demarshalled.marshalled = command;
|
|
|
|
for (int i = 0; i < 15; i++)
|
|
printf("%x ", demarshalled.data[i]);
|
|
```
|
|
|
|
```sass
|
|
60 21 a7 3e ff 58 21 32 10 fe 5 11 22 33 44
|
|
```
|
|
|
|
`union`s are special datatypes that share a single memory footprint (equal to it's largest member) starting at the exact same point memory.
|
|
They combine neatly with `struct`s to allow us to represent the `AddressedSingleBlockWrite` as a single byte array.
|
|
|
|
<center>
|
|
<b>Note</b>: When implementing this, I do recommend that you create macro definitions for the length of the final command structure.
|
|
This will help greatly when it comes to refactoring or making adjustments to your command structure.
|
|
</center>
|
|
|
|
#### Reversing Endianness
|
|
|
|
If you check out TI's [sloa141][sloa141] PDF on ISO 15693 commands, you'll notice that many of the examples have
|
|
sections of their bytes reversed - sections like the _Address_ and _Data_ sections, but not the entire compiled command.
|
|
|
|
One such example, `60``21``9080C2E5D2C407E0``06``55443322`, has the _Flag_, _Command_,
|
|
_Address_, _Block_ and _Data_ stored in that order. But for this particular example, how could the address be `E007C4D2E5C28090`?
|
|
How could the data be `0x22334455`? This odd ordering has a name - Endianness - the order of bytes as the underlying architecture understands it.
|
|
|
|
While for my particular usage, reversing endianness was not needed, it's an interesting problem that can be solved
|
|
quite easily with our new data structure.
|
|
|
|
```c
|
|
void ReverseEndianness(struct AddressedSingleBlockWrite asbw) {
|
|
uint8_t temp;
|
|
int i = 0;
|
|
|
|
for (; i < 4; i++) {
|
|
temp = command.Address[i];
|
|
command.Address[i] = command.Address[8 - i - 1];
|
|
command.Address[8 - i - 1] = temp;
|
|
}
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
uint8_t temp = command.Data[4 - i - 1];
|
|
command.Data[4 - i - 1] = command.Data[i];
|
|
command.Data[i] = temp;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Conclusion
|
|
|
|
Working in a restricted memory space is not that hard once you get used to the lack of normal functions such as `malloc`
|
|
and `printf`, but balancing performance, power consumption, memory allocation and code quality gets harder the more
|
|
complex your program gets.
|
|
|
|
[iso15693-diagram]: /assets/img/iso15693_diagram.png
|
|
[iso15693-diagram-edn]: /assets/iso15693_diagram.edn
|
|
[sloa141]: http://www.ti.com/lit/an/sloa141/sloa141.pdf |