寄存器(R0~R16)以及从SysTick系统时钟理解RTOS移植初始化

移植系统最重要的细节之一就是配置系统时钟

第一次玩RT-Thread,发现同样的程序逻辑,测试现象不一样,从现象很明显看出来是时钟频率配置不一样。
由于之前玩STM32几乎没有关注过系统时钟的初始化,并且M3手册和STM32手册对于CTRL寄存器的CLKSOURCE位的说法不一,那么应该相信谁呢?
于是乎花了一下午的时间,才从系统启动过程的角度,经过测试程序的实际验证逐渐找到了原因所在。
(把我直接传送到1.4小节吧~~)

一、ARM寄存器别名及APCS

在这里插入图片描述

二、R0~R16寄存器用途、介绍

R0-R3

用作传入函数参数,传出函数返回值。在子程序调用之间,可以将 r0-r3 用于任何用途。

被调用函数在返回之前不必恢复 r0-r3。如果调用函数需要再次使用 r0-r3 的内容,则它必须保留这些内容。

RT-Thread代码启动过程

1、从系统初始化开始执行,将函数地址赋给R0寄存器,跳转到R0地址执行并返回此处(BLX是带链接的跳转,即带返回的跳转)。
startup_stm32f10x_hd.s 文件:
在这里插入图片描述

1.1 配置系统时钟(相当于CUBEMX配置时钟树)

执行LDR R0, =SystemInit 跳转到 system_stm32f10x.c执行void SystemInit (void)函数配置时钟:
在这里插入图片描述
在这里插入图片描述
RCC->CR时钟控制寄存器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

RCC->CFGR 时钟配置寄存器
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
RCC_CIR 时钟中断寄存器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后跳到 static void SetSysClockTo72(void)函数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2将main函数地址给R0,将函数地址赋给R0,跳转到R0地址执行,不返回(BX是跳转,不返回)。
1.3跳转到了 $ Sub $ $main,主要是一些系统启动代码(系统初始化)。

在这里插入图片描述

在rtthread_startup中,主要实现了板级初始化(初始化外设和驱动);打印RT-Thread的logo和版本信息;初始化系统定时器;初始化调度器;创建application线程(这里将用户main函数作为一个线程,用户main里面是空的);初始化软件定时器;创建空闲线程;启动系统调度(启用调度后,main函数就会参与调度开始运行)。

1.4重点讲一下RTOS系统时钟(滴答定时器)的初始化

1、什么是SYSTICK:
这是一个24位的系统节拍定时器system tick timer,SysTick,具有自动重载和溢出中断功能,所有基于Cortex_M3处理器的微控制器都可以由这个定时器获得一定的时间间隔。
2、作用:
在单任务引用程序中,因为其架构就决定了它执行任务的串行性,这就引出一个问题:当某个任务出现问题时,就会牵连到后续的任务,进而导致整个系统崩溃。要解决这个问题,可以使用实时操作系统(RTOS).
因为RTOS以并行的架构处理任务,单一任务的崩溃并不会牵连到整个系统。这样用户出于可靠性的考虑可能就会基于RTOS来设计自己的应用程序。这样SYSTICK存在的意义就是提供必要的时钟节拍,为RTOS的任务调度提供一个有节奏的“心跳”。
3、微控制器的定时器资源一般比较丰富,比如STM32存在8个定时器,为啥还要再提供一个SYSTICK?原因就是所有基于ARM Cortex_M3内核的控制器都带有SysTick定时器,这样就方便了程序在不同的器件之间的移植。而使用RTOS的第一项工作往往就是将其移植到开发人员的硬件平台上,由于SYSTICK的存在无疑降低了移植的难度。
4、工作原理
SysTick定时器是一个24位的倒计数,在一定频率下,当倒计数为0时,将从RELOAD寄存器中取值作为定时器的初始值,同时可以选择在这个时候产生中断(异常号:15)。
5、实例
在RT-Thread系统初始化void rt_hw_board_init()函数下调用了static __INLINE uint32_t SysTick_Config(uint32_t ticks)
在这里插入图片描述
为了便于理解:
M3权威指南和STM32手册说法不一,那么应该相信谁呢?

  • M3权威指南(SysTick->CTRL寄存器)
  • CLKSOURCE位用于选择外部/内核时钟源
    在这里插入图片描述
  • STM32参考手册
#define SysTick_CLKSource_HCLK_Div8    ((uint32_t)0xFFFFFFFB)
#define SysTick_CLKSource_HCLK         ((uint32_t)0x00000004)
SysTick->CTRL |= SysTick_CLKSource_HCLK; // CTRL寄存器的CLKSOURCE位 置1
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8; // CTRL寄存器的CLKSOURCE位 置0

在这里插入图片描述
在这里插入图片描述

  • 原子哥的解答
    在这里插入图片描述
  • 以及我通过实际实验得到的结果 和理解
    对于CTRL和LOAD的操作:注释部分和未注释部分,两者实验结果及现象一模一样
    在这里插入图片描述
  • 测试程序
static struct rt_thread led0_thread;//线程控制块
static struct rt_thread led1_thread;//线程控制块
ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t rt_led0_thread_stack[256];//线程栈
static rt_uint8_t rt_led1_thread_stack[256];//线程栈
  
//线程LED0
static void led0_thread_entry(void* parameter)
{
	while(1)
	{
		LED0=~LED0;
		rt_thread_delay(300);
	}
}

//线程LED1
static void led1_thread_entry(void* parameter)
{
	while(1)
	{
		LED1=~LED1; 
		rt_thread_delay(1000);
	}
}

int main(void)
{  
    // 创建静态线程
    rt_thread_init(&led0_thread,               	  //线程控制块
                   "led0",                     	  //线程名字,在shell里面可以看到
                   led0_thread_entry,          	  //线程入口函数
                   RT_NULL,                    	  //线程入口函数参数
                   &rt_led0_thread_stack[0],      //线程栈起始地址
                   sizeof(rt_led0_thread_stack),  //线程栈大小
                   3,                          	  //线程的优先级
                   20);                           //线程时间片
                               
    rt_thread_startup(&led0_thread);           	  //启动线程led0_thread,开启调度
				   
    // 创建静态线程                          
	rt_thread_init(&led1_thread,                  //线程控制块
				   "led1",                        //线程名字,在shell里面可以看到
				   led1_thread_entry,             //线程入口函数
				   RT_NULL,                       //线程入口函数参数
				   &rt_led1_thread_stack[0],      //线程栈起始地址
				   sizeof(rt_led1_thread_stack),  //线程栈大小
				   3,                             //线程的优先级
				   20);                           //线程时间片     

	rt_thread_startup(&led1_thread);              //启动线程led1_thread,开启调度  
}
实验现象相同,所以说,对于stm32,将CTRL寄存器的CLKSOURCE位置0所选择的时钟源是:AHB时钟(HCLK)8分频,即 本来就是被8分频过的。
1.5 $ Sub $ $main在main之前干的活就是进行rt-thread系统初始化。

在这里插入图片描述
以下是在rt_application_init()函数中创建的main函数线程:
在这里插入图片描述
在这里插入图片描述
$ Super $ $main可以直接跳到main()函数:
在这里插入图片描述
总结:可以这样使用给main函数打补丁:

int $Sub$$main(void)
{
    //添加补丁函数
 
    $Super$$main(); //使用本句直接转到main()运行
}

当然,main()函数可以换做其它需要的函数

R4-R10

被用来存放函数的局部变量。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。

R11- fp(frame pointer)寄存器

即可以用来记录回溯信息,也可以当做局部变量来使用

R12-内部调用暂时寄存器 ip

它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。

在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复 r12。

R13 -栈指针 sp

用户模式和系统模式共用一个SP,每种异常模式都有各自专用的R13寄存器(SP)。它们通常指向各模式所对应的专用堆栈,也就是ARM处理器允许用户程序有六个不同的堆栈空间,ARM处理器中的R13被用作SP。当不使用堆栈时,R13 也可以用做通用数据寄存器.

当程序的运行进入异常模式时,可以将需要保护的寄存器放入R13所指向的堆栈,而当程序从异常模式返回时,则从对应的堆栈中恢复,采用这种方式可以保证异常发生后程序的正常执行。

R14-链接寄存器 LR

在ARM体系结构中LR的特殊用途有两种:
一是执行子程序调用指令(BL )时,会自动完成将当前的PC的值减去4的结果数据保存到LR寄存器。即将调用指令的下紧邻指令的地址保存到LR。返回时将lr赋给pc即可。

二是当异常发生时,会自动完成将当前的PC保存到LR寄存器,返回时将lr-4赋给pc即可,因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。

为什么异常发生时,需要 sub lr, lr, #4 ?
是因为arm流水线,也就是执行第1条指令,第2条指令进行译码,将第3条指令从存储器中取出,那么pc当前等于pc+8,所以在异常发生时,此时lr=pc+8,但是pc+4是没有被执行的,所以异常返回时需要返回到(lr-4)地址上,执行已经译码的地址上。

stmdb和ldmia汇编指令

