AVR Tutorial: the Architecture of a Microcontroller Program

Lecture



Every microcontroller course I've come across (including, unfortunately, my own assembly-language one, though I hope to gradually fix that) suffers from one and the same problem.
The course rushes to build a house without laying the foundation first. After showing how to blink an LED as the one example, it dives straight into peripherals. People start mastering PWM, timers, hooking up displays and all sorts of temperature sensors.
On one hand that's understandable — you want action and results right away. On the other — sooner or later that approach runs into the fact that a program built up with no clear underlying idea will simply collapse under its own complexity. Making any further development impossible.
The result is either the birth of horrifying programming monstrosities, or millions of forum questions along the lines of “how do I make everything happen at once, seeing as the controller's clock speed is no longer enough for it all?”

The interesting thing is that proper program organization is taught to programmers at universities, only people usually come to the microcontroller not from programming but from hardware. And, as university teaching practice has shown, electronics engineers are barely taught any real programming at all :( You end up having to figure it all out yourself.


So, what is program structure? First and foremost, it's the program's skeleton. The paths the code moves along. How transitions between firmware tasks are organized. How processor time gets allocated. Without a quick primer on the general principles of firmware design, there's no point moving on.

Everything written below is merely a product of my own reasoning, so the terminology may differ from what's generally accepted. If that really bothers someone — correct it in the comments.

So, I distinguish the following structures for myself, in order of increasing design complexity and amount of control code:

  • Super loop
  • Super loop + interrupts
  • Flag-based state machine
  • Task scheduler
  • Priority task scheduler
  • Cooperative RTOS
  • Preemptive RTOS

Now let's go through each point in detail:

Super loop
The simplest way to organize a program. It's marked by the minimum amount of control code (code that doesn't do useful work itself but only organizes the correct order of actions). Perfect for tasks along the lines of “blink an LED”.

