freertos critical section protection

Posted by Pyro4816 on Mon, 31 Jan 2022 05:48:45 +0100

Fundamentals of interruption

Nesting:

Nested vector interrupt controller NVIC (nested vector interrupt controller) is tightly coupled with the kernel. It provides the following functions: nested interrupt support, vector interrupt support, dynamic priority adjustment support, greatly shortening interrupt delay and shielding interrupt.

All external interrupts and most system exceptions support nested interrupts. Exceptions can be given different priorities, and the current priority is stored in the special field of xPSR. When an exception occurs, the hardware automatically compares the priority of the exception with the current exception priority. If the priority of the exception is higher, the processor will interrupt the current interrupt service routine (or ordinary program) and serve the new exception (preemption immediately).

If the priority group setting makes the interrupt nesting level very deep, make sure that the main stack space is enough. The exception service program always uses MSP, and the capacity of the main stack should be the amount required when nesting the deepest.

Priority:

CM3 supports interrupt nesting, so that high priority exceptions will preempt low priority exceptions.

There are three system exceptions: reset, NMI and hard fault. They have a fixed priority, and the priority number is negative, higher than all other exceptions. The priority of all other exceptions is programmable (but not negative).

CM3 supports three fixed high priorities and programmable priorities up to 256 levels, and supports 128 levels of preemption. However, most CM3 chips actually support fewer priority levels, such as level 8, level 16, level 32, etc.

Cut out several low-end significant bits expressing priority, so as to reduce the number of priority levels. If more bits are used to express priority, the number of priority levels increases, and more gates are required, resulting in more cost and power consumption.

Three bits are used to express the priority. The structure of the priority configuration register is shown in the figure below. The eight priorities that can be used are: 0x00 (highest), 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0 and 0xE0.

In order to make the preemption function more controllable, CM3 divides 256 levels of priority into high and low levels, which are preemption priority and sub priority respectively. Preemption priority determines preemption behavior. When more than one exception with the same preemptive priority is suspended, give priority to the exception with the highest sub priority (sub priority handling housekeeping).

Priority grouping provisions: the sub priority level shall be at least 1 bit. Therefore, the preemption priority is up to 7 bits, and there is only 128 levels of preemption at most.

The following figure shows that only three bits are used to express the priority. Grouping from bit5, four levels of preemption priority are obtained. There are two sub priorities inside each preemption priority.

The following figure shows the 3-bit priority, grouped from bit 1, (although [4:0] is not used, grouping from them is allowed).

The application interrupt and reset control register (AIRCR) (address: 0xE000_ED00) is as follows.

Suspension and de suspension of interruption:

When an interrupt occurs, it is processing the same level or high priority exception, or it is masked, then the interrupt cannot be responded immediately. The interrupt is suspended.

You can read the suspended state of the interrupt through the interrupt setting suspended register (setend) and interrupt suspended clear register (clrend), and you can also write them to suspend the interrupt manually.

Tail ‐ training:

When the processor responds to an exception, if an exception with high priority occurs again, the current exception is blocked and the exception with high priority is executed instead. After the exception execution returns, when the system handles the suspended exception, if it pops the POP first and then pushes the POP back, it is a waste of CPU time.

Therefore, CM3 does not POP these registers, but continues to use the results of the last exception that has been pushed. As shown in the figure below.

Late high priority exception:

In the stage of stacking, when its service routine has not been executed, if a request for high priority exception is received at this time, the service routine of high priority exception will be executed after stacking. If the high priority exception comes so late that the instruction of the previous exception has been executed, it will be handled as normal preemption, which will require more processor time and additional 32 bytes of stack space. After the high priority exception is executed, the previously preempted exception is executed by tail biting interrupt.

On interrupt and off interrupt instructions in cortex-m

Critical segment: a code segment that cannot be interrupted during execution (a code segment that must run completely and cannot be interrupted). Generally, critical segments are used when operating on global variables. When a task accesses a global variable, if it is interrupted by other interrupts, it changes the global variable. When it returns to the previous task, the global variable is not what it was at that time. This situation may lead to unexpected consequences.

When the critical section is interrupted: system scheduling (PendSV interrupt is also generated in the end); External interrupt.

freertos needs to close the interrupt when entering the critical segment code, and open the interrupt after processing the critical segment code.

First look at the following code.

