Here we are to the solution that provides the best performance in terms of latency.
With a preemptive kernel, context switch happens under interrupt, so our motor protection process will receive immediately the control, as soon as the alarm pin fires the interrupt. Naturally, it must be configured to have a higher priority than other running processes. This way latency drops to a couple of microseconds, or even below if the CPU is really fast!
The source code remains the same as the cooperative kernel case. To activate the preemptive mode it's enough to change the kernel configuration using BeRTOS Wizard or by editing by hand the file cfg_proc.h. It's very easy to change between configurations, depending on your needs.
Preemptive & Cooperative: differences
However there's a big difference from the cooperative example: now every process can really be interrupted any time, so each shared data structure must be protected against concurrent access.
In our case, each access to the prm struct must be protected through a semaphore or a lock.
Let's discuss both solutions:
- A lock consist in completely disabling preemption (that is, context switching) in particular code sections. Of course, if the section is too long, we lose the low latency advantages given by the preemptive kernel;
- A semaphore is a tool that allows a finer control over access to shared resources, because it only blocks access to processes that try to access that particular shared resource.
In our case, the motor control process does not access the
prmstruct, so this is the preferred solution.
Let's see how the code changes when accessing to the prm struct using a semaphore in the preemptive case:
Semaphore prm_sem;
void temp_handler(void)
{
while (1)
{
/* Check current temperature state. */
temp = adc_read(TEMP_CH);
temp *= T_SCALE + T_OFFSET;
temp_check(temp);
/* Lock prm access */
sem_obtain(&prm_sem);
prm.temp = temp;
sem_release(&prm_sem);
timer_delay(TEMP_PERIOD);
}
}
void voltage_handler(void)
{
while (1)
{
/* Acquire and check voltages */
v = adc_read(VOLT_CH);
v = v * V_SCALE + V_OFFSET;
valve = voltage_check(v);
/* Update output */
setvalve(valve)
/* Lock prm access */
sem_obtain(&prm_sem);
prm.valve = valve;
sem_release(&prm_sem);
timer_delay(VOLTAGE_PERIOD);
}
}
void loadsave_handler(void)
{
while(1)
{
/* Wait for a keypress */
sig_wait(SIG_KEYBOARD);
/* Lock prm access */
sem_obtain(&prm_sem);
/* Handle parameter save/load */
if (load_pressed())
{
/* Load */
load_preset(&prm);
}
if (save_pressed())
{
/* Save */
save_preset(&prm);
}
sem_release(&prm_sem);
}
}
From what you see, before changing prm struct is necessary to obtain the corresponding semaphore. If we forget about it, random and scary things can happen.
The application will work fine most of the time, but sometimes it will output strange and unexpected results, due to that sometimes the struct is in inconsistent states.
These are typical concurrency problems and are also the most difficult to debug.
Before using a semaphore, we need to initialize it and we need to change the init() function:
void init(void)
{
/* Init timer*/
timer_init();
/* Init kernel */
proc_init();
/* init other modules */
...
/* Init prm semaphore */
sem_init(&prm_sem);
/* Create processes */
temp_proc = proc_new(temp_handler, NULL, temp_stack, sizeof(temp_stack));
voltage_proc = proc_new(voltage_handler, NULL, voltage_stack, sizeof(voltage_stack));
loadsave_proc = proc_new(loadsave_handler, NULL, loadsave_stack, sizeof(load_save_stack));
motor_proc = proc_new(motor_handler, NULL, motor_stack, sizeof(motor_stack));
/* Raise priority */
proc_setPri(motor_proc, HIGH_PRIORITY);
The main() function remains the same as the cooperative case.
This issue we have seen the last technique BeRTOS provides to the embedded programmer. The preemptive kernel provides the best performance, but also it's the most difficult to use because concurrency problems aren't easily to reproduce.
Next time we'll summarize all the techniques we have seen till now and we'll discuss pros and cons of each one.
