Scheduling cyclic tasks with Timer/Counter

Hi,
As a practise project, I am trying to program a PID controller on an Arduino nano with a ATmega328P with the following architecture:

  • Timer/Counter 0 manages the PWM output
  • Timer/Counter 1 works as pulse counter for the encoder
  • Timer/Counter 2 works as scheduler, using the internal clock to trigger the PID process (read counter, calculate PID correction value and set PWM) and the communication process

Additionally, there are other communication modules:

  • SPI to communicate the ATmega328p with other devices
  • UART for debug only

SPI, UART, PWM and pulse counter has been relatively easy to implement, but the scheduler requires a deeper knowledge and I am getting issues here.
The goal of the scheduler of to trigger two cyclical tasks, one each 10 ms to process the PID and another each 100 ms to read the target value and report the current value through SPI to the master device.
My current approach is to use Timer/Counter 2 in Clear Timer on Compare Match (CTC) mode with a compare value A and prescaler properly set to 10 ms and then set the Timer/Counter2 Compare Match A interruption to trigger the Task1 and update a cycle counter for the Task2.
Task2 will be triggered by Timer/Counter2 Compare Match B when the cycle counter reach 10 (compare value B works as a shift time of ~ 5ms to avoid overlapping between task 1 and task 2)

The time diagram looks like this:

            10 ms                 10 ms                              10 ms
   |____________________|____________________|______  ...  _____|__________________
       5 ms             |                    |                  |     5 ms
   |__________|         |                    |                  |__________|
   |~~~~~|    |=====|   |~~~~~|              |~~~~~|            |~~~~~|    |=====|
    Task1      Task2     Task1               Task1               Task1      Task2
  cycle = 1             cycle = 2           cycle = 3   ...   cycle = 10

Those are my source code files (header definitions and register configuration is omitted for the sake of simplicity):


#include "schd.h"
#include "schd_cfg.h"

void (*task_10_ms_ptr)(void);
void (*task_100_ms_ptr)(void);
volatile uint8_t task_10_ms_flag = 0;
volatile uint8_t task_100_ms_flag = 0;

/* Configure the TImer/Counter2 as scheduler */
void schd_init(void) {
	// Set the timer configuration registers:
	...;
	// Initialise the tasks to an empty value:
	task_10_ms(schd_empty_task);
	task_100_ms(schd_empty_task);
	// Enable Global Interrupts:
	sei();
}

/**
 * Specifies the 10 ms task
 * @param[in] task - Pointer to the function that will be executed
 */
void task_10_ms(void (*task)(void)) {
	task_10_ms_ptr = task;
}

/**
 * Specifies the 100 ms task
 * @param[in] task - Pointer to the function that will be executed
 */
void task_100_ms(void (*task)(void)) {
	task_100_ms_ptr = task;	
}

/**
 * Checks the tasks flags and execute them if the flag is true
 */
void scheduler(void) {
	if(task_10_ms_flag) {
		(*task_10_ms_ptr)();
		task_10_ms_flag = 0;
	}
	if(task_100_ms_flag) {
		(*task_100_ms_ptr)();
		task_100_ms_flag = 0;
	}
}

ISR(TIMER2_COMPA_vect) {
	// Update the cycle counter
	cycles++;
	// Set the task flag to true
	task_10_ms_flag = 1;
}

ISR(TIMER2_COMPB_vect) {
	switch(cycles)
	{
		case 1:
			// Set the task flag to true
			task_100_ms_flag = 1;
			break;
		case (SCHD_TASK_B_TIME/SCHD_TASK_A_TIME):
			// Reset the cycle counter
			cycles = 0;
			break;
		default:
			break;
	}
}

void schd_empty_task(void) {
}

And this is a minimal test I wrote to test the library:

#include "schd.h"
#include "uart.h"

void task_1(void);
void task_2(void);

char str_task_1[] = "-----1\n";
char str_task_2[] = "+++++2\n";

