For the practical development of embedded systems, there are some purely practical skills that are handy and you should be aware of, here is our list.
Serialization is the process of conversion of values into a sequence of bytes. The process of deserialization is the reverse of that.
In embedded we do this heavily, mostly in the sense of:
It’s good practice to have one pattern per project for handling this conversion and using that consistently. A simple approach for this is to create conversion functions:
// Function takes in a pointer into buffer and value of
// type uint32_t. The value is serialized at the
// beginning of the provided buffer and takes 4 bytes.
// Note that the function does not check whenever buffer
// is big enough.
//
// Returns pointer to first byte after used bytes.
uint8_t* ser_uint32_t(uint8_t* buffer, uint32_t val){
for (uint32_t i = 0; i < 4; i++) {
*buffer = val & 0xFF;
val = val >> 8;
buffer++;
}
return buffer;
}
This is just a specific instance of a more generic pattern that we can describe as:
uint8_t* ser_<X>(uint8_t* buffer, <X> val);
The idea is that you implement this for each relevant <X>
in your codebase and use only these functions for serialization.
The API can vary and you can do it differently, in some situations it may be wise to implement bounds checking in the API of the function.
In the case of different languages like C++, this can be implemented as more complex template machinery or with overloads.
Note that if done correctly, you can nest the calls to avoid duplication of code:
struct color
{
uint16_t r;
uint16_t g;
uint16_t b;
};
uint8_t* ser_color(uint8_t* buffer, color c){
buffer = ser_uint16_t(buffer, c.r);
buffer = ser_uint16_t(buffer, c.g);
buffer = ser_uint16_t(buffer, c.b);
return buffer;
}
The pattern for deserialization is done in a similar way, but in reverse:
uint8_t* deser_uint32_t(uint8_t*buffer, uint32_t* val){
*val = 0;
buffer += 3;
uint8_t* res = buffer + 1;
for (uint32_t i = 0; i < 4; i++) {
*val = (*val << 8) + *buffer;
buffer--;
}
return res;
}
It’s a good idea to write tests for this, as it is easy to test in the end and it saves a lot of time, example simple test can be (but we expect that you can do better):
void test_serialization()
{
uint8_t buffer[4];
uint32_t test_val = 666;
ser_uint32_t(buffer, test_val);
uint32_t deser_val = 0;
deser_uint32_t(buffer, &deser_val);
assert(test_val == deser_val);
}
map_range
is not a function from the standard library, but it is something that we believe should exist in the toolbox of any embedded developer.
The idea is that map_range(x,from_min,from_max,to_min,to_max)
uses linear interpolation to move numerical value x
from range from_min
..from_max
to range to_min
..to_max
.
Let us use an example: We want to control RGB
LED with PWM pulses, and PWM peripheral uses percentages as its input.
That means that for each color (R
, G
, and B
), we have to specify how light we want in a range of 0%...100%
.
However, our algorithms assume standard RGB
format - three values in the range of 0...255
.
If we want to use our values to control the LED, we have to rescale the values so they fit properly into the specified input.
For that we can use the map_range
function, to convert R
channel: output_red = map_range(source_red,0,100,0,255);
.
This operation happens frequently in the embedded world, and in some cases with even more complex range conversions.
For example to control a servomotor - device with a motor that can spin an axis from a position of -90
degrees to a position of 90
degrees, one has to send it a PWM pulse in a range from 1000ms
to 2000ms
.
Where 1000ms
represents -90
degrees, 1500ms
represents 0
degrees, and 2000ms
represents 90
degrees.
In that case, we could use map range as follows: pulse = map_range(angle,-90,90,1000,2000);
Try to implement the function (note that you have two test cases already) with the following signature:
float map_range(float x, float from_min, float from_max, float to_min, float to_max);
This signature of the function could be problematic in embedded in case the microprocessor does not have a native floating point unit, or in case you run into precision problems.
For the sake of this course, using float
is good enough, but we want to warn you that it has its consequences and you would have to think twice in a real-life situation.
TBD
TBD
TBD
TBD
Here are a few of usefull commands for git that might be of use to you.
Always remember to use git status
to frequently check the state of your repository!
cd existing_folder
git init --initial-branch=main
git remote add origin https://gitlab.fi.muni.cz/pv198/2022/<xname>.git
git add .
git commit -m "Initial commit"
git push -u origin main
git tag <tag_name>
Command creates a new branch and switches the repository to it.
git checkout -b <branch_name>
To push all the content, you need two commands:
git push
git push --tags
The recommended way was described in this Stack Overflow