The algorithm is as plain as can be (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main(void);
{
while(1)
	{
	Led_ON();
	Delay(1000);
	Led_OFF();

	u=KeyScan();
	switch(u)
		{
		case 1: Action1();
		case 2: Action2();
		case 3: Action3();
		}
	}
}

And it all goes on pretty much like that. In other words, plain sequential code execution. The delay — right there, in the same pile. Keyboard polling. Same thing. There can be jumps and branches inside a super loop, but the essence stays the same — the code just plods straight through.
The advantages are obvious right away — logic and simplicity (at first, later it's hell). The drawbacks also crop up right away — the more we cram into our code, the clumsier and slower it gets.
A super loop can, however, be optimized. For example, instead of just burning cycles inside the Delay() function, you can stuff keyboard polling and a whole bunch of other useful actions into it.
In the end, a super loop can give you an extremely compact, reliable, but at the same time completely monolithic program. You won't be able to add anything to it, or take anything away. And to account for everything, write it, and debug it, you'd have to be a genius. Have you read the story about One Byte? This is almost certainly about that, or about the next variant.
Usually, once people have had their fun with the super loop, they get a handle on interrupts and quickly move on to the super loop + interrupts variant.

Super loop + interrupts
A pure super loop is used extremely rarely, because any microcontroller has a pile of peripherals, and they come with a handy thing called interrupts (Although I've seen a comment on the microchip.ru forum claiming that interrupts reduce a program's reliability and are best avoided wherever possible).

Here the situation becomes somewhat different. Thanks to interrupts, we now get parallel processes. For example, we can hang keyboard polling and LED blinking off a timer interrupt (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ISR(Timer1)
{
u=KeyScan();
}

ISR(Timer2)
{
if(LED_ON)
	{
	Led_OFF();
	}
	else
	{
	Led_ON();
	}
}

void main(void);
{
Timer1_Delay=100; 	// Timer 1 interval, 100
Timer1=1<<ON;     	// Enable timer 1

Timer2_Delay=1000;	// Timer 2 interval, 1000
Timer2=1<<ON;		// Enable timer 2

SEI();			// Enable interrupts
while(1)
	{
	switch(u)
		{
		case 1: Action1();
		case 2: Action2();
		case 3: Action3();
		}
	}
}

Already better. It looks like the program now runs as three independent processes. Nothing lags anywhere and everything's fine. Yes, for as long as the hardware resources hold out. Because here's what we've got — one interrupt per process. And there are only so many timers, and you can't load interrupts down with time-heavy processes — they have to be fast.

And this is exactly the spot where the sleep of reason begins to produce monsters. Some extra conditions and checks start appearing. A pile of different events gets hung off a single interrupt, flag checks pile up, and you try to tie it all together… As a result the construction turns into a spaghetti tangle of jumps, calls, conditions… where changing or understanding anything becomes impossible. With no foundation, the construction collapses under its own weight. Wrecking its creator's brain in the process. Anarchy sets in…

What do you do? And what do you do when anarchy breaks out? That's right! You need some kind of unified rule — a bureaucratic apparatus. From a theoretical standpoint it's about as useful as milk from a billy goat — it doesn't poll the keyboard, it doesn't wiggle any pins, it doesn't send data. It just takes up space and eats up processor time. But you can't do without it.

Flag-based state machine
The first control mechanism is the classic flag-based state machine. There are a million implementations of it, everyone comes up with their own variant, but there isn't much difference between them.

So, in super loops our tasks were called one after another. Either in the order of a shared queue, or as a direct call. Obviously this chain ends up unbroken, and if there's a snag in it somewhere (some DELAY, say), the whole chain grinds to a halt.
A flag-based state machine lets you break that chain.

Here's its simplest variant.
First we define the flags. This is usually a bit field, where a single byte has several meaningful bits crammed into it. Each one is responsible for something. Particular extremists use a whole byte per flag.

Let's say we have a byte called flag, with various bits in it, marked with a dot in our pseudocode. So, it looks roughly like this (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void main(void)
{

//Start Task
flag.ScanKey=1;	//Start keyboard scanning
flag.Led_On=1;	// Start the blinker

while(1)
	{
	if(flag.keyscan==1)
		{
		u=ScanKey();
		switch(u)
			{
			case 1: flag.Action1=1;	// Queue task 1 for execution
			case 2: flag.Action2=1;	// Queue task 2 for execution
			case 3: flag.Action3=1;	// Queue task 2 for execution
			}
		}

	if(flag.led_On==1) Led_ON();	// If the on-flag is set - turn on
	if(flag.Led_Off==1) Led_OFF();	// If the off-flag is set - turn off

	if(flag.Action1==1) Action1();	// If task 1 needs to run - run it
	if(flag.Action2==1) Action2();	// If task 2 needs to run - run it
	if(flag.Action3==1) Action3();	// If task 3 needs to run - run it
	}
}


A task might look roughly like this (pseudocode):

1
2
3
4
5
6
7
void Action1(void)
{
flag.Action1=0; 			// Task done. Clear the flag
DoSomeThing();			// Doing something useful
if(Some_Event) flag.Action3=1;	// If some condition is met, queue
				// Task 3 for execution
}


As you can see, there's no direct transfer of control between blocks here. Everything is done through flags. To start a task we don't hand it control, we just set the flag that starts it. And on the next iteration of the main loop the task will run, and the flag will be cleared (or not cleared, if the task is set to run cyclically).

Adding functionality takes no particular effort — we just add new flags and new if(flag.****==1) { } sections

Except our LED was supposed to blink in there. What do we do about assorted time delays of different lengths? After all, the flag-based state machine never actually solved that problem. Yes, on its own a flag-based state machine is useless. Until, that is, along comes…

Software timer
A powerful tool for working with time delays. It lets you use a single hardware timer to easily keep track of a whole pile of delays of varying length.

To begin with, you need to configure the system timer so it generates system ticks for us. I've settled on 1ms per tick. So I set up the timer so that it generates an interrupt for me every millisecond. And it's inside that interrupt that we cause all the mayhem.

A software timer is usually built as an array of structures (pseudocode):

1
2
3
4
5
6
volatile static struct					// Global variable
                  {
                  Number;				// Flag number in the flag byte	
                  Time;					// Interval in ms
                  }
                  SoftTimer[Max_Numbers_of_Timer];   	// Timer queue

And inside the timer interrupt we run roughly the following code (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ISR(Timer1)
{
	for(i=0;i!=Max_Numbers_of_Timer;i++)      // Sweep through the timer queue
	{
  	 if(SoftTimer[i].Number == 255 ) continue; // If we hit an empty slot - next iteration

	 if(SoftTimer[i].Time !=0)		 // If the timer hasn't ticked all the way down, tick it once more.
      		{
      		SoftTimer[i].Time --;	 // Decrement the value in the slot if it's not done yet.
      		}
   		else
      		{
      		flags |= 1<<Number ;	 // Ticked down to zero? Set the flag in the flag byte
      		SoftTimer[i].Number = 255;	// And write a placeholder into the slot -- the timer is empty.
      		}
   	}
}

See, it's all simple. We sweep through the array of timer structures, element by element. If the Number field is 255, then obviously that's an empty timer. Since a flag number can never be that (a flag number can only be a number with exactly one set bit). Such a timer slot gets skipped.
If the timer isn't empty, we check the Time field against zero. If it's not zero, we decrement it and move on to the next array element.
And once the timer has ticked all the way down, we go and set the bit in the flag register that sits in the Number field. And on the next pass of the main loop the if(flag.***==1) control construct will fire off the task we need.

The number of timers is determined by the number of time intervals being counted simultaneously. A dozen is usually enough for me. If you make it too few — you might come up short and get a timer-queue overrun. If you build in too much of a margin, the queue takes longer to process inside the interrupt, which lowers the accuracy of the interval counts, and generally creates an unnecessary bottleneck.

You can set a timer from anywhere using the SetTimer function, roughly like this (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void SetTimer(NewNumber,NewTime)
{
InterruptDisable();			// Disable interrupts. Remember atomic access!

for(i=0;i!=Max_Numbers_of_Timer;i++)   	//Sweep through the timer queue. Check whether one like this is already there
	{
	if(SoftTimer[i].Number == NewNumber)	// If there's already an entry with this flag
		{
		SoftTimer[i].Time = NewTime;	// Overwrite its interval
		InterruptRestore();
		return;				// Exit.
		}
	}

for(i=0;i!=Max_Numbers_of_Timer;i++)	//If we don't find one, look for any empty slot
	{
      	if (SoftTimer[i].Number == 255)
	 	{
	 	SoftTimer[i].Number = NewNumber;	// Fill in the flag field
	 	SoftTimer[i].Time = NewTime;		// And the interval field
		InterruptRestore();
		return;					// Exit.
	 	}
      	}
InterruptRestore();	// Restore interrupts to how they were. 
 // here you could return with an error code - no free timers
}


It works in a way that's not complicated either.
On the input we have two values — the time NewTime and the flag we need to plant.
We sweep through our timer queue, looking for a free slot or for a timer for the same event whose term hasn't expired yet. If we find a matching timer — we update it with the new time. If we don't find one, we plant the data into the first free slot. And if we don't find a free slot either, we get a Timer Fail. That's an error, and you need to account for it by building the queue with some margin, or by precisely calculating how many timers you actually need.

Now, armed with knowledge of software timers, let's look at how our blinker will be organized (pseudocode):

1
2
3
4
5
6
7
8
9
10
11
12
13
void LED_ON(void)
{
flag.led_On=0; 			// Done, the flag can be cleared
SET_BIT_LED(); 			// Actually turned something on there
SetTimer(OFF_LED_FLAG,1000);	// Set the flag to turn off in 1s
}

void LED_OFF(void)
{
flag.led_Off=0;		// Done, the flag can be cleared
CLR_BIT_LED();			// Actually turned something off there
SetTimer(ON_LED_FLAG,1000);	// Set the flag to turn on in 1s
}

That's it! See how simple and linear the pieces turn out. And stuffing another dozen and a half chains into this system won't be any trouble. We add flags and conditions, wire up timers, and off we go. What's more, we can set flags both in tasks and in interrupts. And the main loop will crunch right through it.

I think it's clear that in an organization like this, the speed of the main loop is critical. In the sense that any dumb hardware delays like delay_ms(****) are EXTREMELY UNDESIRABLE in it. Because they stall the whole pipeline. Everything should be done through the timer service. If you need very short delays, you can set up a second hardware timer with the same kind of setup, just ticking more often. But that takes some thought and a careful look.

Another drawback of this kind of organization is the rigid order in which operations execute. That is, until we've run through the whole if(flag.***==1) chain, we can't perform any operation again from scratch (although if the chain is fast, this is rarely critical).
This is partly solved by switching to a construct based on a SWITCH-CASE construct that exits back to the root after every operation. In that case we run into another snag — frequently invoked tasks can block the execution of slower ones.

I think the principle is clear, so I won't give a working code example. I'm just too lazy :) If someone goes down that road, I'll happily post their project in the article. The thing is, I prefer a different organization — a dynamic dispatcher. Or just a dispatcher, for short. And it's for that one that I'll give an example of real code. Further examples in the course will be built on it too :)

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Digital devices. Microprocessors and microcontrollers. computer operating principles"

Terms: Digital devices. Microprocessors and microcontrollers. computer operating principles