FreeRTOS任务切换

3.任务调度器

3.1.vTaskStartScheduler()

这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c
中有定义

1973 /*-----------------------------------------------------------*/
1974
1975 void vTaskStartScheduler( void )
1976 {
1977     BaseType_t xReturn;
1978
1979     /* Add the idle task at the lowest priority. */
         /*创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,
         *优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最
         *低。*/
1980     #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
1981         {
1982             StaticTask_t * pxIdleTaskTCBBuffer = NULL;
1983             StackType_t * pxIdleTaskStackBuffer = NULL;
1984             uint32_t ulIdleTaskStackSize;
1985
1986             /* The Idle task is created using user provided RAM - obtain the
1987              * address of the RAM then create the idle task. */
1988             vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
1989             xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
1990                                                  configIDLE_TASK_NAME,
1991                                                  ulIdleTaskStackSize,
1992                                                  ( void * ) NULL,       /*lint !e961.  The cast is not redundant for all compilers. */
1993                                                  portPRIVILEGE_BIT,     /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
1994                                                  pxIdleTaskStackBuffer,
1995                                                  pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
1996
1997             if( xIdleTaskHandle != NULL )
1998             {
1999                 xReturn = pdPASS;
2000             }
2001             else
2002             {
2003                 xReturn = pdFAIL;
2004             }
2005         }
2006     #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
2007         {
2008             /* The Idle task is being created using dynamically allocated RAM. */
2009             xReturn = xTaskCreate( prvIdleTask,
2010                                    configIDLE_TASK_NAME,
2011                                    configMINIMAL_STACK_SIZE,
2012                                    ( void * ) NULL,
2013                                    portPRIVILEGE_BIT,  /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
2014                                    &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
2015         }
2016     #endif /* configSUPPORT_STATIC_ALLOCATION */
2017
2018     #if ( configUSE_TIMERS == 1 )
2019         {
2020             if( xReturn == pdPASS )
2021             {
2022                 xReturn = xTimerCreateTimerTask();
                     /*如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务
                     *任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的*/
2023             }
2024             else
2025             {
2026                 mtCOVERAGE_TEST_MARKER();
2027             }
2028         }
2029     #endif /* configUSE_TIMERS */
2030
2031     if( xReturn == pdPASS )
         /*空闲任务和定时器任务创建成功*/
2032     {
2033         /* freertos_tasks_c_additions_init() should only be called if the user
2034          * definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
2035          * the only macro called by the function. */
2036         #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
2037             {
2038                 freertos_tasks_c_additions_init();
2039             }
2040         #endif
2041
2042         /* Interrupts are turned off here, to ensure a tick does not occur
2043          * before or during the call to xPortStartScheduler().  The stacks of
2044          * the created tasks contain a status word with interrupts switched on
2045          * so interrupts will automatically get re-enabled when the first task
2046          * starts to run. */
2047         portDISABLE_INTERRUPTS();
             /*关闭中断 在 SVC 中断服务函数 vPortSVCHandler()中会打开中断*/
2048
2049         #if ( configUSE_NEWLIB_REENTRANT == 1 )
2050             {
2051                 /* Switch Newlib's _impure_ptr variable to point to the _reent
2052                  * structure specific to the task that will run first.
2053                  * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
2054                  * for additional information. */
2055                 _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
2056             }
2057         #endif /* configUSE_NEWLIB_REENTRANT */
2058
2059         xNextTaskUnblockTime = portMAX_DELAY;
2060         xSchedulerRunning = pdTRUE;
             /*变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行*/
2061         xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
2062
2063         /* If configGENERATE_RUN_TIME_STATS is defined then the following
2064          * macro must be defined to configure the timer/counter used to generate
2065          * the run time counter time base.   NOTE:  If configGENERATE_RUN_TIME_STATS
2066          * is set to 0 and the following line fails to build then ensure you do not
2067          * have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
2068          * FreeRTOSConfig.h file. */
2069         portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
             /*当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时
            *需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器*/
2070
2071         traceTASK_SWITCHED_IN();
2072
2073         /* Setting up the timer tick is hardware specific and thus in the
2074          * portable interface. */
2075         if( xPortStartScheduler() != pdFALSE )
             /*调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、
             *FPU 单元和 PendSV 中断等等*/
2076         {
2077             /* Should not reach here as if the scheduler is running the
2078              * function will not return. */
                 /*如果调度器启动成功的话就不会运行到这里,函数不会有返回值的*/
2079         }
2080         else
2081         {
2082             /* Should only reach here if a task calls xTaskEndScheduler(). */
2083         }
2084     }
2085     else
2086     {
2087         /* This line will only be reached if the kernel could not be started,
2088          * because there was not enough FreeRTOS heap to create the idle task
2089          * or the timer task. */
2090         configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
             /*程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
             空闲任务或者定时器任务的时候没有足够的内存*/
2091     }
2092
2093     /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
2094      * meaning xIdleTaskHandle is not used anywhere else. */
2095     ( void ) xIdleTaskHandle;
2096 }
2097 /*-----------------------------------------------------------*/

3.2 xPortStartScheduler()

FreeRTOS 系统时钟是由滴答定时器来提供的,而且任务切换也会用到 PendSV 中断,这些硬件的初始化由函数 xPortStartScheduler()来完成.

307 /*-----------------------------------------------------------*/                                                                                                          
308                                                                                                                                                                          
309 /*                                                                                                                                                                       
310  * See header file for description.                                                                                                                                      
311  */                                                                                                                                                                      
312 BaseType_t xPortStartScheduler( void )                                                                                                                                   
313 {                                                                                                                                                                        
314     /* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.                                                                                                        
315      * See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */                                                                                                            
316     configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );                                                                                                                
317                                                                                                                                                                          
318     /* This port can be used on all revisions of the Cortex-M7 core other than                                                                                           
319      * the r0p1 parts.  r0p1 parts should use the port from the                                                                                                          
320      * /source/portable/GCC/ARM_CM7/r0p1 directory. */                                                                                                                   
321     configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );                                                                                                                  
322     configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );                                                                                                                  
323                                                                                                                                                                          
324     #if ( configASSERT_DEFINED == 1 )                                                                                                                                    
325         {                                                                                                                                                                
326             volatile uint32_t ulOriginalPriority;                                                                                                                        
327             volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );                 
328             volatile uint8_t ucMaxPriorityValue;                                                                                                                         
329                                                                                                                                                                          
330             /* Determine the maximum priority from which ISR safe FreeRTOS API                                                                                           
331              * functions can be called.  ISR safe functions are those that end in                                                                                        
332              * "FromISR".  FreeRTOS maintains separate thread and ISR API functions to                                                                                   
333              * ensure interrupt entry is as fast and simple as possible.                                                                                                 
334              *                                                                                                                                                           
335              * Save the interrupt priority value that is about to be clobbered. */                                                                                       
336             ulOriginalPriority = *pucFirstUserPriorityRegister;                                                                                                          
337                                                                                                                                                                          
338             /* Determine the number of priority bits available.  First write to all                                                                                      
339              * possible bits. */                                                                                                                                         
340             *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;                                                                                                         
341                                                                                                                                                                          
342             /* Read the value back to see how many bits stuck. */                                                                                                        
343             ucMaxPriorityValue = *pucFirstUserPriorityRegister;                                                                                                          
344                                                                                                                                                                          
345             /* The kernel interrupt priority should be set to the lowest                                                                                                 
346              * priority. */                                                                                                                                              
347             configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );                                                              
348                                                                                                                                                                          
349             /* Use the same mask on the maximum system call priority. */                                                                                                 
350             ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;                                                                            
351                                                                                                                                                                          
352             /* Calculate the maximum acceptable priority group value for the number                                                                                      
353              * of bits read back. */                                                                                                                                     
354             ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;         
355                                                                                                                                                                          
356             while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
357             {
358                 ulMaxPRIGROUPValue--;
359                 ucMaxPriorityValue <<= ( uint8_t ) 0x01;
360             }
361 
362             #ifdef __NVIC_PRIO_BITS
363                 {
364                     /* Check the CMSIS configuration that defines the number of
365                      * priority bits matches the number of priority bits actually queried
366                      * from the hardware. */
367                     configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
368                 }
369             #endif
370 
371             #ifdef configPRIO_BITS
372                 {
373                     /* Check the FreeRTOS configuration that defines the number of
374                      * priority bits matches the number of priority bits actually queried
375                      * from the hardware. */
376                     configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
377                 }
378             #endif
379 
380             /* Shift the priority group value back to its position within the AIRCR
381              * register. */
382             ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
383             ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
384 
385             /* Restore the clobbered interrupt priority register to its original
386              * value. */
387             *pucFirstUserPriorityRegister = ulOriginalPriority;
388         }
389     #endif /* conifgASSERT_DEFINED */
390 
391     /* Make PendSV and SysTick the lowest priority interrupts. */
392     portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
        /*设置 PendSV 的中断优先级,为最低优先级*/
393     portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
        /*设置滴答定时器的中断优先级,为最低优先级*/
394 
395     /* Start the timer that generates the tick ISR.  Interrupts are disabled
396      * here already. */
397     vPortSetupTimerInterrupt();
        /*调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断*/
398 
399     /* Initialise the critical nesting count ready for the first task. */
400     uxCriticalNesting = 0;
        /*初始化临界区嵌套计数器*/
401 
402     /* Ensure the VFP is enabled - it should be anyway. */
403     prvEnableVFP();
        /*调用函数 prvEnableVFP()使能 FPU*/
404 
405     /* Lazy save always. */
406     *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
407 
408     /* Start the first task. */
409     prvStartFirstTask();
410 
411     /* Should not get here! */
412     return 0;
413 }
414 /*-----------------------------------------------------------*/

3.3 prvEnableVFP()

在函数 xPortStartScheduler()中会通过调用 prvEnableVFP()来使能 FPU,这个函数是汇编形
式的,在文件 port.c 中有定义。

289 /*-----------------------------------------------------------*/
290 
291 __asm void prvEnableVFP( void )
292 {
293 /* *INDENT-OFF* */
294     PRESERVE8
295 
296     /* The FPU enable bits are in the CPACR. */
297     ldr.w r0, =0xE000ED88   (1)
298     ldr r1, [ r0 ]  (2)
299 
300     /* Enable CP10 and CP11 coprocessors, then save back. */
301     orr r1, r1, #( 0xf << 20 )   (3)
302     str r1, [ r0 ]   (4)
303     bx r14  (5)
304     nop
305 /* *INDENT-ON* */
306 }
307 /*-----------------------------------------------------------*/