int main(void){
	// Initialise the scheduler and the UART:
	schd_init();
	uart_init();
	// Set the cyclical task:
	task_10_ms(&task_1);
	task_100_ms(&task_2);
	// Check the scheduler flags and execute the tasks:
	while(1){
		scheduler();
	}
	return 0;
}

/**
 * Dummy task that simply prints a message through the UART
 */
void task_1(void) {
	uint8_t idx = 0;
	while(str_task_1[idx] != '\n'){
		uart_tx(str_task_1[idx]);
		idx++;
	} 
	uart_tx('\n');
}

/**
 * Dummy task that simply prints a message through the UART
 */
void task_2(void) {
	uint8_t idx = 0;
	while(str_task_2[idx] != '\n'){
		uart_tx(str_task_2[idx]);
		idx++;
	} 
	uart_tx('\n');
}

The example works, but there are some problems:

  1. I would like to get rid of the scheduler() function. Ideally, I want to trigger the tasks directly from the interruption instead of setting a flag, but leaving the task execution out of the interruption itself. Is that possible?
  2. Since this approach requires a very accurate control of the time, what is the best way to get the clock cycles required by a function? My idea is to set some kind of post build test that analyses the task and verifies that there is no timing problems
  3. It seems to be some performance issues. If I increase the length of str_task_1[] or str_task_2[] the task2 message starts to appear every 8 task1 messages or less (instead of 10), which means that the current tasks requires more than 5 ms.
    Even without a clock cycle analysis those functions should be simply enough to be executed without any problem (UART uses a baud rate of 9600) so in my opinion there is a leak somewhere.
  1. Nope!
    Consider how the interrupt works. Interrupts are disabled inside of INT routine.
    Of course, you can do a simple task inside of INT, this is not a problem. However in the moment you will enable interrupts, and you have to, the MCU starts performing the next instruction from the main task.
    In addition, your tasks are working with uart, which is using interrupt. Interrupt inside of interrupt will cause hang of MCU.
  2. Probably the best approach is to write this piece of code in the assembler, if you are not CPP guru event though it is pretty hard to control the cpp compiler. Definitely assembler.
1 Like

If you mean precisely that one runs ten times for every time the other runs once, why not just run the slow thing every tenth time you run the fast thing?

a7

As the diagram show, task2 has to run every ten runs of task1, but after 5ms of task1 begin.

The easiest way to accomplish this is using both compare interrupts (COMPA and COMPB). COMPA is your 10 ms tickrate (as you already have planned), and COMPB at the 5 ms mark. This way, COMPA and COMPB interrupts are always triggered, and you are always guaranteed that COMPB is always 5ms from the start of each cycle.

if the dot is 1ms, it would look something like this

START
.....B.....A.....B.....A.....B.....A.....B.....A.....B.....A.....B.....A.....B.....A.....B.....A

as you can see, B is always going to be 5 ms ahead of A, and A happens every 10 ms

You just set a flag on your main loop to count A cycles, and every 10 cycles, you just enable task2 using COMPB interrupt in your main loop


uint8_t COMPA_flag = 0;
uint8_t COMPB_flag = 0;


// 10 ms ticker
ISR(TIMER2_COMPA_vect)
{
	COMPA_flag = 1;
}

ISR(TIMER2_COMPB_vect)
{
	COMPB_flag = 1;
}


int main(void)
{	
	// CTC mode
	TCCR2A = 1<<WGM21 | 0<<WGM20;
	TCCR2B = 0<<WGM22;

	// enable interrupts
	TIMSK2 = 1<<OCIE2B | 1<<OCIE2A;

	// compare match A at the 10 ms mark
	OCR2A = (157 - 1);
	
	// compare match B at the 5 ms mark
	OCR2B = (78 - 1);
	
	TCNT2 = 0;

	// start timer2 (1024 DIV), timer2 runs at 15625 Hz
	TCCR2B = 0<<WGM22 | 1<<CS22 | 1<<CS21 | 1<<CS20;
	
	uint8_t cycCounter = 0;
	
    while (1) 
    {
		// 5 ms mark
		if(COMPB_flag)
		{
			COMPB_flag = 0;
			
			if(cycCounter > 9)
			{
				cycCounter = 0;
				
				// do task 2 here
			}
		}
		
		// 10 ms mark
		if(COMPA_flag)
		{
			COMPA_flag = 0;

			cycCounter++;
			
			// do task 1 here
		}
    }
}

