我们已经可以用裸机点亮LED了,接下来我们要使用OS点亮LED了。
现在的问题是我们还没有OS,哈哈哈哈哈哈,为了学习,还是从0写一个XHOS吧。
我们是为了介绍OS而介绍OS,正常流程应该是先说说裸机和OS的优缺点(详情点击这里),然后再引入OS。
现在假设大家已经知道了一些必要的知识,来看看我们的OS(当然是非常简单的)。
.
├── Makefile // 编译脚本
├── start.s // 启动文件
├── user_main.c // 用户代码,main函数在此
├── xhos.c // OS的实现
├── XHOS.h // OS头文件,要使用xhos要包含该头文件
└── xhos_stm32.ld // 链接脚本
这里面的文件比较多,牵扯到的内容很丰富,先列举出来,然后慢慢解释。
先看user_main.c
// user_main.c
/*
* XHOS 一个为了学习而生的OS
* 官方网站:findxiaohei.xyz
*/
// XHOS创建闪烁LED task
#include "XHOS.h"
/* 方便C语言操作寄存器 */
#define RCC_BASE 0x40021000
#define RCC_CR (*(volatile unsigned long *)(RCC_BASE))
#define RCC_APB2ENR (*(volatile unsigned long *)(RCC_BASE+0x18))
#define GPIOC_BASE 0x40011000
#define GPIOC_CRH (*(volatile unsigned long *)(GPIOC_BASE+0x04))
#define GPIOC_ODR (*(volatile unsigned long *)(GPIOC_BASE+0x0c))
void Delay(uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
/* 定义task */
void task_func(void)
{
int i;
RCC_APB2ENR = 1<<4; //portC 时钟使能
GPIOC_CRH = 3<<20; // pin13 输出模式
GPIOC_ODR = 0;
while(1){
GPIOC_ODR |= 1<<13; //pin13 高电平
Delay(100000);
GPIOC_ODR &= ~(1<<13); //pin13 低电平
Delay(100000);
}
}
int main()
{
// 创建task
osTaskCreate(task_func);
// 启动OS调度
osStartScheduler();
// 不会到达这里
while (1);
return 0;
}
这个是用户的入口文件,在[xhOS]03裸机点亮LED 中main函数直接闪烁LED,而有OS的情况下,main函数通过OS提供的接口osTaskCreate创建了一个闪烁LED的任务(task_func).
任务是一个永不退出的死循环。
然后通过osStartScheduler启动OS的调度,本节中就是启动闪烁LED的task。
start.s
start.s负责从上电到执行main函数的准备工作
// start.s
/*
* XHOS 一个为了学习而生的OS
* 官方网站:findxiaohei.xyz
*/
/* start.S */
.syntax unified /* 统一汇编语法 UAL*/
.cpu cortex-m3
.fpu softvfp
.thumb /* thumb 指令集 */
/* 全局符号 */
.global Vectors
/*****************启动代码**********************/
/* 指定当前段名 */
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
/* 入口点 */
Reset_Handler:
/* 将数据从 flash copy 到 SRAM */
movs r1, #0
b LoopCopyDataInit
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
LoopCopyDataInit:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
ldr r2, =_sbss
b LoopFillZerobss
/* 用0初始化BSS段 */
FillZerobss:
movs r3, #0
str r3, [r2], #4
LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss
/* 调用用户实现的main函数 (这就是为什么用户程序以main开始了。当然也可以是别的)*/
bl main
bx lr
.size Reset_Handler, .-Reset_Handler
/* 所有数据段的开始,在链接脚本中定义,从这里开始拷贝数据到SRAM */
.word _sidata
/* .data section的开始地址. 链接脚本中定义 */
.word _sdata
/* .data section的结束地址. 链接脚本中定义 */
.word _edata
/* .bss section的开始地址. 链接脚本中定义 */
.word _sbss
/* .bss section的结束地址. 链接脚本中定义 */
.word _ebss
/* 栈顶. 链接脚本中定义 */
.word _estack
/*****************异常向量表**********************/
/* 指定向量表所在的段名 */
.section .isr_vector,"a",%progbits
.type Vectors, %object
.size Vectors, .-Vectors
Vectors:
.word _estack /*sram 最后*/
.word Reset_Handler
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word 0
.word 0
.word 0 //PendSV_Handler
.word 0 //SysTick_Handler
这里比裸机点亮LED多了很多内容。
XHOS.h
定义了OS的API和基本的数据结构。
// XHOS.h
/*
* XHOS 一个为了学习而生的OS
* 官方网站:findxiaohei.xyz
*/
#ifndef XHOS_H
#define XHOS_H
/*
* XHOS的头文件,使用XHOS必须包含该文件
*/
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef uint32_t address_t;
typedef long Base_t;
typedef unsigned long UBase_t;
// 定义函数指针,用于任务创建。
typedef void (*TaskFunction_t)( void );
// 任务控制块,就是传说中的TCB
typedef struct TaskControlBlock
{
volatile address_t *TopOfStack;
}TCB_t;
#define NULL 0
///
// 创建task
void osTaskCreate(TaskFunction_t task_func);
// 启动OS调度
void osStartScheduler();
#ifdef __cplusplus
}
#endif
#endif
xhos.c
xhOS的主要实现部分,包括task的创建,和如何切换到task。
// xhos.c
/*
* XHOS 一个为了学习而生的OS
* 官方网站:findxiaohei.xyz
*/
#include "XHOS.h"
/*
* XHOS 的实现 v0.1, 最小OS
*/
#define devCSB_VTOR ( * ( ( volatile uint32_t * ) 0xE000ED08 ) )
#define devINITIAL_XPSR ( 0x01000000UL )
#define devSTART_ADDRESS_MASK ( ( address_t ) 0xfffffffeUL )
TCB_t * g_Current;
extern void test_err();
TCB_t task1tcb;
Base_t task_stack[129];
address_t *devInitialiseStack( address_t *TopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* 制造现场,好像该任务是被切换除去,用同样的方法可以切入 */
TopOfStack--;
*TopOfStack = devINITIAL_XPSR; /* xPSR */
TopOfStack--;
*TopOfStack = ( ( address_t ) pxCode ) & devSTART_ADDRESS_MASK; /* PC */
TopOfStack--;
*TopOfStack = ( address_t ) 0; /* LR */
TopOfStack -= 5; /* R12, R3, R2 and R1. */
*TopOfStack = ( address_t ) pvParameters; /* R0 */
TopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return TopOfStack;
}
void SVC_Handler( void )
{
__asm volatile (
" ldr r3, CurrentTCB \n" /* 获取task的TCB */
" ldr r1, [r3] \n"
" ldr r0, [r1] \n" /* TCB 中第一个就是栈顶 */
" ldmia r0!, {r4-r11} \n" /* 将栈中保存的寄存器值恢复到寄存器 */
" msr psp, r0 \n" /* PSP指向用户task的栈顶 */
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" orr r14, #0xd \n" /* 用于使CPU返回用户模式 */
" bx r14 \n"
" \n"
" .align 4 \n"
"CurrentTCB: .word g_Current \n"
);
}
/*-----------------------------------------------------------*/
static void StartFirstTask( void )
{
// 重定位中断向量
devCSB_VTOR = 0x08000000;
__asm volatile(
" ldr r0, =0xE000ED08 \n" /* 从NVIC 的VTOR中获取中断向量表,向量表的第一个就是栈顶 */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n" /* 将msp设置为栈顶 */
" cpsie i \n" /* 打开中断 */
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n" /* 使用SVC启动第一个task */
" nop \n"
);
}
// 创建task
void osTaskCreate(TaskFunction_t task_func){
g_Current = &task1tcb;
g_Current->TopOfStack = devInitialiseStack(task_stack+128, task_func, NULL);
}
// 启动OS调度
void osStartScheduler(){
/* Start the first task. */
StartFirstTask();
/* Should not get here! */
return ;
}
xhos_stm32.ld
各部分是如何链接成一个可执行的文件。
/* xhos_stm32.ld */
/*
* GCC linker script for STM32 (ARM Cortex-M).
*/
/* 定义memory,根据实际设备调整起始地址和大小 */
MEMORY
{
flash (rx) : ORIGIN = 0x08000000, LENGTH = 64K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
/*
* The stack starts at the end of RAM and grows downwards. Full-descending
* stack; decrement first, then store.
*/
/* 根据memory计算栈顶,因为ARM Cortex-M是满减栈,所以是ram最高地址 */
_estack = ORIGIN(ram) + LENGTH(ram);
/* 输出段 */
SECTIONS
{
/* 最开始是异常向量,其中有栈顶和入口点等 */
.isr_vector :
{
__isr_vector_start__ = .;
KEEP(*(.isr_vector)) /* 使用'KEEP'是为了防止链接器发现没人直接使用本段而不链接 */
ASSERT(. != __isr_vector_start__, "The .isr_vector section is empty");
} >flash
/* Text section (code and read-only data) */
.text :
{
/* 4字节对齐地址 */
. = ALIGN(4);
_stext = .; /* 当前地址赋值给 _xtext 变量*/
*(.text*) /* code ,所有 输入段为".text*"的段都被链接到这里 */
*(.rodata*) /* read only data */
. = ALIGN(4);
_etext = .;
} >flash
/* 已经初始化的 data section. 启动代码将本段从FLASH 拷贝到 RAM */
_sidata = .;
.data : AT(_sidata) /* 链接到FLASH 的地址 _sidata,但需要拷贝的RAM中运行 */
{
. = ALIGN(4);
_sdata = .; /* 这里就是RAM中的偏移了,从0x20000000开始 */
*(.data*)
. = ALIGN(4);
_edata = .;
} >ram /* 最终运行在ram中 */
/* 未初始化的 data section. 启动代码统一置0 */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >ram
}
Makefile
# file :Makefile
All:
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c start.s -o start.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c user_main.c -o main.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c xhos.c -o xhos.o
arm-none-eabi-ld -Txhos_stm32.ld start.o main.o xhos.o -o osLED
arm-none-eabi-objcopy -O binary -S osLED osLED.bin
arm-none-eabi-objdump -D -m arm osLED > osLED.dis
clean:
-rm *.o osLED*
flash: clean All
sudo stm32flash -w osLED.bin -v -g 0 /dev/ttyUSB0
把这6个文件创建好,执行make命令,就能编译一个可以使用task闪烁LED的最小OS了。
各个文件已经有了很多注释,但是对于新手来说,还是太复杂了,我后续将详细介绍每一个文件。
访问findxiaohei.xyz获取更多。