(1)、利用寄存器 CPACR 可以使能或禁止 FPU,此寄存器的地址为 0XE000ED88,此寄存器的 CP10(bit20 和 bit21)和 CP11(bit22和 bit23)用于控制 FPU。这 4 个 bit 的具体含义请参考《权威指南》,通常将这 4 个 bit 都设置为1 来开启 FPU,表示全访问。此行代码将地址 0XE000ED88 保存在寄存器 R0 中。
(2)、读取 R0 中保存的存储地址处的数据,也就是 CPACR 寄存器的值,并将结果保存在R1 寄存器中。
(3)、R1 中的值与(0xf<<20)进行按位或运算,也就是 R1=R1|0X00F00000。此时 R1 所保存的值的 bit20~bit23 就都为 1 了,将这个值写入寄存器 CPACR 中就可开启 FPU。
(4)、将 R1 中的值写入 R0 中保存的地址处,也就是寄存器 CPACR 中。
(5)、函数返回。bx 为间接跳转指令,一般为 BX <Rm>,也就是跳转到存放在 Rm 中的地址处,此处是跳转到 R14 存放的地址处。R14 寄存器也叫做链接寄存(LR),也可以用 LR 表示。

3.4 prvStartFirstTask()

启动第一个任务

258 /*-----------------------------------------------------------*/
259 
260 __asm void prvStartFirstTask( void )
261 {
262 /* *INDENT-OFF* */
263     PRESERVE8
264 
265     /* Use the NVIC offset register to locate the stack. */
266     ldr r0, =0xE000ED08  ;R0=0XE000ED08 (1)
267     ldr r0, [ r0 ]   ;取 R0 所保存的地址处的值赋给 R0 (2)
268     ldr r0, [ r0 ]   ;获取 MSP 初始值 (3)
269     /* Set the msp back to the start of the stack. */
270     msr msp, r0    ;复位 MSP  (4)
271 
272     /* Clear the bit that indicates the FPU is in use in case the FPU was used
273      * before the scheduler was started - which would otherwise result in the
274      * unnecessary leaving of space in the SVC stack for lazy saving of FPU
275      * registers. */
276     mov r0, #0
277     msr control, r0
278     /* Globally enable interrupts. */
279     cpsie i   ;使能中断(清除 PRIMASK) (5)
280     cpsie f   ;使能中断(清除 FAULTMASK) (6)
281     dsb   ;数据同步屏障  (7)
282     isb   ;指令同步屏障  (8)
283     /* Call SVC to start the first task. */
284     svc 0   ;触发 SVC 中断(异常) (9)
285     nop
286     nop
287 /* *INDENT-ON* */
288 }
289 /*-----------------------------------------------------------*/

(1)、将 0XE000ED08 保存在寄存器 R0 中。一般来说向量表应该是从起始地址(0X00000000)开始存储的,不过,有些应用可能需要在运行时修改或重定义向量表,Cortex-M 处理器为此提供了一个叫做向量表重定位的特性。向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。VTOR 寄存器的地址就是 0XE000ED08,通过这个寄存器可以重新定义向量表,比如在 STM32F429 的 ST 官方库中会通过函数 SystemInit()来设置 VTOR 寄存器,
代码如下:

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;  //VTOR=0x08000000+0X00

