打印
[应用方案]

Cortex-M内核知识点总结

[复制链接]
楼主: 米多0036
手机看帖
扫描二维码
随时随地手机跟帖
81
米多0036|  楼主 | 2023-9-15 15:53 | 只看该作者 |只看大图 回帖奖励 |倒序浏览
使用事件驱动的调度器
QP状态机框架: https://www.state-machine.com/
/********************************************************************
upgrade fsm:

       <--------------------------------------------------
      /                                            \      \
    idle -----> info ----->transfer ------------> finsh    \
                  \            \                            \
                   \            \                            \
                    --------------------------------------->error
                    
*********************************************************************/
R_state_t ecu_upgrade_state_idle(fsm_t* me, uint32_t evt)
{
    switch(evt)
    {
        case SYS_ENTER_EVT:
        {
            log_d("enter idle state");
            memset(&upgrade_info , 0 , sizeof(struct ecu_upgrade_info));
            return S_HANDLE();
        }
        case ECU_UPGRADE_REQUEST_EVT:
        {
            return S_TRAN(ecu_upgrade_state_transfer);
            //return S_TRAN(ecu_upgrade_state_info);
        }
        default:
        {
             return S_IGNORE();
        }
    }
}
R_state_t ecu_upgrade_state_info(fsm_t* me, uint32_t evt)
{
    switch(evt)
    {
        case SYS_ENTER_EVT:
        {
            /*step1:   session control*/
            /*step2:   security_access*/
            /*step3:   file_download request*/
            /*period:  tester present*/
            fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
            return S_HANDLE();
        }
        case SYS_TIMEOUT_EVT:
        {
            /*period:  tester present*/
            fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
            return S_HANDLE();
        }   
        default:
        {
            return S_IGNORE();
        }
    }
}
...

使用特权

评论回复
82
米多0036|  楼主 | 2023-9-15 15:53 | 只看该作者
OSAL事件驱动框架 :https://github.com/recheal-zhang/ZStack-CC2530-2.3.0-1.4.0/tree/master/Components/osal
/***********用户任务事件处理函数***********/
uint16 SimpleBLETest_ProcessEvent( uint8 task_id, uint16 events )
{
    VOID task_id;
   
    if ( events & SYS_EVENT_MSG )
    {
        uint8 *pMsg;
        /*  读取发送来的消息            */
        if ( (pMsg = osal_msg_receive( SimpleBLETest_TaskID )) != NULL ){
            /*  消息有效数据处理函数    */
            simpleBLETest_ProcessOSALMsg( (osal_msg_send_dat *)pMsg );
            /*  清除消息内存            */
            VOID osal_msg_deallocate( pMsg );
        }
        return (events ^ SYS_EVENT_MSG);
    }
    if ( events & SBP_START_DEVICE_EVT )
    {
        /*  每500毫秒发送一个事件       */
        if ( SBP_PERIODIC_EVT_PERIOD ){
            osal_start_reload_timer( SimpleBLETest_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );
        }  
        return ( events ^ SBP_START_DEVICE_EVT );   
    }
    if ( events & SBP_PERIODIC_EVT)
    {
        static uint8 byTmp = 0;
        
        if(byTmp > 7) byTmp = 0;
        /*  发送消息及有效数据  byTmp   */
        osalSendUserDat(byTmp++);
        
        return (events ^ SBP_PERIODIC_EVT);
    }
    return 0;
}

使用特权

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

使用特权

评论回复
84
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
需求描述:

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

使用特权

评论回复
85
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
需求拆解:

16MHZ主频跑 FFT极其耗时间,在无硬件加速情况下,执行一遍可能到达秒级
电流电压50ms采集 是硬要求,异常初始优先级最高,须立即处理
电压电流 FFT变换对实时性要求不高,只是需要保存,类似一个历史数据,可供回查。

使用特权

评论回复
86
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
以上需求若使用上面两种调度方式无法满足实时性,需要使用主动中断的方式强行切换任务,处理流程如下图:

使用特权

评论回复
87
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
/*
创建任务:
        1)定义任务句柄
        2)定义任务处理函数
        3)定义任务类型:周期任务或触发任务
        4)定义任务优先级 (根据任务优先级,任务处理函数会被注册到不同的硬件定时器)
*/

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

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

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

使用特权

评论回复
88
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
RTOS调度器
在裸机的几种调度器中,时间与事件调度器的本质是相同的,都是设置任务的就绪标志,在主循环里边查询就绪标志,执行就绪任务,而基于硬件定时器的调度器相较于前两者增加了抢占功能,它已经初具RTOS的雏形了,但还存在以下问题:

相同优先级的任务需要排队执行
高优先级的任务在执行时,无法主动释放cpu使用权,即高优先级任务执行完某一步,需要sleep一段时间再执行不好实现
高优先级任务(定时器中断注册的任务)和低优先级任务(主循环注册的任务)性质不同,不完全等价
会引入更多的中断嵌套,且调度定时器中断优先级要最低,否则会导致其他中断受影响
所有任务使用同一个栈,一崩全崩
为了解决上述问题,RTOS便应运而生

使用特权

评论回复
89
米多0036|  楼主 | 2023-9-15 15:54 | 只看该作者
思考
有了前面几种调度器的铺垫 ,我们对 RTOS 需要实现哪些功能有个大概的认识,应该有以下几点:

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

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

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

由A任务(函数)切换到 B任务(函数),不能直接令PC指针等于任务B(函数)的地址,这样先前B执行的过程和A执行的过程会丢失

使用特权

评论回复
90
米多0036|  楼主 | 2023-9-15 15:55 | 只看该作者

使用特权