__asm void prvStartFirstTask( void )
{

	PRESERVE8

	/* In Cortex-M, 0xE000ED08 is SCB_ The address of the vtor register,
       Inside is the starting address of the vector table, that is, the address of MSP */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* Sets the value of the main stack pointer msp */
	msr msp, r0
    
	/* Enable global interrupt */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* Call SVC to start the first task */
	svc 0  
	nop
	nop
}
__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
    
    PRESERVE8

	ldr	r3, =pxCurrentTCB	/* Load the address of pxCurrentTCB to r3 */
	ldr r1, [r3]			/* Load pxCurrentTCB to r1 */
	ldr r0, [r1]			/* Load the value pointed to by pxCurrentTCB to r0. At present, the value of r0 is equal to the top of the first task stack */
	ldmia r0!, {r4-r11}		/* Take r0 as the base address, load the contents of the stack into r4~r11 registers, and r0 will be incremented at the same time */
	msr psp, r0				/* Update the value of r0, that is, the stack pointer of the task, to psp */
	isb
	mov r0, #0 / * set the value of r0 to 0*/
	msr	basepri, r0         /* Set the value of basepri register to 0, that is, all interrupts are not masked */
	orr r14, #0xd           
	bx r14                  
}
__asm void xPortPendSVHandler( void )
{
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

    /* When entering PendSVC Handler, the environment in which the last task was run is:
       xPSR,PC(Task entry address), R14, R12, R3, R2, R1, R0 (formal parameters of the task)
       The values of these CPU registers will be automatically saved to the task stack, and the rest r4~r11 need to be saved manually */
    /* Get the task stack pointer to r0 */
	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* Load the address of pxCurrentTCB to r3 */
	ldr	r2, [r3]                /* Load pxCurrentTCB to r2 */

	stmdb r0!, {r4-r11}			/* Store the values of CPU registers r4~r11 to the address pointed to by r0 */
	str r0, [r2]             /* Store the new stack top pointer of the task stack to the first member of the current task TCB, that is, the stack top pointer */ 
	
    stmdb sp!, {r3, r14}        /* Temporarily push R3 and R14 onto the stack */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY / * enter critical section*/
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* Call the function vTaskSwitchContext to find a new task to run, and realize task switching by pointing the variable pxCurrentTCB to the new task */ 
	mov r0, #0 / * exit critical section*/
	msr basepri, r0
	ldmia sp!, {r3, r14}        /* Restore r3 and r14 */

	ldr r1, [r3]
	ldr r0, [r1] 				/* The first item of the currently active task TCB saves the stack top of the task stack, and now the stack top value is stored in R0*/
	ldmia r0!, {r4-r11}			/* Out of stack */
	msr psp, r0
	isb
	bx r14                      
	nop
}

From the above code, you can see the following instructions.

cpsie i
cpsie f
msr	basepri, r0

In order to quickly switch interrupt, CM3 has specially set CPS instruction, which has four usages.

CPSID I ;PRIMASK=1, ;Off interrupt
CPSIE I ;PRIMASK=0, ;Open interrupt
CPSID F ;FAULTMASK=1, ;Guan anomaly
CPSIE F ;FAULTMASK=0 ;Abnormal opening

As you can see, the above instruction is also the PRIMASK and FAULTMASK registers of control.

As shown in the figure below, the global interrupt can be turned on or off through the CPS instruction.

basepri is the interrupt mask register. In the following setting, interrupts with priority greater than or equal to 11 will be masked. It is equivalent to turning off the interrupt and entering the critical section.

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 
msr basepri, r0
/*
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	191   /* The upper four bits are valid, which is equal to 0xb0 or 11 */
191 When converted to binary, it is 11000000, and the upper four digits are 1100
*/

The following code: interrupts with priority higher than 0 are masked, which is equivalent to opening interrupts and exiting the critical section.

mov r0, #0 / * exit critical section*/
msr basepri, r0

Switching off and interrupting

The following code with return value means: when writing a new value to BASEPRI, save the value of BASEPRI first. When updating the value of BASEPRI, return the previously saved value of BASEPRI, and the returned value is passed into the interrupt function as a formal parameter.

No return value means that when writing a new value to BASEPRI, you don't have to save the value of BASEPRI first, regardless of the current interrupt state. Since you don't care about the current interrupt state, it means that such a function can't be called in the interrupt.

/*portmacro.h*/

/*The off interrupt function without return value cannot be nested and cannot be used in interrupts*/
#define portDISABLE_INTERRUPTS()				vPortRaiseBASEPRI()
/*Interrupt function without interrupt protection*/
#define portENABLE_INTERRUPTS()					vPortSetBASEPRI( 0 )

