In many embedded applications timing is critical. Control loops have to be called at fixed intervals to guarantee stability and some data conversion have timings dependant on other software functions. One example, on the FlexSEA-Execute board, is the Delta Sigma ADC used for the Strain Gauge Amplifier: its conversion needs to be done when the I²C bus is in idle, otherwise the digital potentiometer are coupling noise in the sensitive analog circuit. We all know that a timer with interrupt can be used to generate precise timing, but we are also told that we should keep the interrupt service routine (ISR) code as short as possible. How can we get the best of all worlds?
Let’s say that we have a timer that calls an ISR every ms. The simplest ISR pseudo-code would look like:
timer_ISR()
{
your_function_that_needs_to_be_called_at_1kHz();
clear_ISR_flags();
}
You need to make sure that your_function_that_needs_to_be_called_at_1kHz() doesn’t take more than 1ms, and keep in mind that it will impact other timings in your code (if you have cycle-based delays like wait_ms(1) they will take longer to execute).
We can improve on that code by using a flag:
timer_ISR()
{
timer_isr_flag = 1; //Make sure to define that variable in your code
clear_ISR_flags();
}
In your main while() loop:
while(1)
{
if(timer_isr_flag == 1)
{
timer_isr_flag = 0;
your_function_that_needs_to_be_called_at_1kHz();
}
//[…] other code […]
}
The timing will not be as precise depending on the other code running in the loop, but if you minimize this “other code” it can be more than good enough (as always, test this with an oscilloscope or a logic analyzer!)
Now, your program will probably require more than one function call. You can add calls, as long as their total execution time is less than the timer period:
while(1)
{
if(timer_isr_flag == 1)
{
timer_isr_flag = 0;
your_function_that_needs_to_be_called_at_1kHz(); //Function #1
some_other_task(); //Function #2
oh_and_I_also_need_to_do_this(); //Function #3
}
//[…] other code […]
}
One thing that I do not like about this code is that if Function #1 takes longer, Functions #2 and #3 will be delayed. Also, how do you make it so #3 is called x µs after #1 without resorting to hard-coded delays? One option is to use more timers, and have one if(flag) call per timer. Another one, a better one in my book, is to divide that 1ms slot.
I’m using a 10kHz timer. Here’s the PSoC ISR code:
CY_ISR(isr_t1_Interrupt)
{
/* Place your Interrupt code here. */
/* `#START isr_t1_Interrupt` */
//Timer 1: 100us
//Clear interrupt
Timer_1_ReadStatusRegister();
isr_t1_ClearPending();
//All the timings are based on 100us slots
//10 slots form the original 1ms timebase
//'t1_time_share' is from 0 to 9, it controls the main FSM
//Increment value, limits to 0-9
t1_time_share++;
t1_time_share %= 10;
t1_new_value = 1;
//Flag for the main code
t1_100us_flag = 1;
/* `#END` */
}
And
here’s a simplified version of the main while(1) loop: //Main loop
while(1)
{
if(t1_new_value == 1)
{
//If the time share slot changed we run the timing FSM. Refer to
//timing.xlsx for more details. 't1_new_value' updates at 10kHz,
//each slot at 1kHz.
t1_new_value = 0;
//Timing FSM:
switch(t1_time_share)
{
//Case 0: I2C
case 0:
i2c_time_share++;
i2c_time_share %= 4;
#ifdef USE_I2C_INT
//Subdivided in 4 slots.
switch(i2c_time_share)
{
//Case 0.0: Accelerometer
case 0:
#ifdef USE_IMU
get_accel_xyz();
i2c_last_request = I2C_RQ_ACCEL;
#endif //USE_IMU
break;
//Case 0.1: Gyroscope
case 1:
#ifdef USE_IMU
get_gyro_xyz();
i2c_last_request = I2C_RQ_GYRO;
#endif //USE_IMU
break;
//Case 0.2: Safety-Cop
case 2:
safety_cop_get_status();
i2c_last_request = I2C_RQ_SAFETY;
break;
//Case 0.3: Free
case 3:
//I2C RGB LED
//minm_test_code();
update_minm_rgb();
break;
default:
break;
}
#endif //USE_I2C_INT
break;
//Case 1:
case 1:
break;
//Case 2:
case 2:
break;
//Case 3: Strain Gauge DelSig ADC, SAR ADC
case 3:
//Start a new conversion
ADC_DelSig_1_StartConvert();
//Filter the previous results
strain_filter_dma();
break;
//[…]
default:
break;
}
//The code below is executed every 100us, after the previous slot.
//Keep it short!
//BEGIN - 10kHz Refresh
//RS-485 Byte Input
#ifdef USE_RS485
//get_uart_data(); //Now done via DMA
if(data_ready_485_1)
{
data_ready_485_1 = 0;
//Got new data in, try to decode
cmd_ready_485_1 = unpack_payload_485_1();
}
#endif //USE_RS485
//[…]
}
else
{
//Asynchronous code goes here.
//WatchDog Clock (Safety-CoP)
toggle_wdclk ^= 1;
WDCLK_Write(toggle_wdclk);
}
}
While
the code above can look complex, it’s a big time saver. Once you implement that
time share strategy you can freely edit your code without messing up with
timings. It makes software updates and maintenance much easier! Please read the
comments to understand the code, I believe they are explicit enough and more
words wouldn’t make it clearer. If I’m wrong, ask any question in the comments.
And, as always, look at the source code for the complete solution.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.