通过上面一行代码就将向量表开始地址重新定义到了 0X08000000,向量表的起始地址存储的就是 MSP 初始值。
(2)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取寄存器 VTOR中的值,并将其保存在 R0 寄存器中。这一行代码执行完就以后 R0 的值应该为 0X08000000。
(3)、读取 R0 中存储的地址处的数据并将其保存在 R0 寄存器,也就是读取地址 0X08000000处存储的数据,并将其保存在 R0 寄存器中。我们知道向量表的起始地址保存的就是主栈指针MSP 的初始值,这一行代码执行完以后寄存器 R0 就存储 MSP 的初始值。现在来看(1)、(2)、
(3)这三步起始就是为了获取 MSP 的初始值而已!
(4)、复位 MSP,R0 中保存了 MSP 的初始值,将其赋值给 MSP 就相当于复位 MSP。
(5)和(6)、使能中断。
(7)和(8)、数据同步和指令同步屏障。
(9),调用 SVC 指令触发 SVC 中断,SVC 也叫做请求管理调用,SVC 和 PendSV 异常对于OS 的设计来说非常重要。SVC 异常由 SVC 指令触发。在 FreeRTOS 中仅仅使用 SVC 异常来启动第一个任务,后面的程序中就再也用不到 SVC 了。

3.5SVC中断服务函数

在函数 prvStartFirstTask()中通过调用 SVC 指令触发了 SVC 中断,而第一个任务的启动就是在 SVC 中断服务函数中完成的,SVC 中断服务函数应该为 SVC_Handler(),但是FreeRTOSConfig.h 中通过#define 的方式重新定义为了 xPortPendSVHandler(),如下:

#define xPortPendSVHandler PendSV_Handler

函数 vPortSVCHandler()在文件 port.c 中定义,这个函数也是用汇编写的,函数源码如下:

238 /*-----------------------------------------------------------*/
239 
240 __asm void vPortSVCHandler( void )
241 {
242 /* *INDENT-OFF* */
243     PRESERVE8
244 
245     /* Get the location of the current TCB. */
246     ldr r3, = pxCurrentTCB   ;R3=pxCurrentTCB 的地址 (1)
247     ldr r1, [ r3 ]   ;取 R3 所保存的地址处的值赋给 R1 (2)
248     ldr r0, [ r1 ]   ;取 R1 所保存的地址处的值赋给 R0 (3)
249     /* Pop the core registers. */
250     ldmia r0 !, {r4-r11,r14}  ;出栈 ,R4~R11 和 R14  (4)
251     msr psp, r0   ;进程栈指针 PSP 设置为任务的堆栈 (5)
252     isb   ;指令同步屏障
253     mov r0, # 0   ;R0=0  (6)
254     msr basepri, r0   ;寄存器 basepri=0,开启中断  (7)
255     bx r14   ;(8)
256 /* *INDENT-ON* */
257 }
258 /*-----------------------------------------------------------*/

(1)、获取 pxCurrentTCB 指针的存储地址,pxCurrentTCB 是一个指向 TCB_t 的指针,这个指针永远指向正在运行的任务。
(2)、取 R3 所保存的地址处的值赋给 R1。通过这一步就获取到了当前任务的任务控制块的存储地址。
(3)、取 R3 所保存的地址处的值赋给 R0,我们知道任务控制块的第一个字段就是任务堆栈的栈顶指针 pxTopOfStack 所指向的位置,所以读取任务控制块所在的首地址(0X20001058)得到的就是栈顶指针所指向的地址。
可以看出(1)、(2)和(3)的目的就是获取要切换到的这个任务的任务栈顶指针,因为任务所对应的寄存器值,也就是现场都保存在任务的任务堆栈中,所以需要获取栈顶指针来恢复这些寄存器值!
(5)、设置进程栈指针 PSP
(6)、设置寄存器 R0 为 0。
(7)、设置寄存器 BASEPRI 为 R0,也就是 0,打开中断!
(8)、执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,堆栈使用进程栈 PSP,然后执行寄存器 PC 中保存的任务函数。至此,FreeRTOS 的任务调度器正式开始运行!

任务切换

FreeRTOS 任务切换场合

● 可以执行一个系统调用
● 系统滴答定时器(SysTick)中断

执行系统调用

执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换函数taskYIELD(),FreeRTOS 有些 API 函数也会调用函数 taskYIELD(),这些 API 函数都会导致任务切换,这些 API 函数和任务切换函数 taskYIELD()都统称为系统调用。函数 taskYIELD()其实就是个宏,在文件 task.h中有如下定义:

 181 #define taskYIELD()                        portYIELD()   

函数 portYIELD()也是个宏,在文件 portmacro.h 中有如下定义:

