Model execution in independent mode
This article describes the runtime of the independent execution mode: the correct call of the model’s C functions on the target platform after generation, assembly and loading. Interactive execution is not considered here.
Generating a C code from a model
Before the target starts working, Engee generates the C-code of the model. This data is input for the target.
The generated C code of the model contains functions and structures that implement the calculation of the model.:
-
the initialization function;
-
one or more step functions that need to be called with a specified period;
-
the completion function;
-
the source files of the model;
-
metadata about sampling periods, signals, parameters, and model entry points.
The target does not generate the content part of the model again. His task is to embed the already generated C code into the runtime of the target platform. In this context, runtime is a binding around a model: an entry point, a step planner, timer operation, drivers, build files, and, if an interactive mode is needed, a data exchange channel.
Where is the execution set?
The Python target class creates a project, but does not define the behavior of the model at each time step. This behavior is set by the runtime code that the target generates in generate_executable_code.
The runtime code is a C/C++ binding around the generated model. It usually includes:
-
the entry point of the program or firmware;
-
initializing the platform timer;
-
calling the model initialization function;
-
A scheduler that calls the model’s step functions with the required frequency.;
-
checking the end time of the simulation;
-
calling the model completion function;
-
handling violations of step execution time.
Most often, the runtime code is created from a Jinja2 template. Jinja2 is not a requirement: you can use any other template engine or generate files programmatically. But the existing targets use Jinja2, and the auxiliary function render_template_to_file It is already designed specifically for Jinja2 templates.
Model entry points
After generating the C code Engee passes a description of the model’s entry points to the target. The entry point is a C function that runtime must call at the right moment.
In the article on code generator capabilities this is described from the side of the generated C interface: the external code connects modelname.h, calls the initialization function once, then periodically calls the step function with the required step, and when completed, it calls the completion function. Target does the same thing, but not manually: it generates a runtime template that performs these calls on the target platform.
The main types of entry points:
-
init— initialization of the model state, called once before the first step; -
steps— a list of step functions that calculate the model; -
terminate— completion of the model, called at a regular stop, if it is available for the platform.
Target receives this data via CodeGenInfo, which is created from model_code.c_code.c_code_info.
Example of receiving CodeGenInfo
codegen_info = create_codegen_info(
model_code.c_code.c_code_info,
model_settings.is_ext_mode,
)
The following fields are usually passed to the runtime template:
-
model_name— the name of the model and the main header file; -
init.cname— the name of the Si initialization function; -
steps— list of step functions; -
step.cname— the name of the specific step function; -
step.base_rate_scale— the ratio of the step period to the base period; -
terminate.cname— name of the C completion function; -
base_rate— base period in seconds; -
base_rate_us— base period in microseconds; -
stop_timeandstop_time_us— the final simulation time, if specified.
Basic Scheduler
The independent execution scheduler must repeat one basic cycle.:
-
Remember the start time of the current basic step.
-
Call all the step functions that should be executed at this basic step.
-
Increase the base step counter.
-
Check if the end simulation time has been reached.
-
Wait for the next basic step or fix a violation of the execution time.
The main rule is that all step functions are checked against a single base step counter. If the function has base_rate_scale == 1 it is called at every basic step. If base_rate_scale == 10, then the function is called at every tenth basic step.
Example of a minimal Jinja2 scheduler template
#include "{{ model_name }}.h"
static uint64_t step_number = 0;
static const uint32_t base_rate_us = {{ base_rate_us }};
int main(void)
{
platform_timer_init(base_rate_us);
{{ init.cname }}();
while (1) {
uint32_t step_start_us = platform_time_us();
{% for step in steps %}
if ((step_number % {{ step.base_rate_scale }}ULL) == 0ULL) {
{{ step.cname }}();
}
{% endfor %}
step_number++;
{% if stop_time %}
if ((step_number * base_rate_us) > {{ stop_time_us }}ULL) {
{{ terminate.cname }}();
break;
}
{% endif %}
platform_wait_until_next_tick(step_start_us, base_rate_us);
}
return 0;
}
Multiple step functions
A model can have one or more step functions. Multiple step functions are used when the model has multiple calculation step values (sample time). The runtime should call each function with its period, but synchronize according to the fastest, base period.
The number of step functions depends on the settings for generating the multi-frequency code. In the single-task version, the generator will create only one step function, which already contains conditions for slower frequencies. In the multitasking version, the generator creates several step functions, each of which corresponds to its sampling frequency; then the external target binding must trigger each of them with the correct period.
The following rule must be followed for the target: instead of assuming a single step in the model, use a list CodeGenInfo.steps. If there is one element, one step is performed; if there are several, the runtime sequentially processes all the elements, applying it to each base_rate_scale.
For example:
-
the base period is 1 ms;
-
step A has
base_rate_scale = 1and it is called every 1 ms; -
step B has
base_rate_scale = 10and it is called every 10 ms.; -
step C has
base_rate_scale = 100and it is called every 100 ms.
Take the order of calling step functions from CodeGenInfo.steps. Functions cannot be called in any other order.
|
You can’t call only the first step function. Such a run-time will be correct only for the simplest models and will lead to errors in models with several sampling steps.
Example of calling multiple steps
for (;;) {
if ((step_number % 1ULL) == 0ULL) {
model_step_1ms();
}
if ((step_number % 10ULL) == 0ULL) {
model_step_10ms();
}
if ((step_number % 100ULL) == 0ULL) {
model_step_100ms();
}
step_number++;
wait_until_next_1ms_tick();
}
Template Dictionary Extension
CodeGenInfo It contains general data that most runtime templates need. But a specific platform often needs additional parameters: timer mode, board name, layout file path, CPU frequency, stack size, RTOS task settings.
In this case, you can expand the dictionary that is passed to the Jinja2 template. They usually take codegen_info.model_dump(), add platform fields and pass the result to render_template_to_file.
Example of adding EDM-Target block parameters to the template context
codegen_info = create_codegen_info(
model_code.c_code.c_code_info,
model_settings.is_ext_mode,
)
context = codegen_info.model_dump()
context.update(
{
"cpu_frequency_hz": self.target_block.cpu_frequency_hz,
"timer_prescaler": self.target_block.timer_prescaler,
"linker_script": self.target_block.linker_script,
}
)
render_template_to_file(
self.main_template,
self.project_path / "src" / "main.c",
context,
)
An example of using additional fields in a template
#define CPU_FREQUENCY_HZ {{ cpu_frequency_hz }}UL
#define TIMER_PRESCALER {{ timer_prescaler }}UL
extern const char linker_script_name[] = "{{ linker_script }}";
If Jinja2 is not used, the principle remains the same: first, a structured data set about the model and platform is formed, then runtime and build files are created based on it.
TET and step time violation
TET (task execution time) is the execution time of one basic runtime step. Usually, TET is measured from the beginning of the basic step to the moment when all the necessary step functions and overhead processing are completed.
For an independent execution mode, it is important that TET is less than or equal to the base period of the model.:
TET <= base_rate_us
If the TET is longer than the base period, the runtime will not have time to complete the model at the specified pace. This condition is usually called overrun.
Violation of TET makes the model work incorrectly:
-
discrete blocks start to be executed less frequently than specified in the model;
-
time delays, filters, regulators, and signal generators receive an incorrect time grid.;
-
slow calculation steps may be caused with a drift relative to the expected time.;
-
external devices receive commands later than the model suggests.;
-
The accumulated delay can make the simulation results unpredictable.
To prevent a violation of TET, it is recommended:
-
Measure TET at each basic step.
-
Set the flag
overrun_flagbyTET > base_rate_us. -
Do not perform the skipped steps in a batch if the platform is not explicitly designed for catch-up.
-
Document the selected processing strategy: continue execution, stop the model, skip a step, or only diagnostic indication.
Example of TET measurement
uint32_t step_start_us = platform_time_us();
run_due_model_steps();
uint32_t step_end_us = platform_time_us();
uint32_t tet_us = step_end_us - step_start_us;
if (tet_us > base_rate_us) {
overrun_flag = true;
} else {
overrun_flag = false;
platform_sleep_us(base_rate_us - tet_us);
}
On bare-metal and RTOS platforms, it is better to measure TET using a timer with sufficient resolution. If the base period of the model is 1 ms, the timer with a resolution of 1 ms is already too rough for diagnosis: it will not show how close the runtime comes to violating the period.
Model time and timestamp
The runtime must distinguish between the physical time of the platform and the model time.
The physical time of the platform is taken from the hardware timer, the RTOS tick, or the system clock and is used to wait for the next step and measure TET.
The model time is usually calculated from the step counter:
model_time = step_number * base_rate
This is enough to check the achievement. stop_time when running independently, and for user drivers who need to know the current model time.
Example of a timestamp calculated based on the model time
uint32_t getCurrentTimestampUs(void)
{
return step_number * base_rate_us;
}
double getCurrentTimestamp(void)
{
return step_number * base_rate;
}
What to choose for the new platform
Fix the solutions before implementing the runtime template.:
-
which time source sets the base period;
-
where step functions are executed: in the main loop, in an interrupt, in an RTOS task, or in a separate process;
-
which timer is used to measure TET;
-
what happens during overrun;
-
how is the stop implemented?;
-
is it called
terminateat the regular completion; -
which platform drivers are available from the step functions;
-
whether additional fields are needed in the context of the template.
For the first version of the target, a simple main loop or periodic task is usually sufficient. The main thing is to correctly call all step functions using their base_rate_scale, withstand the base period and explicitly handle the TET violation.