[应用方案] Cortex-M内核知识点总结

[复制链接]
5812|105
 楼主| 米多0036 发表于 2023-9-15 15:53 | 显示全部楼层
使用事件驱动的调度器
QP状态机框架: https://www.state-machine.com/
  1. /********************************************************************
  2. upgrade fsm:

  3.        <--------------------------------------------------
  4.       /                                            \      \
  5.     idle -----> info ----->transfer ------------> finsh    \
  6.                   \            \                            \
  7.                    \            \                            \
  8.                     --------------------------------------->error
  9.                     
  10. *********************************************************************/
  11. R_state_t ecu_upgrade_state_idle(fsm_t* me, uint32_t evt)
  12. {
  13.     switch(evt)
  14.     {
  15.         case SYS_ENTER_EVT:
  16.         {
  17.             log_d("enter idle state");
  18.             memset(&upgrade_info , 0 , sizeof(struct ecu_upgrade_info));
  19.             return S_HANDLE();
  20.         }
  21.         case ECU_UPGRADE_REQUEST_EVT:
  22.         {
  23.             return S_TRAN(ecu_upgrade_state_transfer);
  24.             //return S_TRAN(ecu_upgrade_state_info);
  25.         }
  26.         default:
  27.         {
  28.              return S_IGNORE();
  29.         }
  30.     }
  31. }
  32. R_state_t ecu_upgrade_state_info(fsm_t* me, uint32_t evt)
  33. {
  34.     switch(evt)
  35.     {
  36.         case SYS_ENTER_EVT:
  37.         {
  38.             /*step1:   session control*/
  39.             /*step2:   security_access*/
  40.             /*step3:   file_download request*/
  41.             /*period:  tester present*/
  42.             fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
  43.             return S_HANDLE();
  44.         }
  45.         case SYS_TIMEOUT_EVT:
  46.         {
  47.             /*period:  tester present*/
  48.             fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
  49.             return S_HANDLE();
  50.         }   
  51.         default:
  52.         {
  53.             return S_IGNORE();
  54.         }
  55.     }
  56. }
  57. ...
 楼主| 米多0036 发表于 2023-9-15 15:53 | 显示全部楼层
OSAL事件驱动框架 :https://github.com/recheal-zhang/ZStack-CC2530-2.3.0-1.4.0/tree/master/Components/osal
  1. /***********用户任务事件处理函数***********/
  2. uint16 SimpleBLETest_ProcessEvent( uint8 task_id, uint16 events )
  3. {
  4.     VOID task_id;
  5.    
  6.     if ( events & SYS_EVENT_MSG )
  7.     {
  8.         uint8 *pMsg;
  9.         /*  读取发送来的消息            */
  10.         if ( (pMsg = osal_msg_receive( SimpleBLETest_TaskID )) != NULL ){
  11.             /*  消息有效数据处理函数    */
  12.             simpleBLETest_ProcessOSALMsg( (osal_msg_send_dat *)pMsg );
  13.             /*  清除消息内存            */
  14.             VOID osal_msg_deallocate( pMsg );
  15.         }
  16.         return (events ^ SYS_EVENT_MSG);
  17.     }
  18.     if ( events & SBP_START_DEVICE_EVT )
  19.     {
  20.         /*  每500毫秒发送一个事件       */
  21.         if ( SBP_PERIODIC_EVT_PERIOD ){
  22.             osal_start_reload_timer( SimpleBLETest_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );
  23.         }  
  24.         return ( events ^ SBP_START_DEVICE_EVT );   
  25.     }
  26.     if ( events & SBP_PERIODIC_EVT)
  27.     {
  28.         static uint8 byTmp = 0;
  29.         
  30.         if(byTmp > 7) byTmp = 0;
  31.         /*  发送消息及有效数据  byTmp   */
  32.         osalSendUserDat(byTmp++);
  33.         
  34.         return (events ^ SBP_PERIODIC_EVT);
  35.     }
  36.     return 0;
  37. }

 楼主| 米多0036 发表于 2023-9-15 15:53 | 显示全部楼层
基于硬件定时器调度器
基于时间或事件的调度器已经能处理绝大多数裸机的应用情况 , 但有些特殊场景,上述两种调度器无法满足实时性。裸机的实时性需要靠中断来保证,上面两种调度器的本质都是定时产生就绪标志 ,然后在主循环里轮询标志 , 标志满足则执行,虽然任务错开时间运行对响应速度有一定的提高,但是还是避免不了排队执行,若某个任务本身的执行时间非常长,比如1S才能执行完,即使其执行周期长,但是在他执行时,其他任务都需要排队。
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
需求描述:

在仪器仪表行业,一般使用低端MCU (主频16MHZ),无法提供足够资源 跑 RTOS
如供电线路检测仪表,可能需要实现,线路电压采集,电流采集,温度采集 ,显示,无线传输等任务
要求 50ms 采集一次电压电流 , 1s采集一次温度,500ms刷新一次显示
采集的电压电流进行 FFT变换,并将结果存储到外部SPI flash
温度,电压,电流异常预警 , 从检查到异常发生,需要在 10ms内控制继电器断电,报警鸣笛
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
需求拆解:

16MHZ主频跑 FFT极其耗时间,在无硬件加速情况下,执行一遍可能到达秒级
电流电压50ms采集 是硬要求,异常初始优先级最高,须立即处理
电压电流 FFT变换对实时性要求不高,只是需要保存,类似一个历史数据,可供回查。
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
以上需求若使用上面两种调度方式无法满足实时性,需要使用主动中断的方式强行切换任务,处理流程如下图:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
  1. /*
  2. 创建任务:
  3.         1)定义任务句柄
  4.         2)定义任务处理函数
  5.         3)定义任务类型:周期任务或触发任务
  6.         4)定义任务优先级 (根据任务优先级,任务处理函数会被注册到不同的硬件定时器)
  7. */

  8. int task_create(TaskHander_t handle ,  TaskCallBack_t cb , uint8_t type , uint8_t prior);

  9. /*
  10. 启动某个任务的调度
  11.    1)任务句柄:根据任务句柄,能得知任务优先级,处理函数等,然后启动对应的定时器
  12.    2)延时多久执行任务调度
  13. */
  14. int task_schedule(TaskHander_t handle , uint32_t time);

  15. /*
  16. 停止某个任务的调度
  17.    1)任务句柄:根据任务句柄,能得知任务优先级,处理函数等,然后启动对应的定时器
  18.    2)延时多久停止任务调度
  19. */
  20. int task_schedule(TaskHander_t handle , uint32_t time);
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
RTOS调度器
在裸机的几种调度器中,时间与事件调度器的本质是相同的,都是设置任务的就绪标志,在主循环里边查询就绪标志,执行就绪任务,而基于硬件定时器的调度器相较于前两者增加了抢占功能,它已经初具RTOS的雏形了,但还存在以下问题:

相同优先级的任务需要排队执行
高优先级的任务在执行时,无法主动释放cpu使用权,即高优先级任务执行完某一步,需要sleep一段时间再执行不好实现
高优先级任务(定时器中断注册的任务)和低优先级任务(主循环注册的任务)性质不同,不完全等价
会引入更多的中断嵌套,且调度定时器中断优先级要最低,否则会导致其他中断受影响
所有任务使用同一个栈,一崩全崩
为了解决上述问题,RTOS便应运而生
 楼主| 米多0036 发表于 2023-9-15 15:54 | 显示全部楼层
思考
有了前面几种调度器的铺垫 ,我们对 RTOS 需要实现哪些功能有个大概的认识,应该有以下几点:

各个任务性质相同 ,但有优先级区分 ,且同一优先级的任务,不能排队运行,而是时间片轮转

任务任何时间都能切换到其它任务 , 而有这种能力的只有中断 ,也就是需要有一个接口,供任务主动触发中断,然后在中断里边切换到其他任务

任务本身是函数 , 所谓的任务切换,其实就是切换当前运行的函数, 通过修改PC指针就可以实现

由A任务(函数)切换到 B任务(函数),不能直接令PC指针等于任务B(函数)的地址,这样先前B执行的过程和A执行的过程会丢失
 楼主| 米多0036 发表于 2023-9-15 15:55 | 显示全部楼层

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 米多0036 发表于 2023-9-15 15:55 | 显示全部楼层
PendSV(可悬起系统调用)
一般设置成最低优先级,这样可以保证 优先级顺序: 高优先级中断 >低优先级中断 > 高优先级任务 > 低优先级任务 ,
 楼主| 米多0036 发表于 2023-9-15 15:55 | 显示全部楼层
线程栈与主栈
在上面的思考中。有一点没有明确:在task_a中主动触发中断,task_a的环境将保存到栈中,然后切换到task_b执行,再次触发中断,将task_b的环境保存到栈中,再回到task_a, 若a和b的环境都保存在同一个栈中,那么根据栈的特性先入后出,由b到a的切换必须先弹出b的环境,再弹出a的环境,这样显然不行。我们知道栈本身是一块连续的内存,PUSH 和 POP指令控制将内容压入栈中,然后移动对应的SP指针 ,若为每一个任务都定义一个栈和SP指针,就能解决上述问题。
 楼主| 米多0036 发表于 2023-9-15 15:55 | 显示全部楼层