评论回复
91
米多0036|  楼主 | 2023-9-15 15:55 | 只看该作者
PendSV(可悬起系统调用)
一般设置成最低优先级,这样可以保证 优先级顺序: 高优先级中断 >低优先级中断 > 高优先级任务 > 低优先级任务 ,

使用特权

评论回复
92
米多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指针,就能解决上述问题。

使用特权

评论回复
93
米多0036|  楼主 | 2023-9-15 15:55 | 只看该作者
在多线程模式下 系统SP指针使用的是 PSP , 若发生中断,在将线程环境压入当前 SP (PSP) 后 ,SP将切换成 MSP,后续的中断局部变量,中断函数调用以及中断嵌套,都将使用MSP 。将系统使用的栈和线程使用的栈分开,可减小全面崩溃的风险。(在中断的具体行为章节有详细描述)

使用特权

评论回复
94
米多0036|  楼主 | 2023-9-15 15:55 | 只看该作者
线程切换汇编分析
;*************************************************************************
; 全局变量(4)
;*************************************************************************
IMPORT rt_thread_switch_interrupt_flag
IMPORT rt_interrupt_from_thread
IMPORT rt_interrupt_to_thread

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

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

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

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

    ; 开中断
    CPSIE F
    CPSIE I

使用特权

评论回复
95
米多0036|  楼主 | 2023-9-15 15:56 | 只看该作者
; *-----------------------------------------------------------------------
; * void PendSV_Handler(void);
; * r0 --> switch from thread stack
; * r1 --> switch to thread stack
; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
; *-----------------------------------------------------------------------
PendSV_Handler PROC
        EXPORT PendSV_Handler
        MRS r2, PRIMASK  ; 失能中断,为了保护上下文切换不被中断
        CPSID I
       
        LDR r0, =rt_thread_switch_interrupt_flag   ;加载rt_thread_switch_interrupt_flag 的地址到r0
    LDR r1, [r0]                               ; 加载rt_thread_switch_interrupt_flag 的值到r1
        CBZ r1, pendsv_exit                        ;判断r1 是否为0,为0 则跳转到pendsv_exit
        MOV r1, #0x00                              ; r1 不为0 则清0
    STR r1, [r0]                               ; 将r1 的值存储到rt_thread_switch_interrupt_flag,即清0
    LDR r0, =rt_interrupt_from_thread          ; 加载rt_interrupt_from_thread 的地址到r0
    LDR r1, [r0]                               ; 加载rt_interrupt_from_thread 的值到r1
        CBZ r1, switch_to_thread ; 判断r1 是否为0,为0 则跳转到switch_to_thread ,第一次线程切换时肯定为0,则跳转到switch_to_thread
    ;================================================================================================
    ;上文保存
    ;================================================================================================
    ; 当进入PendSVC Handler 时,上一个线程运行的环境即:
    ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
    ; 这些CPU 寄存器的值会自动保存到线程的栈中,剩下的r4~r11 需要手动保存
     MRS r1, psp             ; 获取线程栈指针到r1
     STMFD r1!, {r4 - r11}   ; 将CPU 寄存器r4~r11 的值存储到r1 指向的地址(每操作一次地址将递减一次)
     LDR r0, [r0]            ; 加载r0 指向值到r0,即r0=rt_interrupt_from_thread
     STR r1, [r0]            ; 将r1 的值存储到r0,即更新线程栈sp
        ;========================================================================================================
        ;下文切换
        ;========================================================================================================
        switch_to_thread
                LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread 的地址到r1,  它 是一个全局变量,里面存的是线程栈指针SP 的指针
                LDR r1, [r1]                   ; 加载rt_interrupt_to_thread 的值到r1,即sp 指针的指针
                LDR r1, [r1]                   ; 加载rt_interrupt_to_thread 的值到r1,即sp
                LDMFD r1!, {r4 - r11} ; 将线程栈指针r1(操作之前先递减) 指向的内容加载到CPU 寄存器r4~r11
                MSR psp, r1     ; 将线程栈指针更新到PSP

        pendsv_exit
        MSR PRIMASK, r2 ; 恢复中断
        ORR lr, lr, #0x04    ; 确保异常返回使用的栈指针是PSP,即LR 寄存器的位2 要为1
    ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU 寄存器:
    ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
    ; 同时PSP 的值也将更新,即指向线程栈的栈顶
    BX lr
    ENDP    ; PendSV_Handler 子程序结束

使用特权

评论回复
96
米多0036|  楼主 | 2023-9-15 15:56 | 只看该作者
任务优先级与时间片
优先级实现了高优先级任务抢占低优先级任务 ,时间片实现了同等优先级的任务分时间片运行。RTOS由中断 ,优先级 ,时间片决定了 其 RealTime OS 中的RealTime特性。下图中,任务a,b,c,d 允许抢占发生 , b1, b2,b3 允许时间片轮转发生

使用特权

评论回复
97
米多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运行完再运行低优先级任务

使用特权

评论回复
98
米多0036|  楼主 | 2023-9-15 15:56 | 只看该作者

使用特权

评论回复
99
米多0036|  楼主 | 2023-9-15 15:56 | 只看该作者
多线程编程模式
阻塞式编程
顺序逻辑结构
请求应答逻辑结构
发布与订阅逻辑结构
临界区与互斥访问
函数的可重入性
临界区使用场景
互斥访问使用场景
互锁的形成与避免
实时性窗口
推荐此系列文章: https://blog.51cto.com/u_15288275/2975971?articleABtest=0

使用特权

评论回复
100
tpgf| | 2023-10-7 13:42 | 只看该作者
到目前为止这种内核一共有多少个系列了啊

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则