《ARM Cortex-M0 权威指南》笔记(3)—编程入门
1. 输入输出
将printf打印的字符输出到串行口(或者其它接口)的技术一般称为“重定向”,重定向也能处理用户输入和系统函数。
2. 开发流程
程序生成流程:
根据所使用开发工具不同,链接器可能会使用命令行选项来指定内存分布。不过,使用GNU C编译器的工程在内存分布较为复杂时,可能需要一个链接文件。对于ARM开发工具,链接文件被称为分散加载文件。
生成可执行映像文件后,可以将其下载到微控制器的Flash存储器或内存中进行测试,整个过程相对简单。
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启动代码需要初始化堆存储及其控制变量。
4.2 带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一般是作为微控制器厂商提供的设备驱动库的一部分来使用。为了使用诸如NVIC和系统控制功能等处理器特性,CMSIS提供了一种标准化的软件接口。
6.2 CMSIS组织结构
CMSIS可以分为以下几层:
核心外设访问层
命令定义,地址定义,以及访问核心寄存器和NVIC、SCB以及SysTick等核心外设的辅助功能。
中间件访问层
典型嵌入式系统访问外设通用方法
面向通信接口,包括UART、Ethernet和SPI等;
嵌入式软件能够在任何支持特定通信接口的Cortex微控制器上使用;
设备外设访问层
寄存器名称定义,地址定义,以及访问外设的设备驱动代码。
外设的访问函数
可选的外设辅助函数。
CMSIS结构图如下:
6.3 使用CMSIS
CMSIS被集成在微控制器供应商提供的设备驱动包中,如果使用设备驱动库进行软件开发,那么就已经在使用CMSIS了。
工程中使用CMSIS:
CMSIS示例:
CMSIS中文件描述: