Lab 4
Lab 4
Le Trong Nhan
Mục lục
Microcontroller Page 5
Page 6 HCMUT - Computer Engineering
CHƯƠNG 1
A cooperative scheduler
1 Introduction
1 }
2 void main ( void ) {
3 // Prepare f o r Task X
4 X_Init ( ) ;
5 while ( 1 ) { // ’ f o r ever ’ ( Super Loop )
6 X ( ) ; // Perform the t a s k
7 }
8 }
Program 1.1: Super loop program
The main advantages of the Super Loop architecture illustrated above are:
However, we get ‘nothing for nothing’: Super Loops consume little memory or pro-
cessor resources because they provide few facilities to the developer. A particular
limitation with this architecture is that it is very difficult to execute Task X at pre-
cise intervals of time: as we will see, this is a very significant drawback.
For example, consider a collection of requirements assembled from a range of dif-
ferent embedded projects (in no particular order):
• The current speed of the vehicle must be measured at 0.5 second intervals.
• The calculated new throttle setting must be applied every 0.5 seconds.
• If the alarm sounds, it must be switched off (for legal reasons) after 20 min-
utes.
• If the front door is opened, the alarm must sound in 30 seconds if the correct
password is not entered in this time.
• The engine vibration data must be sampled 1,000 times per second.
• The master (control) node must communicate with all other nodes (sensor
nodes and sounder nodes) once per second.
We can summarize this list by saying that many embedded systems must carry out
tasks at particular instants of time. More specifically, we have two kinds of activity
to perform:
This is very difficult to achieve with the primitive architecture shown in Program
above. Suppose, for example, that we need to start Task X every 200 ms, and that
the task takes 10 ms to complete. Program below illustrates one way in which we
might adapt the code in order to try to achieve this.
1 }
2 void main ( void ) {
3 // Prepare f o r Task X
4 X_Init ( ) ;
5 while ( 1 ) { // ’ f o r ever ’ ( Super Loop )
6 X() ; // Perform the t a s k (10 ms duration )
7 Delay_190ms ( ) ; // Delay f o r 190 ms
8 }
9 }
Program 1.2: Trying to use the Super Loop architecture to execute tasks at regular
intervals
The approach is not generally adequate, because it will only work if the following
conditions are satisfied:
Microcontroller Page 9
An interrupt is a hardware mechanism used to notify a processor that an ‘event’
has taken place: such events may be internal events or external events.
When an interrupt is generated, the processor ‘jumps’ to an address at the bottom
of the CODE memory area. These locations must contain suitable code with which
the microcontroller can respond to the interrupt or, more commonly, the locations
will include another ‘jump’ instruction, giving the address of suitable ‘interrupt
service routine’ located elsewhere in (CODE) memory.
Please see lab 3 for the more information of this approach.
2 What is a scheduler?
• When the CPU is free, the next waiting task (if any) is executed
Implementation:
• The scheduler must allocate memory for only a single task at a time
Performance:
• Obtaining rapid responses to external events requires care at the design stage
Reliability and safety:
One area of the language with which many ‘C’ programmers are unfamiliar is the
function pointer. While comparatively rarely used in desktop programs, this lan-
guage feature is crucial in the creation of schedulers: we therefore provide a brief
introductory example here.
The key point to note is that – just as we can, for example, determine the starting
address of an array of data in memory – we can also find the address in memory
at which the executable code for a particular function begins. This address can be
used as a ‘pointer’ to the function; most importantly, it can be used to call the func-
tion. Used with care, function pointers can make it easier to design and implement
complex programs. For example, suppose we are developing a large, safety-critical,
application, controlling an industrial plant. If we detect a critical situation, we may
wish to shut down the system as rapidly as possible. However, the appropriate way
to shut down the system will vary, depending on the system state. What we can
do is create a number of different recovery functions and a function pointer. Every
time the system state changes, we can alter the function pointer so that it is always
pointing to the most appropriate recovery function. In this way, we know that –
if there is ever an emergency situation – we can rapidly call the most appropriate
function, by means of the function pointer.
Microcontroller Page 11
1 // −−−−−− P r i v a t e f u n c t i o n p r o to t y pe s −−−−−−−−−−−−−−−−−−−−−−−−−−−−−
2 void Square_Number ( i n t , i n t * ) ;
3
4 i n t main ( void )
5 {
6 int a = 2 , b = 3;
7 / * Declares pFn to be a p o i n t e r to fn with
8 i n t and i n t p o i n t e r parameters ( r e t u r n i n g void ) * /
9 void ( * pFn ) ( i n t , i n t * ) ;
10
11 i n t Result_a , Result_b ;
12 pFn = Square_Number ; // pFn holds address o f Square_Number
13 p r i n t f ( " Function code s t a r t s a t address : %u\n" , ( tWord ) pFn ) ;
14 p r i n t f ( " Data item a s t a r t s a t address : %u\n\n" , ( tWord ) &a ) ;
15 // C a l l ‘ Square_Number ’ in the conventional way
16 Square_Number ( a , &R e s u l t _ a ) ;
17 // C a l l ‘ Square_Number ’ using f u n c t i o n p o i n t e r
18 ( * pFn ) ( b,& Result_b ) ;
19 p r i n t f ( "%d squared i s %d ( using normal fn c a l l ) \n" , a , R e s u l t _ a
);
20 p r i n t f ( "%d squared i s %d ( using fn p o i n t e r ) \n" , b , Result_b ) ;
21 while ( 1 ) ;
22 return 0;
23 }
24
25 void Square_Number ( i n t a , i n t * b )
26 { // Demo − c a l c u l a t e square o f a
27 *b = a * a ;
28 }
Program 1.4: Example of how to use function pointers
2.3 Solution
• An initialization function.
• A single interrupt service routine (ISR), used to update the scheduler at reg-
ular time intervals.
• A dispatcher function that causes tasks to be executed when they are due to
run.
• A function for removing tasks from the scheduler (not required in all appli-
cations).
Before discussing the scheduler components, we consider how the scheduler will
typically appear to the user. To do this we will use a simple example: a scheduler
used to flash a single LED on and off repeatedly: on for one second off for one
second etc.
1 i n t main ( void ) {
2 // I n i t a l l the requirments f o r the system to run
3 System_Initialization () ;
4 // I n i t a schedule
5 SCH_Init ( ) ;
6 //Add a t a s k to r e p e a t l y c a l l in every 1 second .
7 SCH_Add_Task ( Led_Display , 0 , 1000) ;
8 while ( 1 ) {
9 SCH_Dispatch_Tasks ( ) ;
10 }
11 return 0;
12 }
Program 1.5: Example of how to use a scheduler
• We assume that the LED will be switched on and off by means of a ‘task’
Led_Display(). Thus, if the LED is initially off and we call Led_Display() twice,
we assume that the LED will be switched on and then switched off again.
To obtain the required flash rate, we therefore require that the scheduler calls
Led_Display() every second ad infinitum.
• After preparing the scheduler, we add the function Led_Display() to the sched-
uler task list using the SCH_Add_Task() function. At the same time we specify
that the LED will be turned on and off at the required rate as follows:
SCH_Add_Task(Led_Display, 0, 1000);
We will shortly consider all the parameters of SCH_Add_Task(), and examine
its internal structure.
• The ‘Update’ function does not execute the task: it calculates when a task is
due to run and sets a flag. The job of executing LED_Display() falls to the dis-
patcher function (SCH_Dispatch_Tasks()), which runs in the main (‘super’)
loop:
Microcontroller Page 13
1 while (1) {
2 SCH_Dispatch_Tasks () ;
3 }
At the heart of the scheduler is the scheduler data structure: this is a user-defined
data type which collects together the information required about each task.
1
2 t ypedef s t r u c t {
3 // P o i n t e r to the t a s k ( must be a ’ void ( void ) ’ f u n c t i o n )
4 void ( * pTask ) ( void ) ;
5 // Delay ( t i c k s ) u n t i l the f u n c t i o n w i l l ( next ) be run
6 u i n t 3 2 _ t Delay ;
7 // I n t e r v a l ( t i c k s ) between subsequent runs .
8 u i n t 3 2 _ t Period ;
9 // Incremented ( by scheduler ) when t a s k i s due to execute
10 u i n t 8 _ t RunMe ;
11 // This i s a h i n t to s o l v e the question below .
12 u i n t 3 2 _ t TaskID ;
13 } sTask ;
14
• SCH_Add_Task(Function_A, 0, 2);
• SCH_Add_Task(Function_C, 3, 15);
then SCH_MAX_TASKS must have a value of three (or more) for correct operation
of the scheduler.
Note also that, if this condition is not satisfied, the scheduler should generate an
error code.
Like most of the tasks we wish to schedule, the scheduler itself requires an ini-
tialization function. While this performs various important operations – such as
preparing the scheduler array (discussed earlier) and the error code variable (dis-
cussed later) – the main purpose of this function is to set up a timer that will be
used to generate the regular ‘ticks’ that will drive the scheduler.
1 void SCH_Init ( void ) {
2 unsigned char i ;
3 f o r ( i = 0 ; i < SCH_MAX_TASKS ; i ++) {
4 SCH_Delete_Task ( i ) ;
5 }
6 // Reset the g l o b a l e r r o r v a r i a b l e
7 // − SCH_Delete_Task ( ) w i l l generate an e r r o r code ,
8 // ( because the t a s k a r r a y i s empty )
9 Error_code_G = 0 ;
10 Timer_init ( ) ;
11 Watchdog_init ( ) ;
12 }
Program 1.8: Example of how
The ‘Update’ function is involved in the ISR. It is invoked when the timer is over-
flow.
When it determines that a task is due to run, the update function increments the
RunMe field for this task: the task will then be executed by the dispatcher, as we
discuss later.
1 void SCH_Update ( void ) {
2 unsigned char Index ;
3 // NOTE: c a l c u l a t i o n s are in * TICKS * ( not m i l l i s e c o n d s )
4 f o r ( Index = 0 ; Index < SCH_MAX_TASKS ; Index ++) {
5 // Check i f t h e r e i s a t a s k a t t h i s l o c a t i o n
6 i f ( SCH_tasks_G [ Index ] . pTask ) {
7 i f ( SCH_tasks_G [ Index ] . Delay == 0 ) {
8 // The t a s k i s due to run
9 // Inc . the ’RunMe ’ f l a g
10 SCH_tasks_G [ Index ] . RunMe += 1 ;
Microcontroller Page 15
11 i f ( SCH_tasks_G [ Index ] . Period ) {
12 // Schedule p e r i o d i c t a s k s to run again
13 SCH_tasks_G [ Index ] . Delay = SCH_tasks_G [ Index ] .
Period ;
14 }
15 } else {
16 // Not y e t ready to run : j u s t decrement the delay
17 SCH_tasks_G [ Index ] . Delay −= 1 ;
18 }
19 }
20 }
21 }
22
As the name suggests, the ‘Add Task’ function is used to add tasks to the task array,
to ensure that they are called at the required time(s). Here is the example of add
task function: unsigned char SCH_Add_Task ( Task_Name , Initial_Delay, Period
)
The parameters for the ‘Add Task’ function are described as follows:
• Task_Name: the name of the function (task) that you wish to schedule
• Initial_Delay: the delay (in ticks) before task is first executed. If set to 0, the
task is executed immediately.
• Period: the interval (in ticks) between repeated executions of the task. If set
to 0, the task is executed only once
As we have seen, the ‘Update’ function does not execute any tasks: the tasks that
are due to run are invoked through the ‘Dispatcher’ function.
1 void SCH_Dispatch_Tasks ( void )
2 {
3 unsigned char Index ;
4 // Dispatches ( runs ) the next t a s k ( i f one i s ready )
5 f o r ( Index = 0 ; Index < SCH_MAX_TASKS ; Index ++) {
6 i f ( SCH_tasks_G [ Index ] . RunMe > 0 ) {
7 ( * SCH_tasks_G [ Index ] . pTask ) ( ) ; // Run the t a s k
Microcontroller Page 17
8 SCH_tasks_G [ Index ] . RunMe −= 1 ; // Reset / reduce RunMe
flag
9 // P e r i o d i c t a s k s w i l l a u t o m a t i c a l l y run again
10 // − i f t h i s i s a ’ one shot ’ task , remove i t from the
array
11 i f ( SCH_tasks_G [ Index ] . Period == 0 )
12 {
13 SCH_Delete_Task ( Index ) ;
14 }
15 }
16 }
17 // Report system s t a t u s
18 SCH_Report_Status ( ) ;
19 // The scheduler e n t e r s i d l e mode a t t h i s point
20 SCH_Go_To_Sleep ( ) ;
21 }
When tasks are added to the task array, SCH_Add_Task() returns the position in the
task array at which the task has been added: Task_ID = SCH_Add_Task(Do_X,1000,0);
Sometimes it can be necessary to delete tasks from the array. To do so, SCH_Delete_Task()
can be used as follows: SCH_Delete_Task(Task_ID)
1 /*
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
*/
2 unsigned char SCH_Delete_Task ( const t B y t e TASK_INDEX ) {
3 unsigned char Return_code ;
4 i f ( SCH_tasks_G [ TASK_INDEX ] . pTask == 0 ) {
5 // No t a s k a t t h i s l o c a t i o n . . .
6 //
7 // S e t the g l o b a l e r r o r v a r i a b l e
8 Error_code_G = ERROR_SCH_CANNOT_DELETE_TASK
9
10 // . . . a l s o r e t u r n an e r r o r code
11 Return_code = RETURN_ERROR ;
12 } else {
13 Return_code = RETURN_NORMAL;
14 }
15 SCH_tasks_G [ TASK_INDEX ] . pTask = 0 x0000 ;
16 SCH_tasks_G [ TASK_INDEX ] . Delay = 0 ;
17 SCH_tasks_G [ TASK_INDEX ] . Period = 0 ;
18 SCH_tasks_G [ TASK_INDEX ] . RunMe = 0 ;
19 r e t u r n Return_code ; // r e t u r n s t a t u s
20 }
Program 1.13: An implementation of the scheduler ‘delete task’ function
Microcontroller Page 19
3 }
Program 1.14: An implementation of the scheduler ‘go to sleep’ function
Hardware fails; software is never perfect; errors are a fact of life. To report errors
at any part of the scheduled application, we can use an (8-bit) error code variable
Error_code_G
unsigned char Error_code_G = 0;
To record an error we include lines such as:
• Error_code_G = ERROR_SCH_TOO_MANY_TASKS;
• Error_code_G = ERROR_SCH_WAITING_FOR_SLAVE_TO_ACK;
• Error_code_G = ERROR_SCH_WAITING_FOR_START_COMMAND_FROM_MASTER;
• Error_code_G = ERROR_SCH_ONE_OR_MORE_SLAVES_DID_NOT_START;
• Error_code_G = ERROR_SCH_LOST_SLAVE;
• Error_code_G = ERROR_SCH_CAN_BUS_ERROR;
• Error_code_G = ERROR_I2C_WRITE_BYTE_AT24C64;
Note that, in this implementation, error codes are reported for 60,000 ticks (1 minute
at a 1 ms tick rate). The simplest way of displaying these codes is to attach eight
LEDs (with suitable buffers) to the error port, as discussed in IC DRIVER [page
134]: Figure 14.3 illustrates one possible approach.
What does that error code mean? The forms of error reporting discussed here are
low-level in nature and are primarily intended to assist the developer of the appli-
cation or a qualified service engineer performing system maintenance. An addi-
tional user interface may also be required in your application to notify the user of
errors, in a more user-friendly manner.
The basic scheduler presented here does not provide support for a watchdog timer.
Such support can be useful and is easily added, as follows:
1 IWDG_HandleTypeDef hiwdg ;
2 s t a t i c u i n t 3 2 _ t counter_for_watchdog = 0 ;
3
Microcontroller Page 21
19 return 0;
20 }
21 void Watchdog_Counting ( void ) {
22 counter_for_watchdog ++;
23 }
24
2.3.12 Portability
3 Objectives
The aim of this lab is to design and implement a cooperate scheduler to accurately
provide timeouts and trigger activities. You should add a file for the scheduler im-
plementation and modify the main system call loop to handle timer interrupts.
• void SCH_Dispatch_Tasks(void): This function will get the task in the queue
to run.
You should add more functions if you think it will help you to solve this problem.
Your main program must have 5 tasks running periodically in 0.5 second, 1 second,
1.5 seconds, 2 seconds, 2.5 seconds.
5 Demonstration
You should be able to show some test code that uses all the functions specified in
the driver interface.
Specifically set up and demonstrate:
• Then, print the value returned by get_time every time this callback is re-
ceived.
• Before entering the main loop, set up a few calls to SCH_Add_Task. Make sure
the delay used is long enough such that the loop is entered before these wake
up. These callbacks should just print out the current timestamp as each delay
expires.
Note this is not a complete list. The following designs are considered unsatisfac-
tory:
Microcontroller Page 23
• Delivering callbacks in the wrong order
6 Submission
You need to
7 References
8 Link Github
https://github.com/minhgiang1234/lab4-vxl