82 /*-----------------------------------------------------------*/                                                                                                          
 83                                                                                                                                                                          
 84 /* Scheduler utilities. */                                                                                                                                               
 85     #define portYIELD()                                 \                                                                                                                
 86     {                                                   \                                                                                                                
 87         /* Set a PendSV to request a context switch. */ \                                                                                                                
 88         portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \             
           /*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。
           *这样就可以在 PendSV 中断服务函数中进行任务切换了。*/                                                                                                   
 89                                                         \                                                                                                                
 90         /* Barriers are normally not required but do ensure the code is completely \                                                                                     
 91          * within the specified behaviour for the architecture. */ \                                                                                                     
 92         __dsb( portSY_FULL_READ_WRITE );                           \                                                                                                     
 93         __isb( portSY_FULL_READ_WRITE );                           \                                                                                                     
 94     }                                                                                                                                                                    
 95 /*-----------------------------------------------------------*/    

系统滴答定时器(SysTick)

FreeRTOS 中滴答定时器(SysTick)中断服务函数中也会进行任务切换,滴答定时器中断服务函数如下:

void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
xPortSysTickHandler();
}
HAL_IncTick();
}

在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler(),此函数源码如下:

515 /*-----------------------------------------------------------*/
516 
517 void xPortSysTickHandler( void )
518 {
519     /* The SysTick runs at the lowest interrupt priority, so when this interrupt
520      * executes all interrupts must be unmasked.  There is therefore no need to
521      * save and then restore the interrupt mask value as its value is already
522      * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
523      * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
524     vPortRaiseBASEPRI(); /*关闭中断*/
525     {
526         /* Increment the RTOS tick. */
527         if( xTaskIncrementTick() != pdFALSE )
528         {
529             /* A context switch is required.  Context switching is performed in
530              * the PendSV interrupt.  Pend the PendSV interrupt. */
531             portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
                /*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中
                *断。这样就可以在 PendSV 中断服务函数中进行任务切换了。*/
532         }
533     }
534 
535     vPortClearBASEPRIFromISR();/*打开中断*/
536 }
537 /*-----------------------------------------------------------*/

PendSV中断服务函数

前面说了 FreeRTOS 任务切换的具体过程是在 PendSV 中断服务函数中完成的,本节我们就来学习一个 PendSV 的中断服务函数,看看任务切换过程究竟是怎么进行的。PendSV 中断服务函数本应该为 PendSV_Handler(),但是 FreeRTOS 使用#define 重定义了,如下:

#define xPortPendSVHandler PendSV_Handler

函数 xPortPendSVHandler()源码如下:

451 /*-----------------------------------------------------------*/
452 
453 __asm void xPortPendSVHandler( void )
454 {
455     extern uxCriticalNesting;
456     extern pxCurrentTCB;
457     extern vTaskSwitchContext;
458 
459 /* *INDENT-OFF* */
460     PRESERVE8
461 
462     mrs r0, psp
463     isb
464     /* Get the location of the current TCB. */
465     ldr r3, =pxCurrentTCB
466     ldr r2, [ r3 ]
467 
468     /* Is the task using the FPU context?  If so, push high vfp registers. */
469     tst r14, #0x10
470     it eq
471     vstmdbeq r0!, {s16-s31}
472 
473     /* Save the core registers. */
474     stmdb r0!, {r4-r11, r14}
475 
476     /* Save the new top of stack into the first member of the TCB. */
477     str r0, [ r2 ]
478 
479     stmdb sp!, {r0, r3}
480     mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
481     msr basepri, r0
482     dsb
483     isb
484     bl vTaskSwitchContext
485     mov r0, # 0
486     msr basepri, r0
487     ldmia sp!, {r0, r3}
488 
489     /* The first item in pxCurrentTCB is the task top of stack. */
490     ldr r1, [ r3 ]
491     ldr r0, [ r1 ]
492 
493     /* Pop the core registers. */
494     ldmia r0!, {r4-r11, r14}
495 
496     /* Is the task using the FPU context?  If so, pop the high vfp registers
497      * too. */
498     tst r14, # 0x10
499     it eq
500     vldmiaeq r0!, {s16-s31}
501 
502     msr psp, r0
503     isb
504     #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
505         #if WORKAROUND_PMU_CM001 == 1
506             push { r14 }
507             pop { pc }
508             nop
509         #endif
510     #endif
511 
512     bx r14
513 /* *INDENT-ON* */
514 }
515 /*-----------------------------------------------------------*/

查找下一个要运行的任务

在 PendSV 中断服务程序中有调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务,函数源码如下:

