Super loop software structures, also known as foreground-background systems, provides small microprocessor systems with no operating system, a rudimentary sense of multi-tasking. My original Super Loop notes are written for embedded 8088 assembly language applications. I also have notes for the 68HC12 using 'C' and Metrowerks Code Warrior. These notes are for the Motorola 68HC11, assuming use of an absolute assembler that largely follows Motorola's familiar 68HC11 syntax:
o The free-ware as11 assembler
o Peter Gargano's ASHC11 freely available assembler
o The THRSim assembler THRAss11 product
Jean Labrosse points out that most high-volume low-complexity microcontroller applications use a structure like that in Figure 1. The tasks are collectively referred to as the task level. Interrupt service routines handle asynchronous events in what is called the interrupt level. In Figure 1, task 2 is temporarily suspended by an interrupt service routine (ISR). A super loop is sometimes thought of as cooperative multi-taking for two reasons: (1) each task is responsible for passing control of the CPU to the next task, and (2) following an ISR, control is always returned to the interrupted task.
Figure 1: Task and interrupt levels
The notion of a context is basically a list of things associated with a running program or task and is what the CPU knows at a given moment. At the very least, the context is the list of CPU register values and the stack. An operating system switches between tasks by saving the current context and replacing it with another. In a super loop however, execution in the task level passes from subroutine to subroutine in a simple way, within a single context. A super loop does not provide an actual context switch between tasks. It is for this reason alone that super loop applications are not regarded as truly multitasking.
The DELAY block in Figure 1 is optional, it provides a measure of control over execution time. In general terms, the time to execute one iteration of a super loop, including the DELAY is called the overall loop time. A delay associated with an independent hardware timer can be used to provide super loop applications with particularly reliable timing. Such a delay mechanism is referred to here as a delay gate as it prevents the task code from executing, until the hardware timer counts out.
It is important to understand that invoking an interrupt creates a temporary new context that exists only as long as the ISR executes. When the ISR exits, execution is returned to the suspended task, destroying the ISR context. While the interrupt mechanism provides a tempting means to approximate multitasking, actually using an ISR in this way undermines one reason for having interrupts in the first place; to provide a immediate response to external events. In a nutshell, any time spent in an ISR cannot be used to service other interrupts. To reduce so-called interrupt bloat, it is common to begin the handling of an external event at the interrupt level and then complete the handling at the task level. The actual details of interrupt handling are beyond the scope of this document and will not be discussed much further.
Consider the design of a digital alarm clock. The clock has push buttons that the user manipulates to set the current time, alarm time, display mode, and enable or disable the alarm. Dividing the alarm clock program into tasks helps in that each task is relatively simple and understandable. The following is a list of tasks that might be useful in designing a digital alarm clock:
- Display Task
- Depending on display mode, display either the current time or the alarm time
- Time Update Task
- Updates the current time, when not being set by the user
- Button State Task
- Detect valid button closure and release events
- Alarm Task
- Manage the alarm and alarm state
- Delay Gate Task
- Wait for timer timeout to provide accurate loop timing
Each of the above tasks can be thought of as a mini-program of sorts. Each task has a defined relationship and dependency on other tasks in the system. The delay gate, which is linked to a separate timer deserves special attention. Given that the average time for the tasks in the task level to do work is less than the timer period, the overall loop time will be constant. If the tasks finish work in little time, there is a long delay wait. Otherwise in cases where the tasks consume considerable time, there is a very short delay wait.
Information that is handled by the combination of an ISR and a task, or just by a task will not be fully processed until the intended task is executed. Labrosse defines the term task level response to be the time for a task to respond to an external event. The worst case task level response time is the overall loop time plus the additional time to execute the ISR, if there was one. Hence, for a reasonable task level response time, the overall loop time should be small.
Calling a task simply gives it an opportunity to execute, or in other words, perform useful work. However many tasks do not execute at every opportunity. When a task does execute, it is also said to be triggered. This notion of allowing tasks to forego execution is a good thing as it allows the overall loop time to be small and yet allows tasks to deal with a range of time scales. In the alarm clock example, for the pushbuttons to behave normally an overall loop time in the neighborhood of 100ms or less is needed. The time however is updated once per minute. Hence, given a 100ms overall loop time, the Time Update Task increments the number of minutes, once in every 600 calls.
A simple counter variable provides a means to trigger a task in a periodic fashion. Peatman uses such a counter like that in the figure below. The variable timer is the count and is decremented by one each time the task is called. Once the counter reaches zero the maximum value is assigned to the variable and the task's work is performed.
Figure 2: Flow inside a periodic task
There are many possible mechanisms for triggering a task. A so called flagged task might be triggered by an interrupt service routine (ISR) or another task. Any time spent by an ISR is time that the tasks and other interrupts cannot use, hence an ISR should interrupt for only a brief moment. To reduce ISR bloat, it is common practice to start handling an external event in an ISR and triggering a task to later complete the handling at its first possible opportunity. In recognizing such a request, the task in Figure 3 is triggered once.
Figure 3: Flow inside a one-shot task
A task named LedTask resembling the flowchart in Figure 2 is responsible for controlling the LED. This task performs work once every 122 calls, or approximately every 0.4997 seconds. Each time the LED is toggled, a flag is set to request that a beep sound be made.
A second task named SpkTask produces a beep sound by providing periodic and non-periodic behavior. Work for such a task involves toggling the state of a pin or pins attached to a speaker, over a brief time interval, producing an effect that our ears recognize as a beep sound. In Figure 4, a non-zero flag indicates a request made for a beep sound. The task clears the flag and assigns a new value to the counter. In the situation where the task is not performing work, 'other work' is performed to silence the speaker.
Figure 4: Flow inside a task that makes a beep
The first few lines are just opening comments and equates. Get into the habit of starting all your programs with comments that includes the name of the file, your name, the date, and a brief description of what the program does. Equates are used to assign a constant value to a name, called a symbol. Using symbols helps to make a program more readable.
* blink.asm - Jonathan Hill - 11/7/2004 * Blink and Beep example for the 68HC11E9 * This is an example superloop application * University of Hartford College of Engineering LED_MAX: EQU 122 SPK_MAX: EQU 24 PORTA: EQU $1000 PORTB: EQU $1004 TFLG2: EQU $1025 RTIF: EQU $40 PACTL: EQU $1026
The next few lines define variables. While Buffalo assumes the addresses $0000 to $0041, comprising 66 bytes are for the user stack, this application uses only a small part of it. Since the stack grows downward, it is safe to use the first few bytes for variables.
ORG $0000
*=========================
* The Data Section
*=========================
LedCount RMB 1
LedVal RMB 1
SpkCount RMB 1
SpkVal RMB 1
SpkReq RMB 1
The next section of code executes only once when the program starts, its job is to initialize the stack, variables, and devices. Pin 4 in port A is used to control the LED and pins 0 and 1 in port B are used to control the speaker, which is in series with a current limiting resistor. Since these port pins are outputs, the only device to be initialized is the real time clock, which is configured using the PACTL register.
*=========================
* Program starting point
*=========================
ORG $0100
Start: lds #$41
* Initialize the variables
clr LedVal ; clear store for LED
clr SpkVal ; clear store for speaker
ldaa #LED_MAX ; Initialize LED count
staa LedCount ; to maximim and
clr SpkCount ; turn off the speaker
clr SpkReq ; clear the req flag
* Initialize ports and real time counter
clr PORTA ; clear LED
clr PORTB ; clear speaker
ldaa PACTL ; load control value
anda #$FC ; set RTI rate
staa PACTL ; update the register
The main loop in the program is surprisingly simple, just a series of subroutine calls that repeat. In limiting the dependencies between tasks, there is little danger in shutting down a task by simply commenting it out in the source code.
*===============
* The Main Loop
*===============
Top: jsr LedTask ; run the LED task
jsr SpkTask ; run the speaker task
jsr delay ; wait for timeout
bra Top ; back for more
The task controlling the LED matches the flowchart in Figure 2. In most cases when this code is called, the first bne instruction passes execution to the rts instruction. The exclusive-or function is used to toggle the LED state.
*======================================
* The LED Task
*======================================
LedTask: dec LedCount ; decrement LED count
bne LedDone ; exit from work?
ldaa #LED_MAX ; Reset the LED count
staa LedCount ; to maximum
ldaa #$10 ; LED bit
eora LedVal ; toggle the LED
staa LedVal ; save LED value
staa PORTA ; update output
ldaa #$FF ; Value to make a
staa SpkReq ; make a request
LedDone: rts ; done for now
The task controlling the speaker matches the flowchart in Figure 4.
Notice that by inserting blank lines, to form three blocks of instructions
helps us to read the code and see the correspondence with the flowchart.
Please use techniques like this to make your programs more readable.
*=======================================
* The Speaker Task - Beep when asked to
*=======================================
SpkTask: tst SpkCount ; test the count
bne SpkNext ; update speaker?
tst SpkReq ; test the request
bne SpkStart ; start new beeo?
clr PORTB ; clear the port
rts ; done for now
SpkNext: dec SpkCount ; decrement count
ldaa #$03 ; Speaker bits
eora SpkVal ; toggle the speaker
staa SpkVal ; save new speaker value
staa PORTB ; update the speaker
rts ; done for now
SpkStart: clr SpkReq ; clear the request
ldaa #SPK_MAX ; restart the count
staa SpkCount ; value and
ldaa #$01 ; value to turn
staa SpkVal ; on the speaker
staa PORTB ; perform update
rts ; done for now
This example uses the real time clock to control the overall loop time. The real time clock is really just a counter that sets a flag named RTIF, in a periodic fashion, whether or not the flag is cleared. This program does not use interrupts, rather the delay subroutine is a tight loop that repeatedly inspects the real time clock status. The moment RTIF becomes high, the subroutine clears the flag and exits.
*=============================================
* The delay gate - wait for clock to timeout
*=============================================
delay: ldaa TFLG2 ; get the flag register
anda #RTIF ; mask out the timer flag
beq delay ; and wait if not done
ldaa #RTIF ; if done then clear the
staa TFLG2 ; flag and restart
rts ; done for now
The Achilles heel of super loop structures involves the overall loop timing. All the tasks in a super loop are dependent on each other for timely behavior. For the overall loop timing to be constant, the timing of all tasks must be somewhat predictable. In this example the real time clock is said to be periodic in that the RTIFLG is set every 4.096ms, regardless of when it is cleared. Thus, if the tasks execute in 0.096ms then the delay subroutine waits for 4ms. If the tasks execute in 4.090ms then the delay subroutine waits for 0.006ms. If the total amount of time to execute the tasks is less than 4.096ms then the overall loop time is constant.
In closing, the super loop structure provides a simple outline for developing real time software for low complexity microcontroller based applications. In truth, such a structure is ideally suited to microcontrollers that are limited in memory and don't allow the programmer to have access to the stack. Despite its strengths, the super loop structure has numerous weaknesses which you will discover. Such weaknesses tend to cause super loop structures to take on a spaghetti appearance.
The blink.asm example program beeps the speaker every time the LED changes state. Consider how to change the LedTask subroutine so that a beep is produced only when the LED is turned on. Describe the change in words and submit a printout or handwritten copy of the new LedTask subroutine.
Consider how to change the LedTask task in the blink.asm example so that a beep is produced only when pin 7 in port A, used as an input is high. Describe the change in words and submit a printout or handwritten copy of the new LedTask subroutine.
In the example program, LedTask starts a beep by setting the SpkReq flag. In looking at the SpkTask subroutine it appears that all it does is set a counter value and set the speaker pin values a certain way. To simplify the program you might be tempted to remove the SpkReq global variable and allow the LedTask to directly manipulate 'things' to start a beep. Think of and justify in a few sentences at least one reason why making such a change is a bad idea.
Determine the the amount of time for which a beep is made. Don't bother counting instruction cycles. Assume the time spent waiting in the delay subroutine is very long compared to the rest of the program, so the overall loop time is constant. You may assume that the beep starts at the moment the speaker pins are non-zero and stops when the speaker pins become zero.
What is the significance of the following instruction in the function SpkTask(void) in the blink.asm example program? How would the behavior of the program change if the instruction was removed? Give your answer in a few sentences.
clr SpkReq
The SpkReq flag is used to request that the SpkTask subroutine start producing a beep. In a few brief sentences describe what will happen if SpkReq flag is set while SpkTask is currently producing a beep. In particular describe what happens to the value in SpkReq and for how long a beep is actually made.
In general terms it is considered a bad idea to include a spin lock or delay loop inside a super loop task. The Buffalo subroutines that handle output, described in Part 3 of Introduction to Buffalo are said to be blocking as they must wait for the serial port transmitter to complete work. Assume that each byte being transmitted, also has a start bit and a stop bit, comprising ten bits. The Baud Rate is the the inverse of the Baud Period, which is the time to transmit one such bit. Assume the serial port is communicating at 300 Baud. Approximately how much time does it take to send one character and what percentage difference is this, compared with the overall loop time in the blink.asm example program?
The real time clock is periodic in that the RTIF bit is set every T seconds, regardless of when it is cleared. Consider a super loop structure where the real time clock period is 4.096ms. Suppose that for every iteration the average amount of time for the tasks is 2ms but that one loop iteration out of every one hundred must perform an excessive amount of work, executing in 5ms. Indicate whether or not the overall average loop time will be 4.096ms. Justify your answer with a few sentences and if it helps, include a diagram to support your answer.
In the blink.asm example program, the LedTask subroutine communicates a request that SpkTask produce a beep by setting the SpkReq flag. Once set, SpkTask clears the flag and starts producing a beep. This dependency between tasks is called a simple agreement. One task only sets a flag and the other only clears the flag. Briefly suppose that in certain cases that LedTask needs to revoke a previously made request. Would it be enough for LedTask to clear SpkReq? Use a few sentences to justify your answer.
Please Let me know that you read my web pages.
This document is written for ECE332, the Microprocessor Applications course offered by the ECE department in the College of Engineering at the University of Hartford. Copies of this document may be made gratis for educational purposes only, provided that this statement remains intact. The author retains the copyright on this document.