stmdb用于将寄存器压栈,ldmia用于将寄存器弹出栈。
stmdb和ldmia指令一般配对使用,用于保存使用到的寄存器。

ARM指令的多数据传输(STM、LDM)中,提到:多寄存器的Load和Store指令分为2组:一组用于数据的存储与读取,对应于IA、IB、DA、DB,一组用于堆栈操作,对应于FD、ED、FA、EA,两组中对应的指令含义相同。
即:
STMIB(地址先增而后完成操作)、STMFA(满递增堆栈);
STMIA(完成操作而后地址递增)、STMEA(空递增堆栈);
STMDB(地址先减而后完成操作)、STMFD(满递减堆栈);
STMDA(完成操作而后地址递减)、STMED(空递减堆栈)。
上述各组2个指令含义相同只是适用场合不同,同理有:
LDMIB、LDMED;
LDMIA、LDMFD;
LDMDB、LDMEA;
LDMDA、LDMFA。

IA模式表示:每次传送后地址+4;(After Increase)
DB模式表示:每次传送前地址-4;(Before Decrease)
多寄存器加载/存储指令共有8种模式(4个用与数据块的传输,4个用于栈操作)

例1:汇编指令

stmdb sp!,{r0-r12,lr}

含义:sp = sp - 4,先压lr,sp = lr(即将lr中的内容放入sp所指的内存地址)。sp = sp - 4,再压r12,sp = r12。sp = sp - 4,再压r11,sp = r11…sp = sp - 4,最后压r0,sp = r0。

如果想要将r0-r12和lr弹出,可以用ldmia指令:

ldmia sp!,{r0-r12,lr}

例二:

//将R1-R7的数据保存到寄存器中,存储器指针在保存第一个值之后增加,增长方向为向上增长
STMIA R0!,{R1-R7} 
//将R1-R7的数据保存到寄存器中,存储器指针在保存第一个值之后增加,增长方向为向下增长
STMDB R0!,{R1-R7} 

STMIA:比如当前r0指向的内存地址是 0x1000
STMIA R0!,{R1-R7} 就是首先把r1存入0x1000,然后r2存入0x1004,然后r3存入0x1008。如果是32位的处理器就是每次加4个字节,以此类推把 r1-r7按照递增的地址存入。R0!就是从R0的地址开始存的意思。
STMDB则是地址从R0开始减少,依次存储。

R15-程序计数器 PC

PC总是指向当前指令的下两条指令的地址,即PC的值为当前指令的地址值加8个字节程序状态寄存器

R16-CPSR(CurrentProgram Status Register,当前程序状态寄存器)

CPSR可在任何运行模式下被访问,它包括条件标志位、中断禁止位、当前处理器模式标志位,以及其他一些相关的控制和状态位。

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SysTick定时器是一种基于硬件的定时器,常用于嵌入式系统中。在Cortex-M系列的微控制器中,SysTick定时器是一个系统寄存器,用于提供一个可编程的定时器功能。 下面是一个示例代码,演示如何初始化和配置SysTick定时器: ```c #include <stdint.h> #include "stm32f4xx.h" // 根据具体的芯片型号选择对应的头文件 void SysTick_Init(uint32_t ticks) { // 设置SysTick定时器的重载值 SysTick->LOAD = (ticks - 1) & 0x00FFFFFF; // 设置SysTick定时器的优先级 NVIC_SetPriority(SysTick_IRQn, 0); // 启用SysTick定时器,并使能中断 SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; } void SysTick_Handler(void) { // 在这里处理SysTick定时器中断事件 // ... } int main() { // 初始化SysTick定时器,设置定时周期为1秒 // 假设系统时钟频率为100MHz,要实现1秒的定时周期,可以设置重载值为100000000-1 SysTick_Init(100000000); while (1) { // 主循环中的其他任务 // ... } } ``` 在上面的示例代码中,`SysTick_Init`函数用于初始化和配置SysTick定时器。其中,`ticks`参数表示定时周期的节拍数,该值可以根据系统时钟频率和所需的定时周期进行计算。在函数中,首先设置SysTick定时器的重载值为`(ticks - 1) & 0x00FFFFFF`,然后设置定时器的优先级,最后使能SysTick定时器和中断。 此外,代码中还提供了一个`SysTick_Handler`函数,用于处理SysTick定时器中断事件。在实际应用中,可以根据需求在该函数中编写相应的定时任务处理逻辑。 请注意,以上示例代码是基于STM32F4系列微控制器的CMSIS库进行编写的,具体的头文件和寄存器定义可能会因芯片型号和开发环境而有所不同。因此,在实际开发中,请根据具体的芯片型号和开发环境进行相应的调整和配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值