1. 输入输出

将printf打印的字符输出到串行口(或者其它接口)的技术一般称为“重定向”,重定向也能处理用户输入和系统函数。

2. 开发流程

程序生成流程:

RVDS编译流程.png程序生成流程

根据所使用开发工具不同,链接器可能会使用命令行选项来指定内存分布。不过,使用GNU C编译器的工程在内存分布较为复杂时,可能需要一个链接文件。对于ARM开发工具,链接文件被称为分散加载文件。

生成可执行映像文件后,可以将其下载到微控制器的Flash存储器或内存中进行测试,整个过程相对简单。

程序下载流程程序下载流程.png

3. 程序映像

Cortex-M0的程序映像一般包含以下几个部分:

  • 向量表

  • C启动例程

  • 程序代码(应用程序代码和数据)

  • C库代码(C库函数的程序代码,链接时插入)

3.1 向量表

向量表可以用C语言或汇编实现。由于向量表的入口需要编译器和链接器生成的内容,所以向量表代码的实现细节是同开发工具链相关的。

例如:

栈指针的初始值被链接到链接器生成的栈空间地址,而复位向量则指向了C启动代码的地址,这些都是同编译器相关的。

有些工具,包括 Keil MDK,则将向量表作为汇编启动代码的一部分,并且使用定义常量数据(DCD)指令创建。

; 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     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     SVC_Handler                    ; SVCall Handler
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     PendSV_Handler                 ; PendSV Handler
                DCD     SysTick_Handler                ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler                ; Window Watchdog
                DCD     PVD_IRQHandler                 ; PVD through EXTI Line detect
                DCD     RTC_IRQHandler                 ; RTC through EXTI Line
                DCD     FLASH_IRQHandler               ; FLASH
                DCD     RCC_IRQHandler                 ; RCC
                DCD     EXTI0_1_IRQHandler             ; EXTI Line 0 and 1
                DCD     EXTI2_3_IRQHandler             ; EXTI Line 2 and 3
                DCD     EXTI4_15_IRQHandler            ; EXTI Line 4 to 15
                DCD     TS_IRQHandler                  ; TS
                
                ...
                
                DCD     CEC_IRQHandler                 ; CEC
                DCD     0                              ; Reserved
                
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

注意,向量表被赋以一个段名RESET(Reset_Handler),为了将向量表置于系统存储器映射的开头(如:0x00000000),链接文件或命令行选项需要知道 段的名字,以便链接器能够正确识别向量并将其进行地址映射。

; Reset handler routine
Reset_Handler    PROC
                 EXPORT  Reset_Handler                 [WEAK]
        IMPORT  __main
        IMPORT  SystemInit  
                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

复位向量一般指向C启动代码的开头,不过,也可以自己定义复位处理,在跳转到C启动代码前执行附加的初始化操作。

3.2 C启动代码

C启动代码用于设置像全局变量之类的数据,也会清零加载时未初始化的内存区域。对于使用malloc()等C函数的应用程序,C启动代码还需要初始化堆空间的空间变量。初始化后,启动代码跳转到main()程序执行。

C启动代码由编译器/链接器自动嵌入到程序中,并且和开发工具链相关的。对于ARM编译器,C启动代码被标识为 "__main'', 而GNU C编译器生成的代码则通常编辑为 "__start"。

3.3 程序代码

用户指定的任务由一个应用程序生成的指令完成的,除了指令外,还有以下各类数据:

  • 变量初始值

    函数或子程序中的局部变量需要初始化,这些初始值会在程序执行期间被赋给相应的变量。

  • 程序代码中的常量

    如:数据值、外设地址、字符串等,需要在程序映像中一般作为数据块放在一起。

  • 其它常量

    有些应用程序可能也会包含其它常量。比如:查找表和图像数据,它们也被合并在程序映像中。

3.4 C库代码

当使用特定C/C++库函数时,它们的库代码就会由链接器嵌入到程序映像中。

有些开发工具提供多个版本的C函数库,并且用途不同。例如,keil MDK可以通过选项配置,选择使用Microlib的特殊版本C函数库。

Microlib体积小,专门用于微控制器,而且没有实现C标准库的全部功能。

3.5 启动文件介绍

启动文件由汇编编写,是系统上电复位后第一个执行的程序。

详解 startup_stm32f0xx.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,
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,
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8
                THUMB


