前面介绍了操作系统的工作原理,操作系统可以看作是应用程序与底层硬件中间的管理层,对下管理各硬件设备,对上服务各应用程序。对于应用程序的编写,学习编程语言时大家就不陌生了,如果想编写多线程并发的应用程序,之前也有一个系列介绍:C++多线程并发编程,这里就不介绍上层应用程序的开发了,本文把重点放到操作系统与硬件之间的交互上。
访问硬件主要是靠操作该硬件相关的寄存器实现的,前面介绍ARM中断系统与存储管理时有过详细的介绍。要想使硬件设备工作,一般需要RCC(Reset and Clock Control)时钟系统驱动,又由于外设通信速度远低于处理器运算速度,为了不让处理器白白等待外设通信过程而浪费运算资源,还需要有中断系统的支持;要操作外设寄存器,当然也离不开存储器随机访问能力的支持。但对这些硬件外设的访问可以在需要时再进行初始化配置,也即可以在主程序或系统启动之后再访问或管理这些硬件;那么,主程序或系统是如何启动的呢?
不管有没有操作系统,一个硬件平台上运行的系统总有一个main主程序(纯汇编程序除外),操作系统的启动也是在这个main主程序内调用相应的函数实现的。那么,硬件是如何进入main主程序的呢?了解计算机原理和编译原理的朋友应该知道,硬件是不识别C/C++这类高级语言的,硬件实际执行的是机器码,C/C++高级语言又是从main函数开始的,在进入main函数之前就需要更低级的语言引导了。机器码太不直观,没法直接用机器码编写程序,处理器设计者在设计处理器时同时设计了操作该处理器寄存器的指令系统,比如ARM处理器的ARM指令集和Thumb指令集,这些指令集就是比C/C++更低级的汇编语言,由于这些指令集可以直接操作硬件寄存器,从硬件上电到进入main主程序开始执行我们的程序或启动操作系统的任务就要靠这些指令集来完成了。
处理器也属于硬件设备的一种,处理器要想工作也需要上面提到的三个条件支持:时钟系统、中断系统、存储系统。处理器要想执行指令,需要设置RCC驱动时钟;要想响应中断,需要配置中断向量表;要想取指令保存运算数据,需要配置相应支持随机寻址的存储空间。但要执行处理器的指令集来配置这三个系统,处理器需要知道第一条指令的地址PC和运行指令需要的堆栈栈顶地址SP,这个就没法靠指令设置,而只能寄希望于硬件设置了。处理器设计者也确实是这么做的,在处理器上电后,PC与SP寄存器会从硬件设置的固定的物理地址处读取该地址保存的数据并赋值给PC与SP寄存器,然后处理器就可以开始执行第一条指令了,接下来的处理器就按照其指令集编写的程序配置RCC时钟、中断向量表、内存空间等基本环境,最后进入main主程序开始执行主程序内的任务。
还记得前面介绍中断管理时谈到的中断向量表吗?下面再附一段上电后的中断向量表:
中断向量表的前两项分别保存了MSP与PC的初始值,处理器刚上电复位后,硬件会自动根据向量表偏移地址(查询VTOR向量表偏移量寄存器,在中断管理中介绍过)找到中断向量表,硬件会自动从向量表首地址(映射地址0,在CM3中实际地址为Flash起始地址0x0800 0000)处读取数据并赋给栈指针SP,然后自动从向量表第二项地址处(CM3中一般为0x0800 0004)读取数据赋给程序计数器PC,开始执行PC地址上存储的指令,PC初始值指向复位向量,所以刚上电后首先执行的是复位指令。
下面以STM32F103为例,看看中断向量表前两项的值是多少:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm\startup_stm32f10x_hd.s
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
...
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
从上面的代码来看,SP的初始值为__initial_sp,最开始的代码Stack_Mem SPACE Stack_Size分配了一段1KB(0x0000 0400)大小、不初始化(NOINIT)、可读写(READWRITE)、2^3=8字节对齐(ALIGN=3)的新内存空间,__initial_sp紧挨其后表示栈的结束地址,也即栈顶地址(别忘了栈一般是由高地址向低地址生长的)。PC的初始值为Reset_Handler,也即开始执行复位处理程序,再继续看Reset_Handler程序段的指令,主要作用是先执行SystemInit函数,再执行__main函数,下面先介绍SystemInit函数代码如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\system_stm32f10x.c
/**
* @brief Setup the microcontroller system
* Initialize the Embedded Flash Interface, the PLL and update the
* SystemCoreClock variable.
* @note This function should be used only after reset.
* @param None
* @retval None
*/
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= (uint32_t)0xEBFFFFFF;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}
从上面的代码来看,SystemInit主要是对时钟系统的操作,如果涉及到中断向量表的偏移,还有对中断向量表的操作,时钟系统主要靠RCC(Reset and Clock Control)相关寄存器配置,下面先介绍下RCC相关寄存器:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\stm32f10x.h
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define RCC ((RCC_TypeDef *) RCC_BASE)
/**
* @brief Reset and Clock Control
*/
typedef struct
{
__IO uint32_t CR;
__IO uint32_t CFGR;
__IO uint32_t CIR;
__IO uint32_t APB2RSTR;
__IO uint32_t APB1RSTR;
__IO uint32_t AHBENR;
__IO uint32_t APB2ENR;
__IO uint32_t APB1ENR;
__IO uint32_t BDCR;
__IO uint32_t CSR;
#ifdef STM32F10X_CL
__IO uint32_t AHBRSTR;
__IO uint32_t CFGR2;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)
uint32_t RESERVED0;
__IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL || STM32F10X_MD_VL || STM32F10X_HD_VL */
} RCC_TypeDef;
// Libraries\STM32F10x_StdPeriph_Driver\inc\stm32f10x_rcc.h
/** @defgroup RCC_Exported_Types
* @{
*/
typedef struct
{
uint32_t SYSCLK_Frequency; /*!< returns SYSCLK clock frequency expressed in Hz */
uint32_t HCLK_Frequency; /*!< returns HCLK clock frequency expressed in Hz */
uint32_t PCLK1_Frequency; /*!< returns PCLK1 clock frequency expressed in Hz */
uint32_t PCLK2_Frequency; /*!< returns PCLK2 clock frequency expressed in Hz */
uint32_t ADCCLK_Frequency; /*!< returns ADCCLK clock frequency expressed in Hz */
}RCC_ClocksTypeDef;
/** @defgroup RCC_Exported_Functions
* @{
*/
void RCC_DeInit(void);
void RCC_HSEConfig(uint32_t RCC_HSE);
ErrorStatus RCC_WaitForHSEStartUp(void);
void RCC_AdjustHSICalibrationValue(uint8_t HSICalibrationValue);
void RCC_HSICmd(FunctionalState NewState);
void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t RCC_PLLMul);
void RCC_PLLCmd(FunctionalState NewState);
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL) || defined (STM32F10X_CL)
void RCC_PREDIV1Config(uint32_t RCC_PREDIV1_Source, uint32_t RCC_PREDIV1_Div);
#endif
#ifdef STM32F10X_CL
void RCC_PREDIV2Config(uint32_t RCC_PREDIV2_Div);
void RCC_PLL2Config(uint32_t RCC_PLL2Mul);
void RCC_PLL2Cmd(FunctionalState NewState);
void RCC_PLL3Config(uint32_t RCC_PLL3Mul);
void RCC_PLL3Cmd(FunctionalState NewState);
#endif /* STM32F10X_CL */
void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);
uint8_t RCC_GetSYSCLKSource(void);
void RCC_HCLKConfig(uint32_t RCC_SYSCLK);
void RCC_PCLK1Config(uint32_t RCC_HCLK);
void RCC_PCLK2Config(uint32_t RCC_HCLK);
void RCC_ITConfig(uint8_t RCC_IT, FunctionalState NewState);
#ifndef STM32F10X_CL
void RCC_USBCLKConfig(uint32_t RCC_USBCLKSource);
#else
void RCC_OTGFSCLKConfig(uint32_t RCC_OTGFSCLKSource);
#endif /* STM32F10X_CL */
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);
#ifdef STM32F10X_CL
void RCC_I2S2CLKConfig(uint32_t RCC_I2S2CLKSource);
void RCC_I2S3CLKConfig(uint32_t RCC_I2S3CLKSource);
#endif /* STM32F10X_CL */
void RCC_LSEConfig(uint8_t RCC_LSE);
void RCC_LSICmd(FunctionalState NewState);
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
void RCC_RTCCLKCmd(FunctionalState NewState);
void RCC_GetClocksFreq(RCC_ClocksTypeDef* RCC_Clocks);
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
#ifdef STM32F10X_CL
void RCC_AHBPeriphResetCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
#endif /* STM32F10X_CL */
void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_BackupResetCmd(FunctionalState NewState);
void RCC_ClockSecuritySystemCmd(FunctionalState NewState);
void RCC_MCOConfig(uint8_t RCC_MCO);
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
void RCC_ClearFlag(void);
ITStatus RCC_GetITStatus(uint8_t RCC_IT);
void RCC_ClearITPendingBit(uint8_t RCC_IT);
RCC相关寄存器与封装的操作函数还是蛮多的,毕竟STM32的时钟树也算比较复杂,不管是处理器还是外设,要工作起来都需要时钟系统的周期方波驱动,毕竟不管是数据运算还是传输,都要涉及高低电平信号的变化或触发,所以时钟系统可以算作处理器与外设工作的心跳,处理器开始工作前需要先配置好时钟系统。
处理器工作除了时钟驱动源,当然还需要数据和指令的存储载体,接下来就要初始化系统运行的存储环境了。还记得前面Reset_Handler函数执行完SystemInit函数后接着继续执行__main函数吗?这里的__main函数可不是我们的主程序入口main函数,两者并不是相同的函数,需要注意区分。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。
__main函数代码没有查到,这里贴出其反汇编代码如下:
__main:
0x08000130 F000F802 BL.W __scatterload (0x08000138)
0x08000134 F000F83A BL.W __rt_entry (0x080001AC)
__scatterload:
0x08000138 A00A ADR r0,{pc}+4 ; @0x08000164
0x0800013A E8900C00 LDM r0,{r10-r11}
0x0800013E 4482 ADD r10,r10,r0
0x08000140 4483 ADD r11,r11,r0
0x08000142 F1AA0701 SUB r7,r10,#0x01
__scatterload_null:
0x08000146 45DA CMP r10,r11
0x08000148 D101 BNE 0x0800014E
0x0800014A F000F82F BL.W __rt_entry (0x080001AC)
0x0800014E F2AF0E09 ADR.W lr,{pc}-0x07 ; @0x08000147
0x08000152 E8BA000F LDM r10!,{r0-r3}
0x08000156 F0130F01 TST r3,#0x01
0x0800015A BF18 IT NE
0x0800015C 1AFB SUBNE r3,r7,r3
0x0800015E F0430301 ORR r3,r3,#0x01
0x08000162 4718 BX r3
0x08000164 0B08 DCW 0x0B08
0x08000166 0000 DCW 0x0000
0x08000168 0B28 DCW 0x0B28
0x0800016A 0000 DCW 0x0000
__scatterload_copy:
0x0800016C 3A10 SUBS r2,r2,#0x10
0x0800016E BF24 ITT CS
0x08000170 C878 LDMCS r0!,{r3-r6}
0x08000172 C178 STMCS r1!,{r3-r6}
0x08000174 D8FA BHI __scatterload_copy (0x0800016C)
0x08000176 0752 LSLS r2,r2,#29
0x08000178 BF24 ITT CS
0x0800017A C830 LDMCS r0!,{r4-r5}
0x0800017C C130 STMCS r1!,{r4-r5}
0x0800017E BF44 ITT MI
0x08000180 6804 LDRMI r4,[r0,#0x00]
0x08000182 600C STRMI r4,[r1,#0x00]
0x08000184 4770 BX lr
0x08000186 0000 MOVS r0,r0
__scatterload_zeroinit:
0x08000188 2300 MOVS r3,#0x00
0x0800018A 2400 MOVS r4,#0x00
0x0800018C 2500 MOVS r5,#0x00
0x0800018E 2600 MOVS r6,#0x00
0x08000190 3A10 SUBS r2,r2,#0x10
0x08000192 BF28 IT CS
0x08000194 C178 STMCS r1!,{r3-r6}
0x08000196 D8FB BHI 0x08000190
0x08000198 0752 LSLS r2,r2,#29
0x0800019A BF28 IT CS
0x0800019C C130 STMCS r1!,{r4-r5}
0x0800019E BF48 IT MI
0x080001A0 600B STRMI r3,[r1,#0x00]
0x080001A2 4770 BX lr
__rt_lib_init:
0x080001A4 B51F PUSH {r0-r4,lr}
__rt_lib_init_alloca_1:
0x080001A6 BD1F POP {r0-r4,pc}
__rt_lib_shutdown:
0x080001A8 B510 PUSH {r4,lr}
__rt_lib_shutdown_fp_trap_1:
0x080001AA BD10 POP {r4,pc}
__rt_entry:
0x080001AC F000FD27 BL.W __user_setup_stackheap (0x08000BFE)
0x080001B0 4611 MOV r1,r2
__rt_entry_li:
0x080001B2 F7FFFFF7 BL.W __rt_lib_init (0x080001A4)
__rt_entry_main:
0x080001B6 F000F810 BL.W main (0x080001DA)
0x080001BA F000FD45 BL.W exit (0x08000C48)
...
__use_two_region_memory:
0x08000BF8 4770 BX lr
__rt_heap_escrow$2region:
0x08000BFA 4770 BX lr
__rt_heap_expand$2region:
0x08000BFC 4770 BX lr
__user_setup_stackheap:
0x08000BFE 4675 MOV r5,lr
0x08000C00 F000F828 BL.W __user_libspace (0x08000C54)
0x08000C04 46AE MOV lr,r5
0x08000C06 0005 MOVS r5,r0
0x08000C08 4669 MOV r1,sp
0x08000C0A 4653 MOV r3,r10
0x08000C0C F0200007 BIC r0,r0,#0x07
0x08000C10 4685 MOV sp,r0
0x08000C12 B018 ADD sp,sp,#0x60
0x08000C14 B520 PUSH {r5,lr}
0x08000C16 F7FFFF6B BL.W __user_initial_stackheap (0x08000AF0)
0x08000C1A E8BD4020 POP {r5,lr}
0x08000C1E F04F0600 MOV r6,#0x00
0x08000C22 F04F0700 MOV r7,#0x00
0x08000C26 F04F0800 MOV r8,#0x00
0x08000C2A F04F0B00 MOV r11,#0x00
0x08000C2E F0210107 BIC r1,r1,#0x07
0x08000C32 46AC MOV r12,r5
0x08000C34 E8AC09C0 STM r12!,{r6-r8,r11}
0x08000C38 E8AC09C0 STM r12!,{r6-r8,r11}
0x08000C3C E8AC09C0 STM r12!,{r6-r8,r11}
0x08000C40 E8AC09C0 STM r12!,{r6-r8,r11}
0x08000C44 468D MOV sp,r1
0x08000C46 4770 BX lr
exit:
0x08000C48 4604 MOV r4,r0
0x08000C4A F3AF8000 NOP.W
0x08000C4E 4620 MOV r0,r4
0x08000C50 F7FFFAB5 BL.W __rt_exit (0x080001BE)
__user_libspace:
0x08000C54 4800 LDR r0,[pc,#0] ; @0x08000C58
0x08000C56 4770 BX lr
0x08000C58 002C DCW 0x002C
0x08000C5A 2000 DCW 0x2000
_sys_exit:
0x08000C5C 4901 LDR r1,[pc,#4] ; @0x08000C64
0x08000C5E 2018 MOVS r0,#0x18
0x08000C60 BEAB BKPT 0xAB
0x08000C62 E7FE B 0x08000C62
从代码可以看到,__main函数内部主要也是执行两个函数,其功能主要如下:
前一篇文章存储管理中介绍过ARM与STM32的存储器映射,在处理器运行过程中,代码只需要执行,所以可以放在Code段NOR Flash区(可按字节随机寻址);但全局变量与静态变量需要可读写访问,只能把其复制到SRAM内存段了,__scatterload()正好完成了初始化变量的复制和未初始化变量的初始化操作。
系统运行堆栈的大小和属性需要开发者设定,所以__rt_entry()函数实际调用了外面定义的__user_setup_stackheap函数,该函数在STM32提供的汇编文件中定义,代码如下:
// Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm\startup_stm32f10x_hd.s
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
PRESERVE8
THUMB
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
以__initial_sp为栈顶的栈Stack大小和属性设置在前面已经介绍过了,堆的设置与此类似,Heap_Mem SPACE Heap_Size分配了一段512B(0x0000 0200)大小、不初始化(NOINIT)、可读写(READWRITE)、2^3=8字节对齐(ALIGN=3)的新内存空间,其中__heap_base为堆的起始地址,__heap_limit为堆的结束地址,堆是从低地址向高地址生长的,主要用于动态内存的分配释放,比如malloc/free函数。
初始化堆栈空间和库函数后,就通过__rt_entry_main跳转到了我们的主程序入口地址main处,开始执行我们在主程序main中用C/C++编写的程序了,主程序启动到这里就完成了。
我们开发产品时,出于功能需求和成本的考虑,不会一直使用同一款处理器。更换处理器后,常需要从头开始实现所有需要的功能,但很多时候,需要的功能都是之前开发过的,重复开发浪费资源且延长了产品的开发周期。有什么办法能复用之前的代码,只修改跟处理器直接相关的代码,更上层的代码就可以直接拿来使用呢?
如果看过前面介绍的任务调度器和虚拟内存管理的文章,就会想到尝试使用分层模型来实现。在底层处理器等硬件与上层应用之间加一层中间层,这个中间层对上层的应用程序屏蔽了硬件平台差异,对下层硬件提供统一的接口进行管理,应该就能实现复用代码在不同硬件平台上的移植。对于ARM内核来说,对应的这个中间层就是CMSIS(Cortex Microcontroller Software Interface Standard),该层是直接跟硬件交互,比操作系统层更靠近硬件,CMSIS在整个开发系统中所处的位置如下所示:
有了CMSIS层,我们进行复用代码移植就相对简单了,CMSIS包含了MCU中内核及外设的软件接口标准,换一个硬件平台(比如换个处理器型号),只需要更换CMSIS层的相关接口文件就可以了,而该文件一般会由芯片厂商提供。
下面以STM32F103平台为例,介绍下固件库的移植过程,固件库包含了启动过程的相关代码,让我们专注于main函数内程序的开发。
STM32F1固件库的下载网址如下:
https://www.st.com/content/st_com/en/products/embedded-software/mcu-mpu-embedded-software/stm32-embedded-software/stm32-standard-peripheral-libraries/stsw-stm32054.html#overview
下载STM32F10x_StdPeriph_Lib_V3.5.0固件库,解压后的文件结构如下:
STM32固件库的移植重点就是对Libraries目录下CMSIS文件夹中的内容进行移植,下面详细看看CMSIS文件夹下有哪些文件:
总结下前面介绍的跟启动过程相关的CMSIS层接口文件主要有如下六个:
文件名 | 功能描述 |
---|---|
core_cm3.c core_cm3.h |
提供了进入CM3内核的接口 |
stm32f10x.h | 包含了STM32F103芯片支持的所有外设相关寄存器的结构体定义和地址 |
system_stm32f10x.c system_stm32f10x.h |
提供了设置系统与总线时钟的函数比如SystemInit |
startup_stm32f10x_hd.s | 处理器的启动文件 |
由于这些文件都是ARM与ST官方针对特定芯片型号编写好的,一般情况下不需要我们修改这些文件的内容,只需要在创建工程项目时添加或包含这些文件就可以了,如果想要使用更直观的库函数编程,还需要添加STM32F10x_StdPeriph_Driver目录下相关外设库文件进入工程中。
将固件库CMSIS与STM32F10x_StdPeriph_Driver添加进工程后,可以参照Project目录下的工程模板或示例源码,看看自己移植的固件库是否缺少什么文件。对照后发现,工程模板中多了stm32f10x_it.c、stm32f10x_it.h与stm32f10x_conf.h三个文件,前两个文件主要定义了系统异常处理函数,最后一个文件则包含了一些外设头文件,把这三个工程配置中的文件添加进你自己的工程即可。
需要提醒的一点是在stm32f10x.h文件中需要选择使用了哪个启动文件(这里选择的大容量_HD)、是否使用标准外设库,具体配置是在stm32f10x.h文件取消第70行的注释(使宏定义#define STM32F10X_HD生效)、取消第105行的注释(使宏定义#define USE_STDPERIPH_DRIVER生效)。
固件库移植完成后编写个程序测试下移植是否有问题,拿板载的LED灯作为示例,两个LED灯分别对应PB.5与PE.5,通过置高相应GPIO电平就可以让对应的LED灯亮起,下面给出示例代码:
// USER\main.c
#include "stm32f10x.h"
void Delay(u32 count)
{
u32 i=0;
for(;i<count;i++);
}
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE); //使能PB,PE端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化GPIOB.5
GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1-->PE.5推挽输出
GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化GPIO
GPIO_SetBits(GPIOE,GPIO_Pin_5); //PE.5 输出高
}
int main(void)
{
LED_Init(); //初始化LED设备
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
Delay(8000000);
GPIO_ResetBits(GPIOE,GPIO_Pin_5);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
Delay(8000000);
};
}
编译没有报错,说明没有编译时错误,看运行是否有问题。由于板子实际亮灯情况不方便展示,所以下面给出仿真结果,可以直观显示电平高低:
仿真结果跟预期一致,将HEX程序实际烧录到开发板上运行也正常,说明固件库的移植没有问题。ST固件库官方源码及移植代码:https://github.com/StreamAI/UCOS_STM32