Recently, I encountered a bug in code that handled longitude values, where 33 degrees longitude is typically written as 033. Upon investigation, I discovered that Java was calculating 033 + 1 as 28 instead of 34. This happened because, when we write longitude values like "033" in geographic notation, we mean decimal 33 degrees. However, if we write it directly in Java, C++, or similar languages, it is interpreted as octal 33 (which equals decimal 27) due to the leading zero!
Hardware in the Loop Aerospace Simulation Development
C++ and MATLAB Simulink tips for HWIL simulation software engineers
Wednesday, February 12, 2025
Longitude convention and octal values
Monday, February 10, 2025
Verifying Simulink generated C code
To verify that the generated code behaves the same as the Simulink model, I could save the controller outputs to a file, create a C project with a main() function to read those outputs, and feed them to the plant C code at each time step—essentially converting a closed-loop simulation into an open-loop one. This approach would work if I use a single-step numerical integrator/solver like ode1 (Euler method). However, I usually prefer ode4 (Runge-Kutta 4) due to its better accuracy with less computation. Despite RK4 having 4 intermediate computations, it has a global error of O(h^4) and Euler O(h). If RK4 uses a step size of h, Euler would need h^4 to achieve similar accuracy. So, in total Euler would require more function evaluations than RK4—specifically, (1/h^3)/4 times more. For example, if hRK4 = 0.1, Euler would need 250x more computations.
Unfortunately, RK4 is a multi-step method with minor step sizes. Since the Simulink model saves controller outputs only at major steps, reading them from a file to drive the plant might result in divergent behavior—especially if the system has high-frequency dynamics—because controller outputs could differ at minor steps, while reading from the file only accounts for major step outputs.
You can only fully verify the system by integrating the C code into your embedded setup, which generates controller outputs based on real time plant-sensor data. Since testing on an embedded system takes time, to catch issues such as uninitialized variables in the plant code, you can create a C project, build and run your plant model with constant controller inputs. If it compiles and runs without NaN values, you can proceed to the embedded testing phase.
If you want to be more rigorous, you could save the plant outputs together with the controller outputs. In your C project, you read the controller output, feed it to the plant, run it for only one time step, read the Simulink plant output for that time instance from a file, and compare it with the C code plant output. Even this comparison is not perfect and only works for the first time step because Simulink integrator block outputs depend on the system state in the previous time step. To check the whole time span, you also have to save the system state and initialize the system with the correct state at each time step.
Wednesday, January 29, 2025
Linux: Dynamic vs Static Linking
Today, we had a chat with a colleague about whether a C/C++ binary (ELF) built on one Linux distribution would work on another. After some research, I found that default GCC builds are dynamically linked, and the ELF file contains:
- Your program's code
- A list of dynamic dependencies (shared libraries) it needs
- Symbols that need to be resolved at runtime
linux-vdso.so.1 (0x00007fff6a0e1000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdaf6400000)/lib64/ld-linux-x86-64.so.2 (0x00007fdaf66b3000)
not a dynamic executable
Thursday, January 23, 2025
C++ std::endl vs "\n"
Thursday, January 9, 2025
Memory Insights from a Segfault
float var[4096]
. The quick fix was to increase the stack size of the thread calling that function from 32kB to 1MB. static float var[4096]
. Upon closer inspection of the code, I saw that var was just used to copy a static array before sending it to another function. Since that function was not modifying the array, there was no reason for the copy. Removing var removed large stack allocation. static uint8_t buffer[1024];
void foo() { uint8_t temp[1024]; }
auto* buffer = new uint8_t[1024];
Monday, December 30, 2024
Embedded Software: Simple vs. Complex
Embedded software is integral to modern technology, ranging from simple home appliances to advanced autonomous systems. It can be broadly classified into two categories: simple (Non-OS) and complex (OS-driven) embedded software.
Simple Embedded Software: When simplicity and low cost are priorities and an OS would be overkill
Examples:
- Power or temperature monitoring systems.
- Simple applications in household appliances like ovens and washing machines.
- Typically designed for applications with few tasks.
- No operating system necessary.
- Software interacts directly with the microcontroller’s hardware (registers etc.), forcing rewrites if the hardware changes.
- Low power consumption, low cost.
- Can be developed by electronics engineers, no need for computer engineers because basic embedded programming knowledge is sufficient.
- Deterministic: By sidestepping the complexity of OS schedulers, simple systems achieve predictable performance.
- Fewer abstraction layers make verification and validation straightforward, which is a huge advantage for safety-critical certification.
Examples:
- IoT devices requiring seamless connectivity.
- Systems involving advanced sensor integration or navigation.
- Runs on an operating system that manages tasks and system resources.
- Capable of handling multiple tasks and applications simultaneously.
- Safety-critical certification is difficult. To make it easier, safety-critical parts should be developed as separate, simpler modules.
- Less competition and higher profit margins, provided that you have a strong technical team.
- Requires computer engineers to lead the development because of increased software complexity. Besides embedded software courses, related concepts of algorithms, data structures, and operating systems are also a core parts of computer engineering but not electronics engineering.
- The OS abstracts low-level hardware management, enabling developers to focus on application logic. A POSIX-compliant application, for instance, can run on any POSIX-supporting OS with minimal changes.
- Easier for new developers to adapt and contribute due to less hardware dependency.
- A broad range of pre-existing libraries simplifies development.
- Operating systems provide abstraction layers (e.g., Linux Device Model), allowing drivers to expose standard interfaces while interacting with specific hardware.
- Simplifies adding new functionality (e.g. telemetry) or adapting to new hardware (e.g. new/different sensors).
- With minor modifications, software can be tested on a PC, speeding up testing with less effort (no need for electronic cards, power supplies, etc.) and reducing bugs.
Monday, December 16, 2024
Serialization
In C++, to serialize simple data, aka plain old data (POD), where the layout in memory is predictable, you can use a char*
(byte) buffer:
This method cannot be used for non-POD types (e.g., those with pointers or virtual methods) because their memory layout is not portable. Examples are std::string, std::vector. For such types, you can use std::ostringstream
:
Both approaches assume that the serialized data format and endianness match between serialization and deserialization. For more complex cases, use libraries like nlohmann/json for JSON-based serialization and Boost.Serialization for binary/text serialization with more features.