16 KiB
Week 1.4
Assinment 4.1
The teacher has built a simple preemptive OS that is still missing some important features. In these assignments you’ll implement some extra features and gain a bigger understanding in how an OS operates. Next week we’ll start using a fully developed RTOS with all necessary features for a production environment.
A) Download the project VersdOS.zip.
B) Import this project, Open and Finish. Build and debug the project. The LEDs should blink.
C) Browse through the code and especially make sure you understand the scheduling process and the context switch.
D) Measure the periods at which the LEDs toggle using a logic analyzer and explain why this is not 2× but 8× theblocking_delaytime.
In the Code it the values for blocking delay are 100, 200, 400, 800 for green, orange, red, blue. The blocking_delay function wait for this number of SysTicks, witch is set to 1ms. I measure delays of 0.4s, 0.8s, 1.6s and 3.2s. This is indeed 4x slower of what I would expect on first glance. This delay is because this is a preemptive scheduler, the other tasks run in between. There are 4 task all with the same priority, so each task takes 4 time longer.
Assignment 4.2
A) implement a non-blocking delay so that a task can request the OS to be kept out of the scheduling loop for a certain number of system ticks. The scheduler should remain preemptive and perform round-robin on all the available(not delayed) tasks. Once the requested number of system ticks have passed, the scheduler should include the task in the selection process. You may use the
taskYield()function to let the OS know a task is ready to be switched out.
To allow for a non blocking delay the OS should keep track of the time the to wake it up on the correct time. I did this by adding the new task state SLEEPING_DELAY to the enum.
There already is an counter in the task struct. but no function that decrements this counter. So I added the following function the the OS.
void decrement_sleeping_delay()
{
for (usize_t i=0; i < MAX_TASKS; i++)
{
if (taskList[i].state == SLEEPING_DELAY)
{
if (taskList[i].counter == 0)
{
taskList[i].state = READY;
}
else
{
taskList[i].counter--;
}
}
}
}
This function is added to the SysTick_Handeler before the scheduler is run. So that if the counter is 0 the decrement_sleeping_delay makes the task ready again, and is directly available for to be scheduled. This makes a delay of 0, wait for the next SysTick.
The SysTick_Handeler now look like the following
void SysTick_Handler(void)
{
SysTick_flag = true;
//decrement counter for task in SLEEPING_DELAY
decrement_sleeping_delay();
//select the next task
taskToExecute = schedule();
//request context switch
SCB->ICSR |= (1<<28);
}
Before stating to write the delay function for the tasks, in the schedule funciton the line that sets the current task to ready sould be removed (line 163 in VersdOS.c). If the state is chanced during execution of the task, this line reset is at the next try to reschedule this state back to READY so the state will never actually be changed.
As last the delay function itself.
void delay(uint32_t ticks)
{
currentTask->state = SLEEPING_DELAY;
currentTask->counter = ticks;
taskYield();
}
Now all the line extern void delay(uint32_t ticks); can be added to the begin the main.c and all the blocking_delay calls can be replace by delay.
Result
The compete code can be found at /report-2/week_1.4/assignment_4.2/.
This implementation works, only the delays are a bit off. To make it a nice counter again all delays should be decremented by one (figure \ref{42_logic} shows the output with the original timings, not the decremented ones). This is because with my implementation there is a delay added until the next SysTick after delay is called. blocking_delay(0) instantly returns without waiting for an SysTick (code below). It check if the counter is zero before waiting. For my delay(0), it does wait for a SysTick and checks the counter after. To me the added delay make more sens. My delay is between number of the arguments in SysTick up to one extra, depending how long until the first SysTick is.
void blocking_delay(unsigned int ticks)
{
while (ticks != 0) {
extern bool SysTick_flag;
while (SysTick_flag == false); // busy wait
SysTick_flag = false;
ticks--;
}
}
Assignment 4.3
A) Try changing the SysTick period to 2 ms within a task. A task can change the SysTick period to any value it wants, but you can imagine this isn’t desired behavior.
I changed the toggleBlue task to the following
void toggleBlue(void)
{
while(1)
{
GPIOD->ODR ^= 1 << BLUE;
delay(800-1);
SysTick->LOAD = 2 * CLOCK_FREQ_IN_KHz - 1;
}
}
B) Implement unprivileged mode for the tasks. When succeeded the previous test should fail and be caught in an infinite ISR fault-handler.
With Ctrl+F in some books I only found the nPRIV bit in the control register (PM0214 - STM32 Cortex® -M4 MCUs and MPUs programming manual) as information how to switch privilege. I assume the interrupts used by the OS automatically go to privileged mode, and I only need to go to unprivileged mode before starting a task.
The control register is a special register to it need to be set and read with special instructions. With MSR (page 187 of PM0214) a special register, like control, can be set and MRS (page 186 of PM0214) it can be read. With this information I added the following function to VersdOS_ll.s to set the nPRIV bit.
.global toUnprivileged
toUnprivileged:
mrs R0, CONTROL // copy control register to R0
ORR R0, R0, #1 // clear bit 0 (nPRIV)
msr CONTROL, R0 // copy R0 to control register
bx lr
And called this function in the PendSV_Handeler
void toUnprivileged(void);
__attribute__((naked)) // No function entry and exit code
void PendSV_Handler(void)
{
//Push {R4-R11} context to PSP
pushRegistersToCurrentPSP();
//Save the new stack pointer after the push
currentTask->stack = readPSP();
currentTask = taskToExecute;
//Load the new stack pointer from (new) currentTask
writePSP(currentTask->stack);
//Pop {R4-R11} context from PSP
popRegistersFromCurrentPSP();
toUnprivileged(); // <--- this line was added
returnToPSP();
}
The resulting code can be found at /report-2/week_1.4/assignment_4.3/
And tested it.
One step after the 'malicious' code, it jumps to this infinite loop.
and when the 'malicious' code is removed, it functions as normal.
Assignment 4.4
Tasks are no longer able to access the SysTick peripheral. It is time to implement a system call using the SVC interrupt mechanism. The
taskYield()function would also classify as a system call. Write a system call function that will allow a task to change the SysTick period to a value between 1 and 10 ms . The system call should utilize the SVC mechanism by passing a different number than thetaskYield()call. Make sure both calls keep working! Within the SVC interrupt service routine you will have to find the used SVC instruction using the PC value and look at the LSB to determine the system call number. You can also utilize the tasks’ stack to find the passed SysTick period parameter. When this parameter is invalid the system call should simply return.
To find the instruction I first added the function to that contains the different SVC insurrections. I did this by adding the following code to VersOS.c and add the corresponding lines to VersOS.h.
void setSysTick_10ms(void)
{
asm(" svc #2");
}
void setSysTick_1ms(void)
{
asm(" svc #3");
}
And made use of them in the toggelBlue task by updating it to the following.
void toggleBlue(void)
{
while(1)
{
GPIOD->ODR ^= 1 << BLUE;
delay(800-1);
setSysTick_10ms();
GPIOD->ODR ^= 1 << BLUE;
delay(800-1);
setSysTick_1ms();
}
}
Then added an break-point at each SVC instruction and noted PS and PC. Then stepped to the SVC_Handler, and looked at the memory at the address stored in the PSP register.
task:
PC: 0x08000660
SP: 0x200005ac
SVC_Handler:
PSP: 0x20000588
0x20000588 : 0x20000588 <Hex>
Address 0 - 3 4 - 7 8 - B C - F
20000580 1F030000 9C000020 1F030000 01000000
20000590 1F030000 9C000020 0C000000 F9060008
200005A0 62060008 00020001 B0050020 B0050020
200005B0 07000000 FDFFFFFF 0030012B 11D10260
200005C0 08230C68 E40348BF 0123F2D4 0C68E406
200005D0 48BF0523 EDD40C68 14F0EF0F E5D00623
200005E0 E7E79DF8 00004A68 52085200 4A6032BD
200005F0 78B40822 784B8DF8 14020020 00000000
20000600 00000000 3C060020 00000000 00000000
20000610 00000000 00000000 00000000 01000000
20000620 02000000 03000000 0C000000 FDFFFFFF
20000630 28020008 00020001 23020008 E8FF0120
20000640 11D11460 08251E68 F60348BF 0125F2D4
20000650 1E68F606 48BF0525 EDD41E68 16F0EF0F
20000660 E5D00625 E7E79DF8 0020082A 47D15A68
20000670 22F44072 5A605A68 5A605A68 42F00102
In the memory map I found a littleendien encoded address witch was very close to the tasks PC at 0x200005A0. This could very well be the place the PC is stored. To test this I updated the SVC_Handler to the following.
void SVC_Handler(void)
{
uint32_t* sp = readPSP();
// get PC location
sp = sp + 6;
// get PC
uint16_t* pc = (uint16_t*)*sp;
// get SVC intruction
uint16_t instruction = *(pc - 1);
if ((instruction & 0xFF00) != 0xDF00) {
return;
}
uint8_t svc_id = instruction & 0x00FF;
switch (svc_id)
{
case 1: // taskYield
taskToExecute = schedule();
SCB->ICSR |= (1<<28);
break;
case 2: // setSysTick_10ms
SysTick->LOAD = 10 * CLOCK_FREQ_IN_KHz - 1;
break;
case 3: // setSysTick_1ms
SysTick->LOAD = 1 * CLOCK_FREQ_IN_KHz - 1;
break;
}
}
This code worked. The resulting code can be found at /report-2/week_1.4/assignment_4.4/.
assignment 4.5
The current scheduler implements a round-robin algorithm. In a real-time situation it would be necessary to give tasks different priorities in order to make important deadlines. A) In reality tasks have more important jobs to do than just toggling a LED. This means tasks will take time. Use the original blocking delay function to give tasks a realistic waiting time in a random amount of milliseconds. In order to verify timing requirements with a logic analyzer, you can toggle the LED at the end of the busy waiting period.
The LED is turn on while the task is await, and of when it is sleeping.
| green | orange | red | blue | |
|---|---|---|---|---|
| blocking_delay | 100 ms | 200 ms | 300 ms | 400 ms |
| delay (sleep) | 100 ms | 200 ms | 300 ms | 400 ms |
void toggleGreen(void)
{
while(1)
{
GPIOD->ODR |= 1 << GREEN;
blocking_delay(100);
GPIOD->ODR &= ~(1 << GREEN);
delay(100-1);
}
}
void toggleOrange(void)
{
while(1)
{
GPIOD->ODR |= 1 << ORANGE;
blocking_delay(200);
GPIOD->ODR &= ~(1 << ORANGE);
delay(200-1);
}
}
void toggleRed(void)
{
while(1)
{
GPIOD->ODR |= 1 << RED;
blocking_delay(300);
GPIOD->ODR &= ~(1 << RED);
delay(300-1);
}
}
void toggleBlue(void)
{
while(1)
{
GPIOD->ODR |= 1 << BLUE;
blocking_delay(400);
GPIOD->ODR &= ~(1 << BLUE);
delay(400-1);
}
}
B) Implement a priority based scheduling algorithm. When adding tasks you should be able to pass the priority. When there are multiple tasks available with the same priority a round-robin algorithm should be applied. Test this functionality for multiple tasks with and without non-blocking delays.
The priority is already passed with the addTaskToList function and this is saved in the task struct. I did update the priorities to have green as the highest priority, then orange, red and blue has the lowest priority.
addTaskToList(toggleGreen, 128, 2);
addTaskToList(toggleOrange, 128, 3);
addTaskToList(toggleRed, 128, 4);
addTaskToList(toggleBlue, 128, 5);
C) The round-robin period thus far equaled the SysTick period. Implement the ability to have a task-dependent round-robin period. E.g. if taskA and taskB have equal priorities, TaskA will run 2 SysTicks, taskB will need 5 SysTicks. This period should also be something passed at task creation. Verify this functionality with a logic analyzer.
I added a new pointer nextTaskPtr to store the highest priority task that is ready. The do while loop is updated to first filter all task tasks that aren't ready than if there is no task yet in nextTaskPtr it should not compare priorities. Otherwise check if it has a higher priority than the current nextTaskPtr, if so replace it.
task * schedule()
{
task* nextTaskPtr = NULL;
task* tempTaskPtr = currentTask;
task* idleTaskPtr = &taskList[IDLE_TASK];
int teller=0;
do
{
tempTaskPtr++;
if( (tempTaskPtr-1) == idleTaskPtr || tempTaskPtr == idleTaskPtr)
{
//since idle task is the last in the list, we've reached the end
//and need to continue at the beginning
tempTaskPtr = &taskList[0];
}
if (tempTaskPtr->state != READY)
{
continue;
}
if (nextTaskPtr == NULL)
{
nextTaskPtr = tempTaskPtr;
continue;
}
if (tempTaskPtr->priority < nextTaskPtr->priority)
{
nextTaskPtr = tempTaskPtr;
continue;
}
} while (teller++ < MAX_TASKS);
//if no task was found
if(nextTaskPtr == NULL)
{
//idle task
nextTaskPtr = idleTaskPtr;
}
return nextTaskPtr;
}
The full code is available at /report-2/week_1.4/assignment_4.5/.
Testing
Green (D0) has the highest priority, so it starts. Orange (D2) follows after the 100ms green is taking, orange should take 200ms but green is correctly halting orange.
It is also clearly viable that red (D1) and blue (D3) have lower priorities. It seems to work correctly.






