- 工具准备:
(1)交叉编译器:
不同的开发板可能需要不同的交叉编译器,笔者之前使用的开发板型号好像是STM32F407VG-Discovery,记不太清了,目前开发板不在身边,不能真机调试,先说一些理论方面的内容。笔者选用的交叉编译器是arm-none-eabi-gcc、arm-none-eabi-gdb, 大家可以到这个地址下载最新版:https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads。
关于编译器的选择这里要简单讲一下,arm-none-eabi-gcc这个名字是依据一种特定格式的。
命名规则:
交叉编译工具链的命名规则为:
- arch [-vendor] [-os] [-(gnu)eabi]
- arch - 体系架构,如ARM,MIPS
- verdor - 工具链提供商
- os - 目标操作系统
- eabi - 嵌入式应用二进制接口
由于我们要尝试开发一款RTOS内核,是开发针对裸机的程序,所以选择了arm-none-eabi-gcc。
有时候为了定制交叉编译器,可能还需要我们自己编译一个交叉编译器以及相对应的c库(由于库体积原因,一般是用newlib这个嵌入式c库,而不用gnu的完整版c库)
(2)OpenOCD:这个是支持在线单步调试的,具体用法后面再详解,需要STLinkV2硬件
(3)Makefile:
Makefile是用来管理如何编译我们的代码的,具体Makefile知识读者可以自行百度学习。
下面这个Makefile是笔者从网上整理并修改的,大家可以简单看一下。
TOOLCHAIN_DIR=/home/jeremy/bin/gcc-arm-none-eabi-8-2018-q4-major
AS=$(TOOLCHAIN_DIR)/bin/arm-none-eabi-as
CC=$(TOOLCHAIN_DIR)/bin/arm-none-eabi-gcc
OBJCOPY=$(TOOLCHAIN_DIR)/bin/arm-none-eabi-objcopy
LD=$(TOOLCHAIN_DIR)/bin/arm-none-eabi-ld
CFLAGS=-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mthumb -mfloat-abi=soft --specs=nosys.specs -nostartfiles -Og -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -ffreestanding -fno-move-loop-invariants -Wall -Wextra -g3 -DDEBUG -DUSE_FULL_ASSERT -DTRACE -DOS_USE_TRACE_SEMIHOSTING_DEBUG -DSTM32F407xx -DUSE_HAL_DRIVER -std=gnu11 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@)" #-c -o "$@" "$<"
# CFLAGS += -fpic -pie
CFLAGS+=-I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/cmsis -I$(INCLUDE_DIR)/stm32f4-hal
SUBDIRS=$(shell ls -l | grep ^d | awk '{if($$9 != "bin" && $$9 != "include") print $$9}')
ROOT_DIR=$(shell pwd)
INCLUDE_DIR=$(ROOT_DIR)/include
ELF=bootloader.elf
BIN=bootloader.bin
BIN_DIR=$(ROOT_DIR)/bin
CUR_DIR=$(shell pwd)
CUR_SOURCE=$(wildcard $(CUR_DIR)/src/*.c)
CUR_ASM_SRC=$(wildcard $(CUR_DIR)/src/*.s)
CUR_OBJS=$(patsubst %.c, %.o, $(CUR_SOURCE))
CUR_ASM_OBJS=$(patsubst %.s, %.o, $(CUR_ASM_SRC))
# OBJS=$(shell ls -l $(OBJS_DIR) | grep ^- | awk '{print $9}' | sed "s:^:$(OBJS_DIR)/:")
OBJS+=$(CUR_OBJS) $(CUR_ASM_OBJS)
# OBJS=$(wildcard $(OBJS_DIR)/*.o)
# OBJ=kernel_obj.elf
export CC ROOT_DIR CFLAGS OBJS
all: subdirs bootloader
target: subdirs
$(CC) $(CFLAGS) -T $(ROOT_DIR)/bootloader.ld $(OBJS) -o $(BIN_DIR)/$(ELF)
# $(LD) -T $(ROOT_DIR)/kernel.ld -L /Users/apple/bin/gcc-arm-none-eabi-5_4-2016q3/lib/gcc/arm-none-eabi/5.4.1/ -lgcc $^ -o $(BIN_DIR)/$(ELF)
bootloader: target
$(OBJCOPY) -O binary $(BIN_DIR)/$(ELF) $(BIN_DIR)/$(BIN)
define build-dir
echo building $1 with action $2
make -C $1 $2
endef
subdirs: $(SUBDIRS)
# $(foreach dir, $(SUBDIRS), $(call build-dir $(dir)))
make -C src
%.o:%.c
$(CC) $(CFLAGS) -c $^ -o $@
%.o:%.s
$(CC) $(CFLAGS) -c $^ -o $@
clean: $(SUBDIRS)
# $(foreach dir, $(SUBDIRS), $(call build-dir $(dir) clean))
make -C src clean
rm -f $(CUR_DIR)/*.o
rm -f $(BIN_DIR)/*
里面注意一个CFLAGS选项 -nostartfiles,这个flag的作用是不链接c库的crt0.c(c运行时库代码),这基本意味着malloc和printf等函数已经不可用了,笔者打算是用开发板自带的uart接口,自己实现printf的简单实现,以及自己的内存管理模块。当然读者如果感兴趣也可以自己实现crt0.c里面的stub函数,来支持标准的malloc和printf等函数,但是这需要自己使用crosstool-NG编译交叉编译器和c库。也可以通过semihosting的方式进行调试,读者可以了解一下semihosting的机制。
(4)链接脚本:
这个的功能可以总结为是控制把某些数据放到指定地方的链接器配置脚本,大家也可以百度学习。下面是笔者使用的链接脚本的例子。
/*
* Memory Spaces Definitions.
*
* Need modifying for a specific board.
* FLASH.ORIGIN: starting address of flash
* FLASH.LENGTH: length of flash
* RAM.ORIGIN: starting address of RAM bank 0
* RAM.LENGTH: length of RAM bank 0
*
* The values below can be addressed in further linker scripts
* using functions like 'ORIGIN(RAM)' or 'LENGTH(RAM)'.
*/
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
FLASHB1 (rx) : ORIGIN = 0x00000000, LENGTH = 0
EXTMEMB0 (rx) : ORIGIN = 0x00000000, LENGTH = 0
EXTMEMB1 (rx) : ORIGIN = 0x00000000, LENGTH = 0
EXTMEMB2 (rx) : ORIGIN = 0x00000000, LENGTH = 0
EXTMEMB3 (rx) : ORIGIN = 0x00000000, LENGTH = 0
MEMORY_ARRAY (xrw) : ORIGIN = 0x20002000, LENGTH = 32
}
/*
* For external ram use something like:
RAM (xrw) : ORIGIN = 0x64000000, LENGTH = 2048K
*/
/*
* Default linker script for Cortex-M (it includes specifics for STM32F[34]xx).
*
* To make use of the multi-region initialisations, define
* OS_INCLUDE_STARTUP_INIT_MULTIPLE_RAM_SECTIONS for the _startup.c file.
*/
/* OUTPUT_FORMAT("elf32littlearm","elf32littlearm","elf32littlearm") */
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler)
/*
* The '__stack' definition is required by crt0, do not remove it.
*/
__stack = ORIGIN(RAM) + LENGTH(RAM);
_estack = __stack - 0x010000; /* STM specific definition */
/*
* Default stack sizes.
* These are used by the startup in order to allocate stacks
* for the different modes.
*/
/* __Main_Stack_Size = 1024 ;
//PROVIDE ( _Main_Stack_Size = __Main_Stack_Size ) ;
//__Main_Stack_Limit = __stack - __Main_Stack_Size ;
*/
/* "PROVIDE" allows to easily override these values from an
* object file or the command line. */
/*
//PROVIDE ( _Main_Stack_Limit = __Main_Stack_Limit ) ;
*/
/*
* There will be a link error if there is not this amount of
* RAM free at the end.
*/
/* */
_Minimum_Stack_Size = 256 ;
/*
* Default heap definitions.
* The heap start immediately after the last statically allocated
* .sbss/.noinit section, and extends up to the main stack limit.
*/
/*
//PROVIDE ( _Heap_Begin = _end_noinit ) ;
//PROVIDE ( _Heap_Limit = __stack - __Main_Stack_Size ) ;
*/
/*
* The entry point is informative, for debuggers and simulators,
* since the Cortex-M vector points to it anyway.
*/
/*
//ENTRY(_start)
*/
/* Sections Definitions */
SECTIONS
{
. = 0x08000000;
. = ALIGN(4);
/*
* For Cortex-M devices, the beginning of the startup code is stored in
* the .isr_vector section, which goes to FLASH.
*/
.isr_vector :
{
FILL(0xFF)
__vectors_start = ABSOLUTE(.) ;
__vectors_start__ = ABSOLUTE(.) ; /* STM specific definition */
KEEP(*(.isr_vector)) /* Interrupt vectors */
KEEP(*(.cfmconfig)) /* Freescale configuration words */
/*
* This section is here for convenience, to store the
* startup code at the beginning of the flash area, hoping that
* this will increase the readability of the listing.
*/
*(.after_vectors .after_vectors.*) /* Startup code and ISR */
__vectors_end = ABSOLUTE(.) ;
__vectors_end__ = ABSOLUTE(.) ; /* STM specific definition */
} > FLASH
/*
* The program code is stored in the .text section,
* which goes to FLASH.
*/
. = ALIGN(4);
.text :
{
__text_start__ = .;
*(.text .text.*) /* all remaining code */
/* read-only data (constants) */
*(.rodata .rodata.* .constdata .constdata.*)
/* *(vtable) /* C++ virtual tables */
KEEP(*(.eh_frame*))
/*
* Stub sections generated by the linker, to glue together
* ARM and Thumb code. .glue_7 is used for ARM code calling
* Thumb code, and .glue_7t is used for Thumb code calling
* ARM code. Apparently always generated by the linker, for some
* architectures, so better leave them here.
*/
*(.glue_7)
*(.glue_7t)
}> FLASH
/* ARM magic sections */
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
}> FLASH
. = ALIGN(4);
__exidx_start = .;
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
}> FLASH
__exidx_end = .;
_etext = .;
__etext = .;
. = ALIGN(4);
/*
* For some STRx devices, the beginning of the startup code
* is stored in the .flashtext section, which goes to FLASH.
*/
.flashtext :
{
*(.flashtext .flashtext.*) /* Startup code */
}> FLASH
. = ALIGN(4);
.index :
{
__data_array_start = .;
LONG(ADDR(.data));
LONG(LOADADDR(.data));
LONG(SIZEOF(.data));
__data_array_end = .;
__bss_array_start = .;
LONG(ADDR(.bss));
LONG(SIZEOF(.bss));
__bss_array_end = .;
__bootloader_end = .;
LONG(LOADADDR(.bss)+SIZEOF(.bss));
} >FLASH
. = ALIGN(4);
__data_lma = .;
/*
* The initialised data section.
*
* The program executes knowing that the data is in the RAM
* but the loader puts the initial values in the FLASH (inidata).
* It is one task of the startup to copy the initial values from
* FLASH to RAM.
*/
. = 0x20010200;
_sidata = .;
.data _sidata :
{
. = ALIGN(4);
FILL(0xFF)
/* This is used by the startup code to initialise the .data section */
_sdata = . ; /* STM specific definition */
__data_start__ = . ;
*(.data_begin .data_begin.*)
*(.data .data.*)
*(.data_end .data_end.*)
. = ALIGN(4);
/* This is used by the startup code to initialise the .data section */
_edata = . ; /* STM specific definition */
__data_end__ = . ;
} > RAM AT>FLASH
. = ALIGN(4);
__bss_start__ = .;
__bss_start = . ; /* standard newlib definition */
/* The primary uninitialised data section. */
.bss (NOLOAD):
{
_sbss = .; /* STM specific definition */
*(.bss_begin .bss_begin.*)
*(.bss .bss.*)
*(COMMON)
*(.bss_end .bss_end.*)
_ebss = . ; /* STM specific definition */
} > RAM
. = ALIGN(4);
__bss_end = .; /* standard newlib definition */
__bss_end__ = .;
__end__ = .;
end = .;
/* Remove information from the standard libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
}
- 实现一个RTOS内核所需的理论支持:
CPU地址空间:目前市面上大多数的32位MCU最大支持4G的地址空间(如果没有PAE功能),目前的MCU大部分都是IO内存寻址的,也就是说外设的部分寄存器和设备内存空间占用了4G中的一部分地址空间,能支持直接映射的SRAM或DRAM是达不到4G的。一个硬件平台的外设和RAM的映射地址一般是不可变化的,我们目前也只关注这种情况。大家心里可以形成一个概念,往某个固定地址写入数据可能就是往RAM中写,从某个地址读,就是从外设的寄存器读取数据。
- RTOS包含的功能模块:
RTOS的功能基本就是管理硬件资源,基本包括内存管理,Task切换管理,中断异常管理,消息管理。目前实现的RTOS内核是不考虑MMU的,最多有对MPU的支持。统一地址空间,不区分内核态和用户态。贴一张RTOS常见的架构图:
- RTOS的引导:
类似Linux常用的UBoot,RTOS也可以有自己的bootloader,这些loader都是很专用的,功能很简单就是基本把copy内核到特定地址(如果需要的话),以及copy数据段,设置BSS段等。上面给出的Makefile和*.ld链接脚本就是针对笔者实现的bootloader的。下期会简单聊一下BootLoader的简单实现,感兴趣的读者可以继续关注。