Author: R.G.Logchies
About an entry in C-programming on small microcontrollers. (uncorrected, provisional)
The project is a practical approach of the implementation of a kind of multitasking on microcontrollers.
Of this design two distributions will, eventually, be available:
In this article I describe the MultitaskingBasic version which is ideally suited for educational purposes and can even be run on a PIC18F248. The added application will demonstrate timing and simple input/output processing.
The MultitaskingCAN version will show the application of this system in a network for signalling alarm situations.
It is an original implementation without the use of existing software and has, by no means, the intention to serve as model or even to be efficient. However, it is used in commercial applications where it has proven to be reliable and practical. In the making, knowledge is collected on the application of C-programming for the PIC18x-series. Comments invited.
As a embedded assembler programmer for over 25 years I always resented programming in C. First of all because I learned to program in assembler first. The problems I solved on small microcontrollers like 8051 and PIC12-16x needed fast, real-time programming. I never saw a C-compiled program that could rival the assembler equivalent of this type of programs. Besides, I had the impression that C-compiled programs are expensive in terms of RAM, ROM, and euros, and impossible to troubleshoot. Maybe it is the awkward structure of an 8051 that stopped me, I never could get past pointer use anyway. But times are a changing for me too... Projects got bigger and I started using the PIC18F258 in a CAN-networked application.
I still get 16 kbytes programmed in a couple of weeks but after peeking and poking in a narrow tunnelview, more and more reasons emerge that force me to adapt to a more worldlike paradigm.
To name a few:
the need to manage larger projects.
sharing intelligence with others.
customer demand for readable code.
the need to manage many projects.
The availability of a free C-compiler for the PIC18F258 made the change.
As it happened I gained some experience in Java programming and got an insight in the world of object oriented hype. I am happy to announce here that we, as embedded programmers for small microcontrollers, are not going to take two steps at one time: this story is about converting from assembler to C.
When you are still with me I solemnly declare that:
we are not going to use any class but, probably, use a collection of items that, given a particular analysis of the problem your program has to solve, are related to one another, and we will call this a structure.
we are not going to instantiate anything but, maybe, use several identical collections of items, called structures, with each its own name, only to emphasize its independant use.
we are not going to subclass anything but, maybe, reuse the source of one of our former projects and adapt its code.
inheritance means that you probably got your intelligence from the genes of your ancestors and not that the behaviour of your program depends on a library you acquired in the past.
On the other end I must admit that, for this type of microcontroller, I will hardly program in assembler anymore for too many reasons to explain in text.
Mission
To share my experience in this article I will describe a multitasking system I designed in all of its basic functions. The complete source code will be available for the Microchip C-compiler MPLAB-C18-v2.30.01-demo-win32 running on MPLAB IDE v6.50. The MPLAB tools are available at microchip.com and the source of this project, MultitaskingBasic.zip, on meetingplace.
for MPLAB IDE v8.33, MPLAB C18 v3.22
The described methodology is used on several computer platforms. The latest application can be experienced on a pc by running the HDC1500-camera protocol simulation which can be found in Interface-projekt.
When you are an assembler programmer and new to C-programming I, humbly, elucidate that you must understand, thoroughly and beyond the end of the tunnel, the following:
a structure is made, not only for us, to collect related data-items sensibly so that we can arrange our program in a way that it will be comprehensable to the outside world (including ourself) even after several years, but because C-compiler builders use this model to take us one step higher in abstraction level. The only thing we need to know is that they call the address of this structure a pointer. All data-items within the structure are accessed via the pointer. Use this pointer everywhere you can and they will do the rest.
You certainly used fifo's something like this:
fifoAlength equ 128
fifoAstorage ds fifoAlength
fifoAstorepoint db
fifoAgetpoint db
In C you make this a structure and you can make several of them: fifoA, fifoB etc. Now you define a pointer (address of) by &fifoA.
the reason why you will not use the term subroutine anymore but function instead is because of the higher abstraction level: you can call a function with one or more parameters and, even better, get a parameter in return. Imagine this parameter being a pointer. Now you will be able to program:
storeCharInFifo(&fifoA,getCharFromFifo(&fifoB));
// or: get a character from fifoB and store it in fifoA
and the compiler will do the hard-work which I don't want to emulate in assembler anymore, only write those two functions.
This article is not intended to teach programming in C but merely to demonstrate the elevated level of program development an engineer will attain by converting to structured programming.
Program elements
Foreground/Background
As the name suggests, the software consists of two main parts: the foreground, which comprises the ISRs that handle asynchronous events in a timely fashion, and the background, which is an infinite loop that uses all remaining CPU cycles to perform less time-critical actions.
Real-time behaviour of this design is only achieved when all relevant signals are catched in time and stored in memory to be communicated to the background for processing. In this design the asynchronous serial data input and one or more high-speed timers are serviced in an interrupt routine. Characters are stored in circular queue's and timers control shared data. The background is responsible for protecting this memory from potential corruption (by disabling interrupts when accessing queue's and data). There is a different characteristic in receiving and transmitting of asynchronous serial data and a corresponding alternative approach to what, in this design, is called SystemI/O. Receiving characters' pace is fully undetermined and dictates the size of the SYS_INPUT_QUEUE in accordance with the background task-level response. Transmitting of characters need not be time critical however, different tasks can output characters and to enable a smooth continuous task processing, output is buffered in SYS_OUTPUT_QUEUE which is emptied in the background or when blocked on a full buffer. Alternatively the outputqueue could be emptied in the interrupt routine but its size is still to be determined carefully to prevent serial output blocking all processing. For instance: the "catalog" command, which has a constant size of characteroutput, could be fit to the size of the outputqueue. However no buffersize is large enough when production of output characters exceeds the pace at which the characters are transmitted to the serial output. See the command display e2prom which uses a strategy for slowing down the production of output in relation to sending the characters in the background.
The background program consists, apart from general purpose housekeeping functions, of a variable list of tasks which are added by executing the corresponding commands. As backgroundprocessing is inherently nondeterministic carefull analysis of timing requirements has to be undertaken to determine the worst-case background task-level response.
In this distribution the background executes the following parts in an endless loop:
handle watchdog triggering.
do system output.
when the main high-speed timer ticks: decrement all currently allocated slow-speed timers until their expiration.
run the command-parser for all statically defined command structures.
the next runlist task.
In the MultitaskingCAN edition of this design, with CAN used as networkbus, analysis will show that handling of CAN-messages could easily be performed in the background, due to fast back-ground processing and slow-speed CAN.
Command
All background actions are performed through commands. This command-model is used to facilitate the user interface with the software system. A command can be given over the RS-232 connection by an operator sitting at a (pc-)terminal. All available commands in the system are clearly defined as strings of text. A command-parser awaits a predefined number of characters of the command (3 in this distribution). A valid command is recognized and waits for arguments or executes. The command-parser (-statemachine) handles an arbitray command-structure in a "multi-tasking" way: it typically checks one character at a time and returns immediately, at worst after executing a valid command. Commands typically have hexadecimal arguments but in the MultitaskingCAN edition we will see commands with binary arguments all packed in an atomic CAN-message. Commands thus act as user-interface (catalog displays all commands in case you forgot ;) but also as network-message. The power-up command takes a list of commands from eeprom to initialize the application.
Multitasking
As opposed to real-time preemptive multitasking the proposed type of multitasking is designed to enable partitioning the application's code in the functional domain. Thus simplifying the design from race conditions, semaphores, mutexes, deadlock and the like. In the background a list of commands is maintained and every command is executed, one after another, at the same priority. Once a command is added to this "runlist" (that is: has added itself to the runlist) it becomes a task that behaves like an autonomous state-machine.
A command must run in the shortest possible time as it is part of the background mainloop and thus influences task-level response. Commands that take a relatively long time to execute can benefit of the inherent state-machine structure of a task. As an example in this distribution the command display e2prom adds itself to the runlist then displays one line at a time as state-machine and deletes itself from the runlist after completion.
Communication with a task is performed by setting its variables by executing the command either through the command-parser or by other tasks. The programmer is free to use dedicated queue's for inter-task communications however this must be done, statically, at design time. Alternatively a pool of eventqueue's could be constructed at a cost. As we will see in the next part about timers a comparable approach is performed to facilitate dynamically allocating slow-speed timers.
Timers
Timers (as well as counters) are frequently used in typical applications for microcontrollers. To release the foreground of time-consuming handling of numerous timers only one maintimer ticks in the interrupt routine. A hardwaretimer of the controller is used with a repetitionrate slower by an order of magnitude in comparison with the background loop frequency. A lightweigth form of memorymanagement is used by reserving a part in RAM-memory as a pool (array) for a maximum number of slow-speed timers. Initially all timers are available(free), indicated by a boolean. Every task can acquire free timers from this pool and initialize it with a time. In the background the maintimer is used to decrement all acquired slow-speed timers.The timers are organized as a linked list which is run through, every time the maintimer advances one tick, thereby decrementing all timers in the list until zero. Tasks use this value as a timout and can restart the timer or return it to the pool.
Main data structures
MACHINE: unites texts to parse and output upon execution as well as Taskdata. i.e.: "catalog\r\n", "Catalog:", &CatalogData
TASKDATA: defines type and number of arguments of the command, the functions to call upon entering the command and, once operating as a statemachine in the runlist, the statefunctions.
i.e.: struct Taskdata CatalogData = {0,HEXARGUMENTS,enterCatalog,taskDelete};
For clarity reasons the parsertexts are outside of this structure and united in the machine stucture.
COMMAND: as a command is given in a "multitasking" way, this structure is responsable of collecting valid command-characters, counting and storing arguments and ultimately delivering this information to the executeCommand function.
CHAR_QUEUE: circular buffer, mainly used for isolating foreground and background processing of characters.
STACKLINE: contains information about a running task. Stacklines are organized as linked list and every iteration of the backgroundloop executes a task from this list.
BYTETIMER only two bytes: the downcounting value and an initial value for restart.
BYTETIMERS: structure contains one BYTETIMER and info to enable creating a linked list of BYTETIMERS.
Implementation
Initialization
Initialization comprises the following items:
TBLPTRU = 0x00;
timer_a is initialized with a value read from eeprom and started.
The memoryspace for the list of bytetimers is initialized by setting the free-flag for every timer.
The function SysIo() is called to initialize the character queue's for input/output as well as the command structure asyncCommand, used for parsing commands from the async connection.
InitializeBoard() defines port i/o, initializes the usart and timer0 and then enables interrupts. The serial input interrupt is enabled after the first (and every) call to system input. At this moment a startupmessage is displayed and timers start ticking.
The general purpose command structure runlistCommand, is then used for executing the commands: "power-up" and "start".
The commandparser is entered with a pointer to a commandstructure and a character to parse. The commandstructure serves as a "memory" for the characters given as command and parameters. Every call to this function a new character is checked versus the array of available commands m[].
When all no_of_command_chars are checked and a valid command is found than the parameters are checked and upon completion the commandstructure is handed over to the executeCommand-function. This function uses the menu_index of the command, found by the parser, to call the appropriate enter_function.
Commands are defined in the file MainMachine.c where MainMachine.h numerates the command labels, preferably in the same order as the definition in MACHINEm[].
The Taskdata structure defines the enter_func_array[] which is an array of functionpointers that, at most, consists of two members: the first enter_function is called upon entering a command without any parameters and the second function is called when one or more parameters are given with the command.
This structure also defines the task_func_array[] with functionpointers to the different states of a task. The following types of command can be programmed:
The runlist consists of an array of STACKLINE's organized as a linked list with functions to emulate stack behaviour (the runstack). A command becomes a task when a free STACKLINE is found and initialized with the command_list_index of the command which serves as an index to the proper task_func_array pointers to statefunctions, then "pushed" on to the runstack. The runmachine executes one task from the list every background iteration. The position of the task in the runstack depends on the order of execution of commands and can not be determined at design time. To facilitate communication between a running task and its command at run time, a copy of the pointer to the task's STACKLINE needs to be kept in the task. This pointer can than be used to set the stackline_mode or other variables in a commands' enter_function while these variables can be checked in the task.
Once a task is added to the runlist, it can only be deleted by the task itself. This is to ensure the proper clean-up of any tasks' variables and/or to free dynamically acquired resources.
#define STACKLINE struct STACKLINE
STACKLINE
{
STACKLINE *next;
BOOL occupied;
BYTE stack_data; // command_list_index
STATE_THREAD stackline_state; // task-state
THREAD_MODE stackline_mode; // general purpose variable
};
.
.
// RUNMACHINE /////////////////////////////////////////
if ((running_flag) && (runstack_head))
{
// command_list_index: function call of current runline-state
m[current_runline->stack_data].task_data->task_func_array[current_runline->stackline_state]();
if (current_runline->next == 0) current_runline = runstack_head;
else current_runline = current_runline->next;
}
// END OF RUNMACHINE
.
.
Timers
Timers play an important role in this design and thus merit an extended explanation. There are several levels of handling various timing elements that are at the programmers' disposal.
TMR0 enters the interrupt routine at 10 msec intervals. (see intrptlow.c)
xtal_timer is incremented in the interrupt routine every 10 msec. This value is used to enable comparing the xtal frequency of different nodes in a network as CAN performance heavily depends on this frequency.
timer_a is a maintimer and decremented in the interrupt every 10*(100/TICKS_PER_SECOND) msec. At TICKS_PER_SECOND=50, timer_a is decremented every 20 msec. timer_a is initialized by the command "timer" (see CMDtimer.c) and, normally, this command can be run at power-up with a value of 5. The expiration of timer_a is tested in the background and serves as signal to decrement all acquired slow-speed timers at a pace of 0,1 sec (5*20 msec) when timer-command 5 has been executed.
timer_a_counter is incremented at every expiration of timer_a and this value is used in the command "led" (see CMDled.c) to realize some blinking led patterns.
bytetimers are slow-speed timers that dynamically can be aquired from a pool. All acquired bytetimers in this pool are decremented at the expiration of timer_a. See command "beep" for an example of a beepercontrol-task with adjustable duty-cycle and # of beeps.
// Timer defines //////////////////////////////////////
#define BYTETIMER struct BYTETIMER
BYTETIMER
{
TIMEBYTE time; // time duration
TIMEBYTE timing; // time downcounting
.
#define BYTETIMERS struct BYTETIMERS
BYTETIMERS
{
BYTETIMERS *next;
BOOL occupied;
BYTETIMER byteTimer;
};
.
.
BYTETIMER timer_a; // timer_a is the maintimer, decremented in the foreground
BYTETIMERS *bytetimers_head;
.
// Timer routine in background loop ///////////////////
// timer_a == 50 ticks/sec; time == 0x05 -> timer_a_counter_VALUE == 5/50 == 1/10 sec
if (bytetimer_elapsed(&timer_a))
{
start_bytetimer(&timer_a);
timer_a_counter++;
decrement_bytetimers(&bytetimers_head);
}
// END OF Timer routine ///////////////////////////////
.
.
// Timer subroutines //////////////////////////////////
void start_bytetimer(BYTETIMER *timer) {timer->timing = timer->time;}
BOOL bytetimer_elapsed(BYTETIMER *timer) {return !(timer->timing);}
void decrement_bytetimers(BYTETIMERS **headp)
{
BYTETIMERS *p;
for (p = *headp; p ; p = p->next)
{
if (p->byteTimer.timing) p->byteTimer.timing-- ;
}
}
Get started:
The following items need to be addressed to align this distribution to the users' hardware and to initialize a simple application.
The xtalfrequency used in the hardware need to correspond with the macro: XTAL_xxMC in the file application.h. Only 3 frequency's are predefined. If any other xtal-value is used then a "Find in Project Files..." of this macro can help to determine the places to change timing values. The timers and baudratevalues will be affected by the xtalfrequency.
Input/output controlled in the example application comprises 2 inputs for switches and two outputs for a led and a beeper. The addresses are defined in the file in_out.h and must be adapted to the users' hardware. The hardware definitions for the async serial port are also set in this file.
When the code is compiled and programmed in the hardware, initially the data-eeprom must be cleared to all ff. Then the default baudrate for the serial port will be 57k. The hardware should start and display a version message and a prompt. The command "cat" will be helpfull in exploring the properties of the software. The first thing to do is to set the maintimer by: "tim05". Commands only need 3 characters and parameters are essentially in hexadecimal notation but must always be entered as two characters and no spaces. The command: "led01" should put itself in the runlist which is started by: "sta". As a result the led should start blinking.
To initialize an application, these commands can be put in eeprom and are then executed at power-up. Also some important constants can be put in eeprom. See the file eeprom.h. At a minimum the maintimer value (05) should be put at address MAIN_MASTERTIMER_VALUE_ADDRESS and at MASTER_STARTUP_LIST the initializing commands can be placed. If any command is put here then the start command is executed automatically. The "led01" command is put in eeprom by placing 0d, 01, ff at addresses 40h, 41h, 42h. (see the explanation of the startup command datablock in file eeprom.h) Commands are identified by their label defined in the file MainMachine.h.
Application
Applications can be realized by writing several tasks that may or may not "inter-communicate". Every task has a specialized functionality and is coded in a separate file. This way applications are easily maintained and projects can "borrow" from former work.
To demonstrate the use of this multitasking system the following application tasks, all of type B, are implemented in MultitaskingBasic:
led: every time timer_a_counter equals the parameter given with the command, a led is set. A parametervalue of 7 deletes this (and other) task.
beep needs two parameters for setting the pace and dutycycle of a beeper and the number of beeps to display. A BYTETIMER is dynamically acquired in this task. This task deletes itself after the number of beeps given.
watch this task checks an input and displays to the serial port the number of background iterations during setting the input to ground. Only a signed wordcounter is used, so count until 32k...
input also checks an input and starts the beep task, upon activation of the input, with a number of beeps given as parameter in this command.
To initialize an application where all these tasks are started at power-up, the eeprom can be set as:
40: 0d 01 ff 0e 14 10 ff 0f ff 10 05 ff ........
Add new commands
To add new commands the best way to proceed is to find a command with comparable behaviour (type, number of arguments, use of resources). Then:
copy the CMDxxxx-file and add the edited and renamed version to the project.
add the command to MACHINE m[] in file MainMachine.c
add the commandlabel and adjust the MAIN_MACHINE_SIZE in file MainMachine.h
recompile and optionally adjust the MASTER_STARTUP_LIST.