如果刚好有大佬看到发现笔记中有什么写错了的,欢迎大佬指点,我十分乐意受到大佬的指点哈哈哈,顺便我还想问一下csdn的编辑器有没有保存的快捷键啊,CTRL+S好像没有用啊。
R13:SP(stack point)
R14:LR(link registor)常用于记录断点,比如调用子程序时先保存当前pc减4的值,还有程序异常时也是
PSR:program state rigestor
PC:程序计数器
4个以内用R0-R3传递,否则就要压栈实现。函数返回值返回到R0中
流程:保存现场——>调用处理函数——>恢复现场
硬件实现保存R0-R3、LR、R12和返回地址。
硬件实现调用该处理函数(函数编写时已经具备不会破坏R4-R11原来的内容的特性)。
当发生中断时:
硬件保存的那几个寄存器和返回地址,这时候的LR只是作为一个普通的寄存器保存,中断结束后的返回地址是那个硬件保存的保存地址。(个人猜想:就是如果发生中断时是在一个子函数中,那么LR里面保存的是那个子函数结束之后的返回地址,如果是在主函数中LR应该是没有存地址,就是说中断的返回地址不存在LR中)
硬件恢复R0-R3、LR、R12和返回地址,软件通过栈恢复其他的那些寄存器。
现场的本质:就是所有寄存器
假设两个程序交替执行,每次只执行1ms
这就意味着程序A/B可能会在任何任何一处发生中断。
这就需要每次中断时保存现场,把寄存器保存到栈里,返回地址保存到栈里,而每个函数进入时本身就分配有一个自己的栈,栈里有他的局部变量,这样一来只要不破坏这个栈,所有的东西就都保存起来了,当下次切换回这个函数时就恢复现场并从返回地址开始就可以了。
假设任务A中有多个子程序a1、a2、a3,那么A的栈里分配出a1、a2、a3的栈,如果他们套娃了,a1中调用到a2,a2中调用到a3,就会如下图所示,这些函数的现场都可以保存在他们的栈里(子函数退出后回到上一个函数的地址应该时在LR中),同时sp跳转到函数起始处。而当从任务A跳到任务B的时候,A的栈里也会有一块内存保存任务A的现场(因为时中断,重新进入任务A的地址应该是保存现场时存下的返回地址),然后跳到任务B去。
跳转任务B时首先第一件事就是要恢复B的现场,然后从B的存好的返回地址处开始继续运行。
创建任务首先要有任务中的函数以及函数所需要的一些参数(param),但是传入一个函数要怎么实现呢,从之前的学习中可以知道函数就是一段代码嘛,而这段代码又保存在一块内存中,所以本质上运行那个函数就是从那个函数的起始地址开始走,所以传个地址就好了,也就是说我们做一个指向这个函数的函数指针传进去就可以了。因为要有函数指针,所以可以先typedef一个定义函数指针的类型,为后续定义函数指针提供便利。
typedef void(*task_function)(void *param)
函数定义:
其次,一个任务还要有自己的栈,其实就是搞一块可以用的内存就行了,char一个数组都行。但是,栈在这块内存如果没有4字节对齐后续的运行就会出问题,可以通过这个指令(不知道是不是叫做指令?)实现字节对齐:
创建任务的本质其实就是在还没有中断发生前模仿中断发生后的现场人为的伪造一个让任务开始的现场。
栈里面的现场保存的排列遵循两个原则:先放硬件保存的,再放软件保存的;高地址对应高标号寄存器,所以,上半部分如果在中断中是硬件实现的,我们的伪造需要和硬件的保存一致,不然之后硬件恢复就会错乱,而下半部分因为汇编中的连续读写多个寄存器的指令(STM/LDM)有个对应关系就是高地址存高标号寄存器,所以为了更方便使用汇编编程软件保存的寄存器就按照高地址存高标号寄存器的原则保存。(大概是因为读写多个寄存器是从低标号往高标号,而SP从下往上走所以应该高标号放在上面吧)
设置好返回地址(设为函数起始地址)和参数(R0-R3)就可以了,这样当第一次中断发生时任务按照切换流程来走就可以从头开始了。
伪造现场本质就是写任务对应的那个栈的那块内存,因为要对栈操作,我们需要知道栈的大小从而得到栈顶,所以创建任务函数需要一个栈大小的参数。所以创建任务的函数应该改为:
typedef void (*task_function)(void *param);//定义了一种task_function的类型,并定义这种类型为指向某种函数的指针,这种函数以(void *param)为参数
void create_task(task_function,void *param,char *stack,stack_len);
如图:
获得栈顶指针后将指针下移16(减16),开始伪造现场,见上面的栈图可知,top此时指的是存R4的地方,以此对应修改各个寄存器的保存值,这里可以将top当数组使用,比如:
int *top = stack_a + stack_len;
top[8] = 参数1;//R0
top[9] = 参数2;//R1
...
top[14] = 函数首地址(函数指针);//返回地址
其实这里有点懵,数组从小到大是从下往上的,但是top不是在最上面吗?还是说这是大小端存储的问题。
除了这些寄存器,还有PSR寄存器需要配置,其中的指令位需要设置选择thumb指令(16位)或者arm指令(32位),M3内核都是用的thumb指令,寄存器结构如图:
用全局变量存储下栈的位置(应该是那堆寄存器后一个位置),还没太搞清楚。
根据局部变量的大小以及函数调用的深度,函数越多那么函数栈就越多,任务栈就得越大。
任务的启动和切换用系统滴答定时器中断实现时间片轮询。
启动的本质就是把任务对应的栈里的寄存器保存值恢复到寄存器。
这里果然就印证了之前听课的猜想。
硬件保存了那堆寄存器后,这个时候LR会被置为一个特殊值,而不是普通的调用函数那样LR存入一个返回地址(像上图右边就是普通调用),然后就开始调用c函数处理,当c函数处理完了跳回LR所指的地址,这个特殊值就会把pc跳到恢复那堆硬件保存的寄存器的代码段,当这堆寄存器恢复好后才会跳到返回地址那里。
所以要实现汇编函数需要的两个功能就要利用到这个特殊值,把LR置为这个特殊值就可以实现硬件中断恢复。
就是让任务标志位变为下一个任务,并获取任务的栈及其大小,然后等待时钟中断时保存当前任务现场,并将任务状态标志位和栈作为参数执行开始任务函数即可。
多个栈的切换,配合各种状态标志位实现休眠、切换任务、优先级等。
如果两个任务中都要对一个全局变量操作,那么有可能操作这个全局变量的过程中刚好到达定时中断切换任务,那么就会导致对这个全局变量的操作失败,这个时候就要在对全局变量操作的过程中关掉定时中断,等操作完了再打开。
上图的例子,A、B任务都会对a++,理论上A、B任务各执行一遍后a的值应该加了2,但是,如果A任务的a++执行了第一步后就被中断了,然后B任务执行完一次才跳回到A任务继续,那么当A、B任务都执行了一遍后a只会加1。
真正的RTOS系统其实并非通过tick中断实现转换的,而是通过tick触发PSV中断,如果需要提前切换任务只有人为触发PSV中断即可。
中断时不会针对每一个核,而是中断先进入中断控制器,然后再根据控制器决定中断进入哪一个核。
当一个核进入一个任务后,就锁上了,直到这个核退出,锁上的时候别的核要进来就得在那个锁里轮询,直到锁开。
如下图:第一条红线就是锁,CPU1进去后CPU2只能在外面,直到CPU1执行完这段代码(这让我联想到了生物的受精过程的那个透明带还是啥来着)。
视频信息量太大了,后半段疯狂汇编,很多东西还不是很通透,无法输出,以后想到再继续修改吧。