Close

Managing timing: how to sequence tasks

A project log for FlexSEA: Wearable robotics toolkit

OSHW+OSS to enable the next generation of powered prosthetic limbs and exoskeletons. Let’s make humans faster/better/stronger!

jean-franois-duvalJean-François Duval 09/04/2015 at 14:380 Comments

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