在多线程模式下 系统SP指针使用的是 PSP , 若发生中断,在将线程环境压入当前 SP (PSP) 后 ,SP将切换成 MSP,后续的中断局部变量,中断函数调用以及中断嵌套,都将使用MSP 。将系统使用的栈和线程使用的栈分开,可减小全面崩溃的风险。(在中断的具体行为章节有详细描述)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 米多0036 发表于 2023-9-15 15:55 | 显示全部楼层
线程切换汇编分析
  1. ;*************************************************************************
  2. ; 全局变量(4)
  3. ;*************************************************************************
  4. IMPORT rt_thread_switch_interrupt_flag
  5. IMPORT rt_interrupt_from_thread
  6. IMPORT rt_interrupt_to_thread

  7. ;*************************************************************************
  8. ; 常量(5)
  9. ;*************************************************************************
  10. ;-------------------------------------------------------------------------
  11. ; 有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
  12. ; 系统控制块外设SCB 地址范围:0xE000ED00-0xE000ED3F
  13. ;-------------------------------------------------------------------------
  14. SCB_VTOR        EQU 0xE000ED08  ; 向量表偏移寄存器
  15. NVIC_INT_CTRL   EQU 0xE000ED04  ; 中断控制状态寄存器
  16. NVIC_SYSPRI2    EQU 0xE000ED20  ; 系统优先级寄存器(2)
  17. NVIC_PENDSV_PRI EQU 0x00FF0000  ; PendSV 优先级值(lowest)
  18. NVIC_PENDSVSET  EQU 0x10000000  ; 触发PendSV exception 的值

  19. ; *-----------------------------------------------------------------------
  20. ; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
  21. ; * r0 --> to
  22. ; * 该函数用于开启第一次线程切换
  23. ; *-----------------------------------------------------------------------
  24. rt_hw_context_switch_to PROC
  25.     EXPORT rt_hw_context_switch_to   ; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C 文件调用
  26.     LDR r1, =rt_interrupt_to_thread  ; 将rt_interrupt_to_thread 的地址加载到r1
  27.     STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_to_thread
  28.     LDR r1, =rt_interrupt_from_thread; 将rt_interrupt_from_thread 的地址加载到r1
  29.     MOV r0, #0x0                     ; 配置r0 等于0 , ;设置rt_interrupt_from_thread 的值为0,表示启动第一次线程切换
  30.     STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_from_thread
  31.     LDR r1, =rt_thread_switch_interrupt_flag  ; 设置中断标志位rt_thread_switch_interrupt_flag 的值为1
  32.     MOV r0, #1                       ; 配置r0 等于1
  33.     STR r0, [r1]                     ; 将r0 的值存储到rt_thread_switch_interrupt_flag

  34.     ; 设置PendSV 异常的优先级
  35.     LDR r0, =NVIC_SYSPRI2
  36.     LDR r1, =NVIC_PENDSV_PRI
  37.     LDR.W r2, [r0,#0x00] ; 读
  38.     ORR r1,r1,r2 ; 改
  39.     STR r1, [r0] ; 写

  40.         ; 触发PendSV 异常(产生上下文切换)
  41.     LDR r0, =NVIC_INT_CTRL
  42.     LDR r1, =NVIC_PENDSVSET
  43.     STR r1, [r0]

  44.     ; 开中断
  45.     CPSIE F
  46.     CPSIE I
 楼主| 米多0036 发表于 2023-9-15 15:56 | 显示全部楼层
  1. ; *-----------------------------------------------------------------------
  2. ; * void PendSV_Handler(void);
  3. ; * r0 --> switch from thread stack
  4. ; * r1 --> switch to thread stack
  5. ; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
  6. ; *-----------------------------------------------------------------------
  7. PendSV_Handler PROC
  8.         EXPORT PendSV_Handler
  9.         MRS r2, PRIMASK  ; 失能中断,为了保护上下文切换不被中断
  10.         CPSID I
  11.        
  12.         LDR r0, =rt_thread_switch_interrupt_flag   ;加载rt_thread_switch_interrupt_flag 的地址到r0
  13.     LDR r1, [r0]                               ; 加载rt_thread_switch_interrupt_flag 的值到r1
  14.         CBZ r1, pendsv_exit                        ;判断r1 是否为0,为0 则跳转到pendsv_exit
  15.         MOV r1, #0x00                              ; r1 不为0 则清0
  16.     STR r1, [r0]                               ; 将r1 的值存储到rt_thread_switch_interrupt_flag,即清0
  17.     LDR r0, =rt_interrupt_from_thread          ; 加载rt_interrupt_from_thread 的地址到r0
  18.     LDR r1, [r0]                               ; 加载rt_interrupt_from_thread 的值到r1
  19.         CBZ r1, switch_to_thread ; 判断r1 是否为0,为0 则跳转到switch_to_thread ,第一次线程切换时肯定为0,则跳转到switch_to_thread
  20.     ;================================================================================================
  21.     ;上文保存
  22.     ;================================================================================================
  23.     ; 当进入PendSVC Handler 时,上一个线程运行的环境即:
  24.     ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
  25.     ; 这些CPU 寄存器的值会自动保存到线程的栈中,剩下的r4~r11 需要手动保存
  26.      MRS r1, psp             ; 获取线程栈指针到r1
  27.      STMFD r1!, {r4 - r11}   ; 将CPU 寄存器r4~r11 的值存储到r1 指向的地址(每操作一次地址将递减一次)
  28.      LDR r0, [r0]            ; 加载r0 指向值到r0,即r0=rt_interrupt_from_thread
  29.      STR r1, [r0]            ; 将r1 的值存储到r0,即更新线程栈sp
  30.         ;========================================================================================================
  31.         ;下文切换
  32.         ;========================================================================================================
  33.         switch_to_thread
  34.                 LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread 的地址到r1,  它 是一个全局变量,里面存的是线程栈指针SP 的指针
  35.                 LDR r1, [r1]                   ; 加载rt_interrupt_to_thread 的值到r1,即sp 指针的指针
  36.                 LDR r1, [r1]                   ; 加载rt_interrupt_to_thread 的值到r1,即sp
  37.                 LDMFD r1!, {r4 - r11} ; 将线程栈指针r1(操作之前先递减) 指向的内容加载到CPU 寄存器r4~r11
  38.                 MSR psp, r1     ; 将线程栈指针更新到PSP

  39.         pendsv_exit
  40.         MSR PRIMASK, r2 ; 恢复中断
  41.         ORR lr, lr, #0x04    ; 确保异常返回使用的栈指针是PSP,即LR 寄存器的位2 要为1
  42.     ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU 寄存器:
  43.     ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
  44.     ; 同时PSP 的值也将更新,即指向线程栈的栈顶
  45.     BX lr
  46.     ENDP    ; PendSV_Handler 子程序结束
 楼主| 米多0036 发表于 2023-9-15 15:56 | 显示全部楼层
任务优先级与时间片
优先级实现了高优先级任务抢占低优先级任务 ,时间片实现了同等优先级的任务分时间片运行。RTOS由中断 ,优先级 ,时间片决定了 其 RealTime OS 中的RealTime特性。下图中,任务a,b,c,d 允许抢占发生 , b1, b2,b3 允许时间片轮转发生

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 米多0036 发表于 2023-9-15 15:56 | 显示全部楼层
任务就绪表与系统滴答时钟
上图定义的就是任务优先级就绪表 ,就绪表的更新如下步骤:

任务在创建后,会根据任务的优先级,将任务插入到就绪表中
启动调度后,系统将从优先级最高的链表(header0)开始查询就绪任务,并执行最高优先级任务 task_a
task_a中 调用sleep函数,等待下一个system_tick中断到来,在中断处理函数中获取优先级最高的就绪任务执行(b1,b2 ,b3)
依次执行完c1 ,c2 , d , 完成所有任务都运行一遍,此时,再次触发 system_stick 可能没有任务处于就绪态
在system_stick 中断处理函数中遍历任务,若任务剩余阻塞tick不为0 (vTaskDelay和其他阻塞函数设置tick),则tick-- , 否则调度任务
c1 , c2 处于同一优先级 , 若c1运行一个周期需要 40ms(未调用阻塞函数,例如运行复杂计算),c2运行周期60ms
当任务时间片设置为5ms时 , c1运行5ms后,在system_tick中会判断c1剩余时间片为0 ,切换到c2运行5ms , 然后切换到c1运行,知道c1,c2运行完再运行低优先级任务
 楼主| 米多0036 发表于 2023-9-15 15:56 | 显示全部楼层

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 米多0036 发表于 2023-9-15 15:56 | 显示全部楼层
多线程编程模式
阻塞式编程
顺序逻辑结构
请求应答逻辑结构
发布与订阅逻辑结构
临界区与互斥访问
函数的可重入性
临界区使用场景
互斥访问使用场景
互锁的形成与避免
实时性窗口
推荐此系列文章: https://blog.51cto.com/u_15288275/2975971?articleABtest=0
tpgf 发表于 2023-10-7 13:42 | 显示全部楼层
到目前为止这种内核一共有多少个系列了啊
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部