When I create applications, I often use cooperative multitasking (event driven programs). Almost all tasks can be programmed using this approach. But what to do if long calculations are to be done? For this purpose I wrote small multitasking system, which uses many features of Cortex-M3 MCUs.
To prepare Tasks for run, SysTick initialization function has to be prepared to get required SysTick time.
void SysTick_init (void);
SysTick handler is included in TASKS source code. If user wants to use SysTick, callback function can be defined:
void Tick_update (void);
To use this function, SYSTICK_USER_CALLBACK has to be defined in tasks.h file.
Now we can start TASKS by calling task_init function:
void task_init (void *int_stack, unsigned long stack_size);
This function accepts two parameters: pointer to system stack and size of this stack. System stack is used for all interrupt calls including SVC calls.
Stack defined in linker script remains for main task.
To run another tasks, call this function:
void task_start (task_struct* (*task)(task_struct*), void *stack, unsigned long stack_size, unsigned char slot, task_struct *ctrl);
First parameter i pointer to task function. All task functions have to have this prototype:
task_struct *MY_TASK (task_struct *ctrl);
Parameters stack and stack_size define stack for this new task. Parameter slot defines number of time slices used by task. Last parameter ctrl is pointer to task control structure, which contains all important information about task. Simple example can be found in tasks.zip file.
If change of time slots is needed, this parameter is stored in control structure as variable ctrl->slot.
If task is to be terminated, task_exit function has to be called. Another way is just to return from task function.
void task_exit (task_struct *ctrl);
Task can be suspended and back resumed, i.e. removed from tasks list and then returned back to it.
For this purpose, two functions are prepared:
void task_suspend (task_struct *ctrl);
void task_resume (task_struct *ctrl);
Every task can be delayed for some amount of SysTicks by calling function:
void task_delay (task_struct *ctrl, int tick);
Function can give up current time slice by calling function:
void task_yield (void);
Stack usage can be checked if compiled with TASK_STACK_MARK defined. Function task_stack_usage returns unused stack size for requested task.
For main task, main_task control structure is used. For system stack, NULL pointer is used.
int task_stack_usage (task_struct *ctrl);
ARM Cortex-M3 MCUs can access single bits in RAM as atomic operations - this feature is called bit-banding.
Unfortunately, not all RAM is accessible this way.
For example: LPC17xx have two RAM regions: one is allocated at address 0x10000000 (this RAM has faster access but lacks bit-banding feature),
second bank is allocated at address 0x20000000, which supports bit-banding.
Fortunately, Cortex-M3 has special instructions for exclusive memory access - LDREX and STREX instructions. These instructions allow to safely support read-modify-write access. To get correct behaviour, this functions has to be used everywhere in code to access modified variables. If regular access to these variables is used (without sem_ functions), exclusive access won't work correctly!
For the present, only byte functions are prepared:
unsigned sem_getB (void *ptr); unsigned sem_putB (void *ptr, unsigned data); unsigned sem_setB (volatile void *ptr, unsigned mask); unsigned sem_clrB (volatile void *ptr, unsigned mask);
Function sem_getB returns value from requested address. This function opens exclusive access "window". Returned value can be modified and has to be written back using sem_putB function.
Functions sem_setB and sem_clrB are used to set or clear bits in byte on requested address.
All functions used for writing to memory return success code: if data is written successfully, zero is returned. If another exclusive access was parfomed between LDREX and STREX instructions, no data is written and one is returned. User should either repeat sem_ function or skip it depending on code functionality.
If variable is read only (no modification needed), regular access has to be used.
Three functions are prepared for easier clock setup in LPC17xx MCUs:
void PLL_init (unsigned long freq);
This function sets PLL frequency to selected value. Function selects the best PLL setting to achieve selected value. It uses global variable const unsigned long XTosc;, which should contain crystall frequency for XT oscillator. If freq parameter is zero, internal RC oscillator is used and PLL is swithced off.
Another function is used to return current frequency for selected periphery. List or supported peripherals can be found in Common.h, e.g. clk_cpu for main system clock, clk_uart0 for UART0 etc.
unsigned long getclk (int type);
UART in LPC17xx has fractional divider for more precise baudrate selection. Function UART_iter tries all combinations and uses the best combination.
unsigned short UART_iter (int uartx, unsigned long speed, unsigned short *DIV, unsigned short *MUL);
Parameter uartx is used for getclk call mentioned above, so this value can contain all clk_uartX enums. Speed is requested baudrate. DIV and MUL and return value are output parameters, which are used for UART setup. Example can be found in UART.c in Ux_init function.
UART.c contains UART I/O functions. Because UART0 is used for bootloader, I use this for debugging as well.
Set of functions prepared for this is included in UART0.* files:
void U0_init (unsigned long speed); initializes UART0.
int U0printf (char *format, ...); is used for formatted output on UART0.
void U0putc (char c); prints one character on UART0.
int U0getc (void); reads one characte on UART0. If no character is present, function waits until character is received.
void U0puts (char *s); prints string on UART0.
int U0kbhit (void); returns nonzero, if character is present on UART0.
For debug purposes, exception.* files were prepared. These functions are used in case any unhandled exception or fault handler is called. Function prints contents of all registers, part of stacked values and some system registers helpful for fault inspection. Debug output is provided on UART0 but can be easily changed by editing EX_CHAR macro in exception.h file. UART setup must be done in main application! To use it just link exception.c in your project and set vector table to exception functions. Example can be found in startup.c file.
Example of three tasks (main + 2 tasks) can be found in main.c file.
Download is available here: LPC17xx version and STM32L version.