; 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     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     SVC_Handler                    ; SVCall Handler
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     PendSV_Handler                 ; PendSV Handler
                DCD     SysTick_Handler                ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler                ; Window Watchdog
                DCD     PVD_IRQHandler                 ; PVD through EXTI Line detect
                DCD     RTC_IRQHandler                 ; RTC through EXTI Line
                DCD     FLASH_IRQHandler               ; FLASH
                DCD     RCC_IRQHandler                 ; RCC
                DCD     EXTI0_1_IRQHandler             ; EXTI Line 0 and 1
                DCD     EXTI2_3_IRQHandler             ; EXTI Line 2 and 3
                DCD     EXTI4_15_IRQHandler            ; EXTI Line 4 to 15
                DCD     TS_IRQHandler                  ; TS
                
                ...
                
                DCD     CEC_IRQHandler                 ; CEC
                DCD     0                              ; Reserved
                
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY

; Reset handler routine
Reset_Handler    PROC
                 EXPORT  Reset_Handler                 [WEAK]
        IMPORT  __main
        IMPORT  SystemInit  
                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler     PROC
                EXPORT  NMI_Handler                    [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler              [WEAK]
                B       .
                ENDP
SVC_Handler     PROC
                EXPORT  SVC_Handler                    [WEAK]
                B       .
                ENDP
PendSV_Handler  PROC
                EXPORT  PendSV_Handler                 [WEAK]
                B       .
                ENDP
SysTick_Handler PROC
                EXPORT  SysTick_Handler                [WEAK]
                B       .
                ENDP

Default_Handler PROC

                EXPORT  WWDG_IRQHandler                [WEAK]
                EXPORT  PVD_IRQHandler                 [WEAK]
                EXPORT  RTC_IRQHandler                 [WEAK]
                EXPORT  FLASH_IRQHandler               [WEAK]
                EXPORT  RCC_IRQHandler                 [WEAK]
                EXPORT  EXTI0_1_IRQHandler             [WEAK]
                EXPORT  EXTI2_3_IRQHandler             [WEAK]
                EXPORT  EXTI4_15_IRQHandler            [WEAK]
                EXPORT  TS_IRQHandler                  [WEAK]
                
                ...
                
                EXPORT  CEC_IRQHandler                 [WEAK]


WWDG_IRQHandler
PVD_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_1_IRQHandler
EXTI2_3_IRQHandler
EXTI4_15_IRQHandler
TS_IRQHandler

...

CEC_IRQHandler   

                B       .

                ENDP

                ALIGN

;*******************************************************************************
; 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

;************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE*****

如上,是系统上电复位后第一个执行的程序,主要工作如下:

  • 初始化堆栈指针(SP = __initial_sp)

  • 初始化PC指针(Reset_Handler)

  • 初始化中断向量表

  • 配置系统时钟(SystemInit)

  • 跳转到main函数(__main)

3.5.1 栈定义

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE,
Stack_Mem       SPACE   Stack_Size
__initial_sp

开辟栈大小为0x400,即1KB字节。名字为STACK,NOINIT即不初始化,可读可写,8(2^3)字节对齐。

EQU:宏定义的伪指令,类似C中define;

AREA:告诉汇编器汇编一个新的代码段或数据段。STACK表示段名;NOINIT表示不初始化;READWRITE表示可读可写;ALIGN=3,表示按照2^3字节对齐。

SPACE:用于分配一定大小的内存空间,单位为字节。

__initial_sp : 紧挨着SPACE语句放置,表示栈的结束地址,即栈顶地址,因为栈是由高往低生长。

3.5.2 堆定义

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE,
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

开辟栈大小为0x200,即512字节。名字为HEAP,NOINIT即不初始化,可读可写,8(2^3)字节对齐。

__heap_base : 表示堆的起始地址;

__heap_limit : 表示堆的结束地址;堆是由低向高生长,跟栈的生长方向相反。

3.5.3 THUMB指令

                PRESERVE8
                THUMB

PRESERVE8 : 指定当前文件的堆栈按照8字节对齐;

THUMB : 表示后面指令兼容THUMB指令。

3.5.4 向量表

; 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     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     SVC_Handler                    ; SVCall Handler
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
                DCD     PendSV_Handler                 ; PendSV Handler
                DCD     SysTick_Handler                ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler                ; Window Watchdog
                DCD     PVD_IRQHandler                 ; PVD through EXTI Line detect
                DCD     RTC_IRQHandler                 ; RTC through EXTI Line
                DCD     FLASH_IRQHandler               ; FLASH
                DCD     RCC_IRQHandler                 ; RCC
                DCD     EXTI0_1_IRQHandler             ; EXTI Line 0 and 1
                DCD     EXTI2_3_IRQHandler             ; EXTI Line 2 and 3
                DCD     EXTI4_15_IRQHandler            ; EXTI Line 4 to 15
                DCD     TS_IRQHandler                  ; TS
                
                ...
                
                DCD     CEC_IRQHandler                 ; CEC
                DCD     0                              ; Reserved
                
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

定义个数据段,名字为RESET,只可读。并声明 __Vectors、__Vectors_End 和__Vectors_Size 这三个标号。

EXPORT : 声明一个标号可被外部文件使用,使标号具有全局属性。若为IAR编译器,则使用GLOBAL指令声明;

DCD : 分配一个或多个以字为单位内存,并要求初始化这些内存。向量表中,DCD分配了一堆内存,并以异常服务函数的入口地址初始化。

__Vectors : 向量表起始地址;

__Vectors_End :向量表结束地址;

__Vectors_Size : 向量表大小;

3.5.5 复位程序

                AREA    |.text|, CODE, READONLY

; Reset handler routine
Reset_Handler    PROC
                 EXPORT  Reset_Handler                 [WEAK]
        IMPORT  __main
        IMPORT  SystemInit  
                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

定义一个名为 .text 的代码段,只可读。

PROC : 定义子程序,与 ENDP 成对使用,表示子程序结束;

IMPORT : 表示该标号来自外部,类似C语言中的EXTERN关键字。这里表示 SystemInit 和 __main这两个函数均来自外部的文件。

WEAK : 表示弱定义,如果外部文件优先定义了该标号,则首先引用该标号;如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。

SystemInit : 一个标准库函数,在 system_stm32f10x.c 库文件中定义。主要作用是配置时钟。

__main : 标准C库函数,主要作用初始化用户堆栈,并在函数最后调用main函数去到C世界。

3.5.6 中断服务程序

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler     PROC
                EXPORT  NMI_Handler                    [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler              [WEAK]
                B       .
                ENDP
SVC_Handler     PROC
                EXPORT  SVC_Handler                    [WEAK]
                B       .
                ENDP
PendSV_Handler  PROC
                EXPORT  PendSV_Handler                 [WEAK]
                B       .
                ENDP
SysTick_Handler PROC
                EXPORT  SysTick_Handler                [WEAK]
                B       .
                ENDP

Default_Handler PROC

                EXPORT  WWDG_IRQHandler                [WEAK]
                EXPORT  PVD_IRQHandler                 [WEAK]
                EXPORT  RTC_IRQHandler                 [WEAK]
                EXPORT  FLASH_IRQHandler               [WEAK]
                EXPORT  RCC_IRQHandler                 [WEAK]
                EXPORT  EXTI0_1_IRQHandler             [WEAK]
                EXPORT  EXTI2_3_IRQHandler             [WEAK]
                EXPORT  EXTI4_15_IRQHandler            [WEAK]
                EXPORT  TS_IRQHandler                  [WEAK]
                
                ...
                
                EXPORT  CEC_IRQHandler                 [WEAK]


WWDG_IRQHandler
PVD_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_1_IRQHandler
EXTI2_3_IRQHandler
EXTI4_15_IRQHandler
TS_IRQHandler

...

CEC_IRQHandler   

                B       .

                ENDP

启动文件里面已经写好所有中断服务函数,这些函数默认都是空的,只是提前占一个位置而已。

若使用中,开启了某个中断,但是忘记了编写配套的中断服务程序或者函数名错误,那么中断来临时,程序就会跳转到启动文件预先写好的空的函数服务程序中,并且在这个空函数中无线循环,即程序死在这里。

3.5.7 堆栈初始化

    ALIGN

;*******************************************************************************
; 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

;************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE*****

ALIGN : 对指令或数据存放的地址进行对齐,后面会跟一个立即数。缺省表示4字节对齐。

首先判断是否定义了 __MICROLIB,如果定义了这个宏,则赋予标号 __initial_sp 、__heap_base、__heap_limit 全局属性,可供外部文件调用。否则,就需要自己定义__user_initial_stackheap。

最终,堆栈的初始化由C库函数 __main 来完成。

参考来源:《零死角玩转STM32—F103霸道.pdf》

4. RAM中的数据

像ROM一样,微控制器的RAM也有很多种用法。典型地,RAM一般可以分为数据、栈和堆区域。

对于嵌入式操作系统(如:υClinux)或RTOS(如:Keil RTX)的微控制器系统,每个任务的栈空间都是独立的。

有些操作系统允许用户自定义任务的栈,这样也就需要更大的栈空间。有些操作系统则将内存分为若干段,每个任务分配一个段,用于各自的数据、栈和堆区域。

4.1 无OS的系统RAM

  • 数据

    数据存储在内存的底部,包含全局变量和静态变量(注意:为了节省内存,可以将局部变量分配在栈上,而且函数内未使用的局部变量不占用存储器空间)。

  • 栈空间用于临时数据存储(PUSH和POP操作),局部变量的存储空间、函数调用参数传递和异常处理的寄存器备份等。Thumb指令集使用一种栈指针相关的寻址模式处理数据访问,这种方式非常高效,而且在访问栈空间数据时,需要很小的指令开销。

  • 对存储用于C函数自动分配存储器的区域,例如:alloc()和malloc(),以及其它使用这些函数的函数调用。为了确保这些函数能够正常分配存储器空间,C启动代码需要初始化堆存储及其控制变量。

无OS_RAM无OS_RAM.png

4.2 带OS的系统RAM

带OS的RAM.png带OS的RAM

5. 用C语言操作外设

除了变量以外,微控制器的C应用程序通常需要操作外设。对于ARM Cortex-M0微控制器,外设寄存器被映射到系统存储器空间,它们可以通过指针进行访问。

typdef struct{
	volatile unsigned long DATA;			// 0x00	
	volatile unsigned long PSR;				// 0x04
	volttile unsigned long RESERVED0[4];	// 0x08 - 0x14
	......
	volatile unsigned long LPR;				// 0x20
	......
}UART_TypeDef;

#define Uart0	(( UART_TypeDef * ) 0x40003000)
#define Uart1	(( UART_TypeDef * ) 0x40004000)
#define Uart2  (( UART_TypeDef * ) 0x40005000)

void uart_init(UART_TypeDef * uart_ptr)
{
	uart_ptr ->DATA = 0x00;
	uart_ptr ->PSR  = 0x0A;
	uart_ptr ->LPR  = 0x00; 
}

大多数情况下,外设寄存器都被定义为32位宽度,这是因为连接外设的外部总线APB协议是按照32位处理数据传输的。

注意:外设访问定义指针时,需要使用 volatile 关键字。

6. Cortex 微控制器软件接口标准(CMSIS)

6.1 CMSIS介绍

在一个工程中应用了多个软件组件,对许多大型软件工程来说,兼容性变得极为重要。

为了使这些软件产品具有高度兼容性和可移植性,ARM同许多微控制供应商和软件方案供应商共同努力,开发了一个通用的软件框架SMSIS,该框架适用于大多数的Cortex-M处理器以及Cortex-M微控制器产品。

cmsis_框架cmsis_框架.png

CMSIS一般是作为微控制器厂商提供的设备驱动库的一部分来使用。为了使用诸如NVIC和系统控制功能等处理器特性,CMSIS提供了一种标准化的软件接口。

6.2 CMSIS组织结构

CMSIS可以分为以下几层:

  1. 核心外设访问层

    命令定义,地址定义,以及访问核心寄存器和NVIC、SCB以及SysTick等核心外设的辅助功能。

  2. 中间件访问层

    • 典型嵌入式系统访问外设通用方法

    • 面向通信接口,包括UART、Ethernet和SPI等;

    • 嵌入式软件能够在任何支持特定通信接口的Cortex微控制器上使用;

  3. 设备外设访问层

    寄存器名称定义,地址定义,以及访问外设的设备驱动代码。

  4. 外设的访问函数

    可选的外设辅助函数。

CMSIS结构图如下:

CMSIS结构图CMSIS结构图.png

6.3 使用CMSIS

CMSIS被集成在微控制器供应商提供的设备驱动包中,如果使用设备驱动库进行软件开发,那么就已经在使用CMSIS了。

工程中使用CMSIS:

工程中使用CMSIS工程中使用CMSIS.png

CMSIS工程.png

CMSIS工程

CMSIS示例:

CMSIS示例.pngCMSIS示例

CMSIS中文件描述:

CMSIS文件描述.png

注意:本站所有文章除特别说明外,均为原创,转载请务必以超链接方式并注明作者出处。 标签:ARM,Cortex-M0,Cortex-M0编程