实现任务需要的基础知识
1、程序内部细节
通过分析C语言程序的编码会发现程序都是一些指令和数据。
什么是程序?
- 指令
- 运行过程中的数据
2、常用汇编指令
汇编指令详解
3、ARM架构过程调用标准AAPCS
传参:
通过r0-r3传递,多于4个参数的部分用栈传递
返回值:
通过r0寄存器
C函数调用过程寄存器变化:
1、随意使用R0、R1、R2、R3、 R12 无需保护它们,硬件自动保存
2、r4-r11 可用,先保存,用完后要恢复原来的值
特殊的寄存器:
r13 - sp指针
r14 - LR链接寄存器,保存子程序返回地址
r15 - PC程序计数器,PC指向哪里,程序就运行到哪里
4、Cortex-M3中断异常处理流程
栈帧图中的返回地址保存中断执行结束后的返回地址,也就是中断结束后返回执行的第一条语句。
中断异常处理流程:
1、保存中断处理完成后的返回地址,这是由硬件自动保存的
2、中断处理,硬件自动调用中断服务函数,中断服务程序也是C函数,C语言函数调用过程会保证不破坏R4~R11
3、中断处理完成之后是恢复现场,硬件自动恢复R4-R11之外的寄存器,R4-R11在中断处理函数执行结束会恢复,所以保持不变
中断异常返回:
这里是引用8.1.4 EXC_RETURN
处理器进人异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN数值。当利用BX、POP或存储器加载指令(LDR或LDM)被加中时,该数值用于触发异常返回机制。
EXC_RETURN中的一些位用于提高异常流程的其他信息。EXC_RETURN义如表8.1所示,EXC_RETURN的合法值则如表8.2所示。由于EXC_RETURN的编码格式,在地址区域0xF0000000一0xFFFFFFFF
行中断返回的。不过,由于系统空间中的地址区域已经被架构定义为不可执会带来什么问题。
—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》
从这段话中可以知道,中断触发时硬件会自动会将LR寄存器设置为0xF0000000—0xFFFFFFFF
这个范围的某个数值,这个数值用于中断返回
当使用BX之类的指令进行BX LR时,由于LR此时是个特殊值,不可执行,此时硬件就知道要触发硬件恢复R0-R3、R12等寄存器。
多任务切换实现
1、任务切换的核心 — 栈
任务切换的核心是切换任务的栈
创建任务实质就是伪造任务现场(栈帧)
。
任务能实现切换的核心也是栈。
Cortex-M3的栈帧的结构:
什么是现场? —— 当前执行程序被打断瞬间所有寄存器的值。
怎么保存现场? —— 存储到内存。
保存到内存什么地方? —— 栈
程序状态寄存器:
—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》 4.2.3 特殊寄存器
PSR寄存器T位
设置为1表示使用的是Thumb指令(16bit)
,0表示使用ARM指令(32bit)
,Cortex-M内核只支持Thumb指令。Thumb指令可以更好的节省空间。
2、伪造任务栈(现场)
伪造任务栈也就是所谓的创建任务(线程)。
void os_thread_create(thread_entry entry, void *arg, void *stack_addr, uint32_t stack_size)
{
char *cstack = (char *)stack_addr;
cstack += stack_size; /*get stack top */
uint32_t *stack = (uint32_t *)cstack;
stack -= 16; /* 因为栈是向下生长;空出16x4的空间刚好能构造一个栈帧 */
stack[0] = 0; /* R4 */
stack[1] = 0; /* R5 */
stack[2] = 0; /* R6 */
stack[3] = 0; /* R7 */
stack[4] = 0; /* R8 */
stack[5] = 0; /* R9 */
stack[6] = 0; /* R10 */
stack[7] = 0; /* R11 */
stack[8] = (uint32_t)arg; /* R0 传递给线程函数的参数,只有一个参数,根据AAPCS规则是通过R0传递第一个参数 */
stack[9] = 0; /* R1 */
stack[10] = 0; /* R2 */
stack[11] = 0; /* R3 */
stack[12] = 0; /* R12 */
stack[13] = 0; /* LR */
stack[14] = (uint32_t)entry; /* 返回地址,中断产生执行结束后返回地址,从中断发挥执行任务第一条语句肯定是线程函数入口地址 */
stack[15] = (1 << 24); /* PSR, 设置24位为1表示使用Thumb指令 */
thread_stacks_sp[thread_count] = (uint32_t)stack; /* 记录栈恢复的位置 */
thread_count++; /* 线程计数+1 */
}
- 对于寄存器位置的排布参考栈帧图才能更好理解。
- R4-R11、R12、LR的值可以随便给,根据AAPCS规则,R0用来参数传递函数的第一个参数,任务函数也是函数,所以R0赋值的是给任务函数的参数。
2、任务切换实现
任务切换基本要靠汇编实现,也可以C内联汇编实现,但还是使用汇编方便。
任务调度器实现:
static uint8_t os_is_starting = 0;
static uint32_t thread_stacks_sp[OS_MAX_THREADS];
static uint32_t thread_count = 0;
static uint32_t cur_thread = -1;
void os_thread_scheduler(uint32_t lr, uint32_t new_sp)
{
uint32_t prev_thread;
uint32_t sp;
if (os_is_starting == 0) return; /* 没有启动 */
if (cur_thread == -1)
{
cur_thread = 0;
sp = thread_stacks_sp[cur_thread]; /* 得到当前任务的sp */
Thread_Switch_Context(sp, lr);
}
else
{
prev_thread = cur_thread;
uint32_t next_thread = (cur_thread + 1) % thread_count;
if (prev_thread != next_thread) /* 当它们相等时只有一个任务,不用切换 */
{
thread_stacks_sp[prev_thread] = new_sp; /* 更新上一个任务的栈位置 */
sp = thread_stacks_sp[next_thread]; /* 获取当前任务SP位置 */
cur_thread = next_thread; /* 指向下一个要执行的任务 */
Thread_Switch_Context(sp, lr); /* 触发任务切换 */
}
}
}
- thread_stacks_sp[OS_MAX_THREADS],保存创建的任务的sp。
- thread_count ,任务计数
- cur_thread ,指向当前任务,当其为-1时,表示第一次切换任务。
- os_is_starting ,系统启动后再进行任务切换。
- Thread_Switch_Context,切换到下一个任务,汇编函数实现。
任务切换汇编实现:
; 在SysTick中断中实现保存上一个任务的现场
SysTick_Handler PROC
IMPORT SysTick_IRQ
STMDB sp!, {r4 - r11} ; 将当前任务的r4-r11寄存器内容到其栈中
STMDB sp!, { lr } ; 保存LR到栈中
MOV R0, LR ; 此时LR是个特殊值,保存此时的LR值,通过R0传递给函数SysTick_IRQ
ADD R1, SP, #4 ; R1 = sp + 4 得到栈的真正位置,因为多保存了LR,栈向下生长,所以需要+4才得到真正的栈点
BL SysTick_IRQ
LDMIA sp!, { r0 } ; 当系统没有,没有任务的时候则执行到这里,所以要从栈中恢复原来的寄存器内容
LDMIA sp!, {r4 - r11}
ENDP
;切换到下一个任务
Thread_Switch_Context PROC
EXPORT Thread_Switch_Context
LDMIA r0!, {r4 - r11} ; 从栈中恢复r4-r11的内容
MSR MSP, R0 ; 将MSP设置为任务的sp
BX r1 ; 通过特殊的LR值跳转触发硬件自动恢复r0、r1、r2等寄存器的
ENDP
-
ADD R1, SP, #4 才能得到任务的SP:
-
r0、r2、r3、r12
等寄存器的值中断触发会由自动保存。 -
STMDB sp!, { lr }
, 在调用SysTick_IRQ
函数前保存LR,LR此时是个特殊值,后续通过其触发硬件自动恢复自动恢复r0、r2、r3、r12
等寄存器的值。为什么要保存LR?因为SysTick_IRQ
是C函数,C函数调用会破坏原来的LR值。 -
Thread_Switch_Context
函数传入LR值,此时lr值是个特殊值,通过BX r1 触发硬件自动恢复r0、r2、r3、r12等寄存器的值。 -
Cortex-M内核有两个SP:MSP和PSP,使用哪一个实现任务切换都可以,在同一时刻只能使用其中一个,这里使用MSP。
-
在中断处理函数,如果直接访问sp,sp指向的MSP。
只是为了实现多任务切换,为了方便直接在Systick中断中进行执行调度器:
void SysTick_IRQ(uint32_t lr, uint32_t prev_thread_sp)
{
SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
os_thread_scheduler(lr, prev_thread_sp);
/* clear exception status */
SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
}
- prev_thread_sp上一个任务的sp,作为参数传进来是为了保存起来。
MDK仿真测试
uint32_t thread_a_stack[1024/4];
uint32_t thread_b_stack[1024/4];
uint32_t thread_c_stack[1024/4];
void thread_a_entry(void *arg)
{
char a = 'a';
while (1)
{
putchar(a);
// puts("\r\n");
}
}
void thread_b_entry(void *arg)
{
char b = 'b';
while (1)
{
putchar(b);
// puts("\r\n");
}
}
void thread_c_entry(void *arg)
{
int i;
int sum = 0;
for (i = 0; i < 10; i++)
{
sum += i;
}
while (1)
{
put_s_hex("sum = ", sum);
}
}
int mymain()
{
puts("os start\r\n");
os_thread_create(thread_a_entry, "Thread a", thread_a_stack, 1024);
os_thread_create(thread_b_entry, "Thread b", thread_b_stack, 1024);
os_thread_create(thread_c_entry, "Thread c", thread_c_stack, 1024);
os_start();
while(1);
return 0;
}
由于SysTick设置的1s定时加之仿真时间也不准,所以数据会疯狂打印,很久才切换到下一个任务,实现多个任务切换是没问题的。