/*The off interrupt function with return value can be nested and can be used in the interrupt*/
#define portSET_INTERRUPT_MASK_FROM_ISR()		ulPortRaiseBASEPRI()
/*Interrupt function with interrupt protection*/
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)	vPortSetBASEPRI(x)

#define configMAX_SYSCALL_INTERRUPT_PRIORITY  	 191 / * the upper four bits are valid, i.e. equal to 0xb0 or 11*/

/*Off interrupt function without return value*/
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
		/* Set BASEPRI to the max syscall priority to effect a critical
		section. */
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
}
/*Off interrupt function with return value*/
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
		/* Set BASEPRI to the max syscall priority to effect a critical
		section. */
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}

	return ulReturn;
}
/*The difference between the interrupt function without interrupt protection and the interrupt function with interrupt protection lies in the value of the parameter*/
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
	__asm
	{
		/* Barrier instructions are not used as this function is only used to
		lower the BASEPRI value. */
		msr basepri, ulBASEPRI
	}
}

Enter critical section and exit critical section

For the case without interrupt protection, uxCriticalNesting in vPortEnterCritical function is a global variable that records the nesting times of critical segments. vPortExitCritical function reduces uxCriticalNesting by one each time. Portenable is called only when uxCriticalNesting = 0_ The interrupts function enables interrupts. In this way, when there are multiple critical segment codes, the protection of other critical segments will not be interrupted due to the exit of one critical segment code. The interruption will be enabled only after all critical segment codes exit.

With interrupt protection, when writing a new value to BASEPRI, save the value of BASEPRI first. When updating the value of BASEPRI, return the previously saved value of BASEPRI, and the returned value is passed into the interrupt function as a formal parameter.

/*Enter critical section without interruption protection*/
#define taskENTER_CRITICAL()		       portENTER_CRITICAL()
/*Exit critical section without interruption protection*/
#define taskEXIT_CRITICAL()			       portEXIT_CRITICAL()

/*Enter the critical section with interrupt protection and can be nested*/
#define taskENTER_CRITICAL_FROM_ISR()      portSET_INTERRUPT_MASK_FROM_ISR()
/*Exit critical section with interrupt protection and can be nested*/
#define taskEXIT_CRITICAL_FROM_ISR( x )    portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

/*Enter critical section without interruption protection*/
#define portENTER_CRITICAL()					vPortEnterCritical()
/*Exit critical section without interruption protection*/
#define portEXIT_CRITICAL()						vPortExitCritical()

/*Enter the critical section with interrupt protection and can be nested*/
#define portSET_INTERRUPT_MASK_FROM_ISR()		ulPortRaiseBASEPRI()
/*Exit critical section with interrupt protection and can be nested*/
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)	vPortSetBASEPRI(x)

/*Enter critical section without interruption protection*/
void vPortEnterCritical( void )
{
    /*The off interrupt function without return value cannot be nested and cannot be used in interrupts*/
	portDISABLE_INTERRUPTS();
	uxCriticalNesting++;

	if( uxCriticalNesting == 1 )
	{
		configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
	}
}
/*Exit critical section without interruption protection*/
void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if( uxCriticalNesting == 0 )
	{
        /*Interrupt function without interrupt protection*/
		portENABLE_INTERRUPTS();
	}
}


/*Enter the critical section with interrupt protection and can be nested*/
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
		/* Set BASEPRI to the max syscall priority to effect a critical
		section. */
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}

	return ulReturn;
}
/*Exit critical section with interrupt protection and can be nested*/
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
	__asm
	{
		/* Barrier instructions are not used as this function is only used to
		lower the BASEPRI value. */
		msr basepri, ulBASEPRI
	}
}
/*Applications of critical segment code*/
/* In case of interruption, critical segments can be nested */
{
    uint32_t ulReturn;
    /* Enter the critical segment, which can be nested */
    ulReturn = taskENTER_CRITICAL_FROM_ISR();
 
    /* Critical segment code */
 
    /* Exit critical section */
    taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}

/* In the case of non interruption, critical segments cannot be nested */
{
    /* Enter critical section */
    taskENTER_CRITICAL();
    
    /* Critical segment code */
    
    /* Exit critical section*/
    taskEXIT_CRITICAL();
}

Topics: Embedded system FreeRTOS