2992 /*-----------------------------------------------------------*/
2993 
2994 void vTaskSwitchContext( void )
2995 {
2996     if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
         /*如果调度器挂起那就不能进行任务切换*/
2997     {
2998         /* The scheduler is currently suspended - do not allow a context
2999          * switch. */
3000         xYieldPending = pdTRUE;
3001     }
3002     else
3003     {
3004         xYieldPending = pdFALSE;
3005         traceTASK_SWITCHED_OUT();
3006 
3007         #if ( configGENERATE_RUN_TIME_STATS == 1 )
3008             {
3009                 #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
3010                     portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
3011                 #else
3012                     ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
3013                 #endif
3014    
3015                 /* Add the amount of time the task has been running to the
3016                  * accumulated time so far.  The time the task started running was
3017                  * stored in ulTaskSwitchedInTime.  Note that there is no overflow
3018                  * protection here so count values are only valid until the timer
3019                  * overflows.  The guard against negative values is to protect
3020                  * against suspect run time stat counter implementations - which
3021                  * are provided by the application, not the kernel. */
3022                 if( ulTotalRunTime > ulTaskSwitchedInTime )
3023                 {
3024                     pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
3025                 }
3026                 else
3027                 {
3028                     mtCOVERAGE_TEST_MARKER();
3029                 }
3030 
3031                 ulTaskSwitchedInTime = ulTotalRunTime;
3032             }
3033         #endif /* configGENERATE_RUN_TIME_STATS */
3034 
3035         /* Check for stack overflow, if configured. */
3036         taskCHECK_FOR_STACK_OVERFLOW();
3037 
3038         /* Before the currently running task is switched out, save its errno. */
3039         #if ( configUSE_POSIX_ERRNO == 1 )
3040             {
3041                 pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
3042             }
3043         #endif
3044 
3045         /* Select a new task to run using either the generic C or port
3046          * optimised asm code. */
3047         taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and re     trieved is the same. */
              /*调用函数 taskSELECT_HIGHEST_PRIORITY_TASK()获取下一个要运行的任务*/
3048         traceTASK_SWITCHED_IN();
3049 
3050         /* After the new task is switched in, update the global errno. */
3051         #if ( configUSE_POSIX_ERRNO == 1 )
3052             {
3053                 FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
3054             }
3055         #endif
3056 
3057         #if ( configUSE_NEWLIB_REENTRANT == 1 )
3058             {
3059                 /* Switch Newlib's _impure_ptr variable to point to the _reent
3060                  * structure specific to this task.
3061                  * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
3062                  * for additional information. */
3063                 _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
3064             }
3065         #endif /* configUSE_NEWLIB_REENTRANT */
3066     }
3067 }
3068 /*-----------------------------------------------------------*/

FreeRTOS 时间片调度

前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级,这些任务的调度是一个值得考虑的问题,不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,至于下一个要运行哪个任务?在 9.4 小节里面已经分析过了,FreeRTOS 中的这种调度方法就是时间片调度。下图展示了运行在同一优先级下的执行时间图,在优先级 N 下有 3 个就绪的任务。


时间片调度

1、任务 3 正在运行。
2、这时一个时钟节拍中断(滴答定时器中断)发生,任务 3 的时间片用完,但是任务 3 还没有执行完。
3、FreeRTOS 将任务切换到任务 1,任务 1 是优先级 N 下的下一个就绪任务。
4、任务 1 连续运行至时间片用完。
5、任务 3 再次获取到 CPU 使用权,接着运行。
6、任务 3 运行完成,调用任务切换函数 portYIELD()强行进行任务切换放弃剩余的时间片,从而使优先级 N 下的下一个就绪的任务运行。
7、FreeRTOS 切换到任务 1。
8、任务 1 执行完其时间片。
要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期,比如本教程中 configTICK_RATE_HZ 为 1000,那么一个时间片的长度就是 1ms。时间片调度发生在滴答定时器的中断服务函数中,前面讲解滴答定时器中断服务函数的时候说了在中断服务函数 SysTick_Handler()中会调用 FreeRTOS 的 API 函数 xPortSysTickHandler(),而函数 xPortSysTickHandler() 会 引 发 任 务 调 度 , 但 是 这 个 任 务 调 度 是 有 条 件 的 , 函 数xPortSysTickHandler()如下:

515 /*-----------------------------------------------------------*/
516 
517 void xPortSysTickHandler( void )
518 {
519     /* The SysTick runs at the lowest interrupt priority, so when this interrupt
520      * executes all interrupts must be unmasked.  There is therefore no need to
521      * save and then restore the interrupt mask value as its value is already
522      * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
523      * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
524     vPortRaiseBASEPRI(); /*关闭中断*/
525     {
526         /* Increment the RTOS tick. */
527         if( xTaskIncrementTick() != pdFALSE )  (1)
528         {
529             /* A context switch is required.  Context switching is performed in
530              * the PendSV interrupt.  Pend the PendSV interrupt. */
531             portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
                /*通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中
                *断。这样就可以在 PendSV 中断服务函数中进行任务切换了。*/
532         }
533     }
534 
535     vPortClearBASEPRIFromISR();/*打开中断*/
536 }
537 /*-----------------------------------------------------------*/