1 Like

I refer to this as "creative head-banging". :grin:

Trying to make things as difficult as possible. :astonished:

Have you not read Demonstration code for several things at the same time? Or did you not understand it?

Your shortest interval is five milliseconds. That's 5000 microseconds! At 16 MHz that is 80,000 instruction cycles.

Are you using interrupts because this is some kind of class assignment that REQUIRES interrupts?

I would just use micros().

unsigned long PreviousMicros = 0;
bool EvenInterval = false;
byte IntervalCount = 0;

void loop()
{
  unsigned long currentMicros = micros();
  // Every 5 milliseconds
  if (currentMicros - PreviousMicros >= 5000)
  {
    PreviousMicros += 5000;

    if (EvenInterval)
      task1();
     else
      task2();
    EvenInterval = !EvenInterval;

    // Every 100 milliseconds
    IntervalCount++;
    if (IntervalCount >= 20)
    {
       IntervalCount = 0;
      task3();
    }
  }
}
1 Like

Hello Alberto_Tejada
Do you have a requirement to use an interrupt.
Do you can use the Arduinio function millis() or micros() either?
Have a nice day and enjoy coding in C++.

Both the micros() and millis() functions are well structured, if you are using the Arduino IDE, there is little reason to change them, or not to use them.

Consider having multiple state machines in an interrupt that are executed in a sequential way, and use a compare if necessary.

As I just said in #5! :grimacing:

Of course, that is a totally valid approach. Nevertheless, I want to build this project totally from scratch, without using built-in Arduino libraries.
I know that this is reinventing the wheel, but I think this is a good exercise to understand the low level programming principles and face the 'correct questions' of embedded development, which are applicable beyond the Arduino boards.

I'm not sure I understand your point. In the example, the slow task is actually running every tenth time the fast task is executed, but in addition, it waits a shift time of 5 ms to allow the task1 to be completed.
That way, if for X reason task1 requires more time, task2 is not affected (assuming that task1 will never take 5 ms even in the worst case). So task1 and task2 are more independent on time from each other.

Good point, I think that moving as much logic as possible out of the interruptions will produce a cleaner code and a better performance.

I will apply those changes. Nevertheless, I doesn't really answer the questions.

Thanks for you contribution

Hello @paulpaulson, @johnwasser and @agm1dr ,
First of all, thanks for your comments. As I commented to @Paul_B, this project is developed from scratch, so I can not use millis() or micros().
Regarding the need of determinism, the problem is that the 'real' purpose of the 10ms task is to read the increment of pulses from the encoder and calculate the speed, so it is critical to guarantee that the pulses will the checked with a consistent cyclicity.

Hello Alberto_Tejada
Many thanks for your feedback.
Have a nice day and enjoy coding in C++.

1 Like

A good place to find AVR processor (not "Arduino") programming help is at AVRFREAKS.NET

1 Like

Also, consider that even if in this example the shortest interval is 1/2 of task1 cyclic time, that can be adjusted depending on the needs of task1 and task2.
If for example during the development I realise that task2 takes twice as long as task 1, I want to to be able of change the shift time from 5ms to 3ms so that there is no risk of overlapping.

Hello @Budvar10,
Thanks for answering my questions. I will proceed with the assembler as you have suggested.

But that is completely missing the point. :roll_eyes:

The use of "millis()" for timing is the basis of a pragmatic approach to timing. It is not some random and arbitrary creation of the Arduino project, it is a universal method of scheduling in a cooperative multi-tasking program. :+1:

It is not a matter of "can not". You do not need multiple timers and interrupts in "spaghetti" code. You simply use only one interrupt to generate your own "millis()" and you organise your timing on that.

Vastly neater.

And that is exactly what we are attempting to teach you! :grin:

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.