移植系统最重要的细节之一就是配置系统时钟
第一次玩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可在任何运行模式下被访问,它包括条件标志位、中断禁止位、当前处理器模式标志位,以及其他一些相关的控制和状态位。