上述代码中(1)表明只有函数 xTaskIncrementTick()的返回值不为 pdFALSE 的时候就会进行任务调度!查看函数 xTaskIncrementTick()会发现有如下条件编译语句:
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中 xTickCount 就会加一,xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的,此函数在文件 tasks.c 中有定义,如下:

2707 /*----------------------------------------------------------*/
2708 
2709 BaseType_t xTaskIncrementTick( void )
2710 {
2711     TCB_t * pxTCB;
2712     TickType_t xItemValue;
2713     BaseType_t xSwitchRequired = pdFALSE;
2714 
2715     /* Called by the portable layer each time a tick interrupt occurs.
2716      * Increments the tick then checks to see if the new tick value will cause any
2717      * tasks to be unblocked. */
2718     traceTASK_INCREMENT_TICK( xTickCount );
         /*每个时钟节拍中断(滴答定时器中断)调用一次本函数,增加时钟节拍计数器 xTickCount 的
         *值,并且检查是否有任务需要取消阻塞。*/
2719 
2720     if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
         /*判断任务调度器是否被挂起*/
2721     {
2722         /* Minor optimisation.  The tick count cannot change in this
2723          * block. */
2724         const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
             /*将时钟节拍计数器 xTickCount 加一,并将结果保存在 xConstTickCount 中,下一行程
             *序会将 xConstTickCount 赋值给 xTickCount,相当于给 xTickCount 加一*/
2725 
2726         /* Increment the RTOS tick, switching the delayed and overflowed
2727          * delayed lists if it wraps to 0. */
2728         xTickCount = xConstTickCount;
2729 
2730         if( xConstTickCount == ( TickType_t ) 0U ) /*xConstTickCount 为 0,说明发生了溢出! */
2731         {
2732             taskSWITCH_DELAYED_LISTS();
                 /*如果发生了溢出的话使用函数 taskSWITCH_DELAYED_LISTS 将延时列表指针
                 *pxDelayedTaskList 和溢出列表指针 pxOverflowDelayedTaskList 所指向的列表进行交换,函数
                 *taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件 tasks.c 中有定义,将这两个指针所指向
                 *的列表交换以后还需要更新 xNextTaskUnblockTime 的值。*/
2733         }
2734         else
2735         {
2736             mtCOVERAGE_TEST_MARKER();
2737         }
2738 
2739         /* See if this tick has made a timeout expire.  Tasks are stored in
2740          * the  queue in the order of their wake time - meaning once one task
2741          * has been found whose block time has not expired there is no need to
2742          * look any further down the list. */
              /*判断是否有任务延时时间到了,任务都会根据唤醒时间点值按照顺序(由小到大的升
              *序排列)添加到延时列表中,这就意味这如果延时列表中第一个列表项对应的任务的
              *延时时间都没有到的话后面的任务就不用看了,肯定也没有到。*/
2743         if( xConstTickCount >= xNextTaskUnblockTime )
             /*变量 xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值,如果
             *xConstTickCount 大于 xNextTaskUnblockTime 的话就说明有任务需要解除阻塞了*/
2744         {
2745             for( ; ; )
2746             {
2747                 if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                     /*判断延时列表是否为空*/
2748                 {
2749                     /* The delayed list is empty.  Set xNextTaskUnblockTime
2750                      * to the maximum possible value so it is extremely
2751                      * unlikely that the
2752                      * if( xTickCount >= xNextTaskUnblockTime ) test will pass
2753                      * next time through. */
2754                     xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
2755                     break;
2756                 }
2757                 else
2758                 {
2759                     /* The delayed list is not empty, get the value of the
2760                      * item at the head of the delayed list.  This is the time
2761                      * at which the task at the head of the delayed list must
2762                      * be removed from the Blocked state. */
                          /*延时列表不为空,获取延时列表的第一个列表项的值,根据判断这个值
                          *判断任务延时时间是否到了, 如果到了的话就将任务移除延时列表。*/
2763                     pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the      type of the pointer stored and retrieved is the same. */
                         /*延时列表不为空,获取延时列表第一个列表项对应的任务控制块*/
2764                     xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
                         /*获取上述获取到的任务控制块中的壮态列表项值*/
2765 
2766                     if( xConstTickCount < xItemValue )
                         /*任务控制块中的壮态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于
                          *当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。*/
2767                     {
2768                         /* It is not time to unblock this item yet, but the
2769                          * item value is the time at which the task at the head
2770                          * of the blocked list must be removed from the Blocked
2771                          * state -  so record the item value in
2772                          * xNextTaskUnblockTime. */
2773                         xNextTaskUnblockTime = xItemValue;
                             /*任务延时时间还未到,而且 xItemValue 已经保存了下一个要唤醒的任务的唤醒时间
                             *点,所以需要用 xItemValue 来更新 xNextTaskUnblockTime*/
2774                         break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
2775                     }
2776                     else
2777                     {
2778                         mtCOVERAGE_TEST_MARKER();
2779                     }
2780 
2781                     /* It is time to remove the item from the Blocked state. */
2782                     ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                         /*任务延时时间到了,所以将任务先从延时列表中移除*/
2783 
2784                     /* Is the task waiting on an event also?  If so remove
2785                      * it from the event list. */
2786                     if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                     /*检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就任务
                     *从相应的事件列表中移除。因为超时时间到了!*/
2787                     {
2788                         ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                             /*将任务从相应的事件列表中移除*/
2789                     }
2790                     else
2791                     {   
2792                         mtCOVERAGE_TEST_MARKER();
2793                     }
2794                     
2795                     /* Place the unblocked task into the appropriate ready
2796                      * list. */
2797                     prvAddTaskToReadyList( pxTCB );
                         /*任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需
                         *要将任务添加到就绪列表中*/
2798                     
2799                     /* A task being unblocked cannot cause an immediate
2800                      * context switch if preemption is turned off. */
2801                     #if ( configUSE_PREEMPTION == 1 )
2802                         {
2803                             /* Preemption is on, but a context switch should
2804                              * only be performed if the unblocked task has a
2805                              * priority that is equal to or higher than the
2806                              * currently executing task. */
2807                             if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                                 /*延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标
                                  *记 xSwitchRequired 为 pdTRUE,表示需要进行任务切换。*/
2808                             {
2809                                 xSwitchRequired = pdTRUE;
2810                             }
2811                             else
2812                             {   
2813                                 mtCOVERAGE_TEST_MARKER();
2814                             }
2815                         }   
2816                     #endif /* configUSE_PREEMPTION */
2817                 }           
2818             }           
2819         }           
2820                 
2821         /* Tasks of equal priority to the currently running task will share
2822          * processing time (time slice) if preemption is on, and the application
2823          * writer has not explicitly turned time slicing off. */
2824         #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
             /*如果使能了时间片调度的话,还要处理跟时间片调度有关的工作
             *当宏 configUSE_PREEMPTION和宏 configUSE_PREEMPTION 都为 1 的时候下面的
             *代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为 1,缺一不可!*/
2825             {
2826                 if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
                     /*判断当前任务所对应的优先级下是否还有其他的任务*/
2827                 {
2828                     xSwitchRequired = pdTRUE;
                         /*如果当前任务所对应的任务优先级下还有其他的任务那么就返回 pdTRUE*/
2829                 }
2830                 else
2831                 {
2832                     mtCOVERAGE_TEST_MARKER();
2833                 }
2834             }
2835         #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
2836 
2837         #if ( configUSE_TICK_HOOK == 1 )
             /*如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函
            * 数的具体内容由用户自行编写。*/
2838             {
2839                 /* Guard against the tick hook being called when the pended tick
2840                  * count is being unwound (when the scheduler is being unlocked). */
2841                 if( xPendedTicks == ( TickType_t ) 0 )
2842                 {
2843                     vApplicationTickHook();
2844                 }
2845                 else
2846                 {
2847                     mtCOVERAGE_TEST_MARKER();
2848                 }
2849             }
2850         #endif /* configUSE_TICK_HOOK */
2851 
2852         #if ( configUSE_PREEMPTION == 1 )
2853             {
2854                 if( xYieldPending != pdFALSE )
2855                 {
2856                     xSwitchRequired = pdTRUE;
2857                 }
2858                 else
2859                 {
2860                     mtCOVERAGE_TEST_MARKER();
2861                 }
2862             }
2863         #endif /* configUSE_PREEMPTION */
2864     }
2865     else
         /*如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不
         *不会更新 xTickCount 了。取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍
         *数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用 uxPendedTicks 次函数
         *xTaskIncrementTick(),这样 xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞*/
2866     {
2867         ++xPendedTicks;
             /*uxPendedTicks 是个全局变量,在文件 tasks.c 中有定义,任务调度器挂起以后此变量
             *用来记录时钟节拍数*/
2868 
2869         /* The tick hook gets called at regular intervals, even if the
2870          * scheduler is locked. */
2871         #if ( configUSE_TICK_HOOK == 1 )
2872             {
2873                 vApplicationTickHook();
2874             }
2875         #endif
2876     }
2877 
2878     return xSwitchRequired;
         /*返回 xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果
         *为 pdTRUE 的话就需要进行任务切换,pdFALSE 的话就不需要进行任务切换。函数
         *xPortSysTickHandler()中调用 xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定
         *是否进行任务切换。*/
2879 }
2880 /*-----------------------------------------------------------*/

从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数xTaskIncrementTick() 就 会 返 回 pdTURE , 由 于 函 数 返 回 值 为 pdTURE 因 此 函 数xPortSysTickHandler()就会进行一次任务切换。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352