借由ARM CORTEX-M芯片分析C程序加载和存储模型

ARM 174浏览

https://zhuanlan.zhihu.com/p/22048373

借由ARM CORTEX-M芯片分析C程序加载和存储模型

借由ARM CORTEX-M芯片分析C程序加载和存储模型

王小军王小军

1 年前

阿军最近在忙着血氧手环嵌入式系统的技术预研,因为整个嵌入式的任务由阿军一个人负责,而阿军又对这个不太了解,只能从头研究,最近有点忙,过了八月份应该会更新比较频繁吧。

阿军把最近学习ARM芯片和做嵌入式编程的一点体会,如有错误欢迎指正。

---------------------------------------------------

本文着眼于以下内容:

  1. ARM处理器基本的存储模型;
  2. C语言示例说明程序在芯片中的存储模型;
  3. C程序如何在ARM裸板上运行
  4. C和嵌入式硬件开发

ARM处理器基本存储模型

nRF51822[ARM CORTEX-M0]寻址空间,如下图所示

arm 32位芯片地址宽度为32位,通过内部总线挂载设备:

  • 高速总线:挂载RAM和GPIO
  • 外设总线:挂载非易失性存储和外设

ARM CPU可以直接通过地址总线读写RAM(高速),读取非易失性存储(一般速度),擦除及写入非易失性存储(慢速),读写外设寄存器(一般速度)。

其中,非易失性存储用于存储代码、恒定数据,RAM用于存储指令执行过程中的临时数据,读写外设寄存器用于控制GPIO、UART等外设。

C语言示例说明程序在芯片中的存储模型

硬件层的设计决定了软件层的设计

存储器

  • 非易失性存储速度慢,便宜,代码断电保留;
  • 易失性存储器速度快,价格贵,数据和代码断电时丢失;

为了综合利用两种存储器,设计了这样的软件架构:

  • 数据分为固定不变的固定数据(代码也可以理解为一种数据),代码执行过程中一直存在的变量,代码执行过程中临时产生的变量
  • 代码和固定的数据保存在非易失性存储中,断电从头开始执行,数据和代码一直保留;
  • 全局变量和临时变量存储在易失性存储中,需要由非易失性存储器中的代码和数据初始化;
  • CPU从非易失性存储器中加载指令,系统开始时必须初始化全局变量(把全局变量的初始值由非易失性存储移动到易失性存储器),初始化临时变量区。
  • 易失性存储器中用静态数据区存储静态变量和全局变量,在堆(通过malloc和free管理)和栈中存储临时变量。
  • CPU通过绝对寻址、间接寻址、寄存器寻址等多种方式获取数据

这个原因导致C中指针的引入异常方便与强大,不论是数据、寄存器、函数等都能通过指针直观的操作

  • 芯片中数据以字(4字节)、半字(2字节)、字节、位的模式存储与操作

对应C中的基本数据类型

...除此之外还有很多硬件和软件的对应,了解系统底层实现原理与熟练掌握C语言相辅相成。

C代码与ARM 存储器结构的对应

  • const 修饰符与固定数据

const 修饰的数据存储在非易失性存储器,运行时不能修改,比如FLASH、磁盘中

  • 全局变量、静态变量与静态数据区

全局数据和static 修饰的变量存储在易失性存储的静态数据区,在程序的整个生命周期内,其一直存在

  • 系统临时变量 与 栈(stack)

系统临时变量(比如函数中的变量,函数调用上下文等)保存在易失性存储器的栈中

注意:以上三种数据类型的存储一般由编译器自动控制,我们无法干预

  • 动态内存管理 与 堆(heap)

通过malloc和free手动申请及释放,这个用起来比较危险,可能会出现内存溢出,野指针等各种复杂的问题,但是给程序设计人员很大的自由,很强大

C库中的动态内存分配策略采用线性方案,就是寻找合适的没有被使用的堆区域,然后把内存分配出去

随着malloc和free的进行,就会产生内存碎片

采用合适的策略分配和整理内存是操作系统很重要的任务,示例FreeRTOS中就实现了5种动态内存管理函数

C语言之所以是嵌入式系统和操作系统可选择唯一语言的原因就是其强大的指针和对内存的精确管理

示例:编写简单的C语言,从其在MDK上编译连接生成的map文件,了解C语言的存储。(待补充)

C语言如何在ARM裸板上运行

nRF51822(ARM CORTEX-M0)的启动代码

现场分析nRF51822的启动代码及汇编执行顺序,了解程序是如何在CPU上加载启动的

启动代码(汇编)

IF :DEF: __STACK_SIZE                   ;预编译指令 #ifdef _STACK_SIZE
Stack_Size      EQU     __STACK_SIZE    ;#define Stack_Size __STACK_SIZE
                      ELSE                    ;#else
Stack_Size      EQU     2048            ;#define Stack_Size 2048
                      ENDIF                   ;#endif

;AREA 命令指示汇编器汇编一个新的代码段或数据段。  
;段是独立的、指定的、不可见的代码或数据块,它们由链接器处理.  
;段是独立的、命名的、不可分割的代码或数据序列。一个代码段是生成一个应用程序的最低要求  

;默认情况下,ELF 段在四字节边界上对齐。expression 可以拥有 0 到 31 的任何整数。  
;段在 2expression 字节边界上对齐

AREA    STACK, NOINIT, READWRITE, ALIGN=3   ;代码段名称为STACK,未初始化,允许读写,8字节对齐
Stack_Mem       SPACE      Stack_Size                          ;分配Stack_Size的栈空间,首地址赋给Stack_Mem
__initial_sp                                                ; 栈顶指针,全局变量

;基本同栈,初始化分配堆
                       IF :DEF: __HEAP_SIZE
Heap_Size       EQU     __HEAP_SIZE
                       ELSE
Heap_Size       EQU     2048
                       ENDIF

 AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8                                   ;8字节对齐
                THUMB                                       ;THUMB命令模式

; Vector Table Mapped to Address 0 at Reset,重启时程序从这里运行

                AREA    RESET, DATA, READONLY               ;代码段名称为RESET,DATA类型,只读
                EXPORT  __Vectors                           ;中断向量表
                EXPORT  __Vectors_End                       ;中断向量表结束指针
                EXPORT  __Vectors_Size                      ;中断向量表大小

__Vectors       DCD     __initial_sp                        ; Top of Stack 中断向量表首位为栈指针
                DCD     Reset_Handler                       ;重启中断
                DCD     NMI_Handler                         ;不可屏蔽中断
                DCD     HardFault_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                         ;监控调用模式中断
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     PendSV_Handler
                DCD     SysTick_Handler

                ; External Interrupts
                DCD     POWER_CLOCK_IRQHandler
                DCD     RADIO_IRQHandler
                DCD     UART0_IRQHandler
                DCD     SPI0_TWI0_IRQHandler
                DCD     SPI1_TWI1_IRQHandler
                DCD     0                         ; Reserved
                DCD     GPIOTE_IRQHandler
                DCD     ADC_IRQHandler
                DCD     TIMER0_IRQHandler
                DCD     TIMER1_IRQHandler
                DCD     TIMER2_IRQHandler
                DCD     RTC0_IRQHandler
                DCD     TEMP_IRQHandler
                DCD     RNG_IRQHandler
                DCD     ECB_IRQHandler
                DCD     CCM_AAR_IRQHandler
                DCD     WDT_IRQHandler
                DCD     RTC1_IRQHandler
                DCD     QDEC_IRQHandler
                DCD     LPCOMP_IRQHandler
                DCD     SWI0_IRQHandler
                DCD     SWI1_IRQHandler
                DCD     SWI2_IRQHandler
                DCD     SWI3_IRQHandler
                DCD     SWI4_IRQHandler
                DCD     SWI5_IRQHandler
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved

__Vectors_End

__Vectors_Size  EQU     __Vectors_End - __Vectors   ;计算中断向量表的大小

                AREA    |.text|, CODE, READONLY     ;代码段,|.text|表示由C语言产生的代码段,CODE类型,只读

; Reset Handler     

NRF_POWER_RAMON_ADDRESS              EQU   0x40000524  ; NRF_POWER->RAMON address
NRF_POWER_RAMONB_ADDRESS             EQU   0x40000554  ; NRF_POWER->RAMONB address
NRF_POWER_RAMONx_RAMxON_ONMODE_Msk   EQU   0x3         ; All RAM blocks on in onmode bit mask

Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]      ;[WEAK]修饰代表其他文件有函数定义优先调用
                IMPORT  SystemInit  ;从外部调用SystemInit
                IMPORT  __main      ;从外部调用__main

;以下代码不同芯片不一样,这个是设定RAM块开启关闭的配置,这里配置为全部开启                
                MOVS    R1, #NRF_POWER_RAMONx_RAMxON_ONMODE_Msk

                LDR     R0, =NRF_POWER_RAMON_ADDRESS
                LDR     R2, [R0]
                ORRS    R2, R2, R1
                STR     R2, [R0]

                LDR     R0, =NRF_POWER_RAMONB_ADDRESS
                LDR     R2, [R0]
                ORRS    R2, R2, R1
                STR     R2, [R0]

                LDR     R0, =SystemInit
                BLX     R0                                  ;无返回调用SystemInit
                LDR     R0, =__main
                BX      R0                                  ;有返回调用__main
                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   POWER_CLOCK_IRQHandler [WEAK]
                EXPORT   RADIO_IRQHandler [WEAK]
                EXPORT   UART0_IRQHandler [WEAK]
                EXPORT   SPI0_TWI0_IRQHandler [WEAK]
                EXPORT   SPI1_TWI1_IRQHandler [WEAK]
                EXPORT   GPIOTE_IRQHandler [WEAK]
                EXPORT   ADC_IRQHandler [WEAK]
                EXPORT   TIMER0_IRQHandler [WEAK]
                EXPORT   TIMER1_IRQHandler [WEAK]
                EXPORT   TIMER2_IRQHandler [WEAK]
                EXPORT   RTC0_IRQHandler [WEAK]
                EXPORT   TEMP_IRQHandler [WEAK]
                EXPORT   RNG_IRQHandler [WEAK]
                EXPORT   ECB_IRQHandler [WEAK]
                EXPORT   CCM_AAR_IRQHandler [WEAK]
                EXPORT   WDT_IRQHandler [WEAK]
                EXPORT   RTC1_IRQHandler [WEAK]
                EXPORT   QDEC_IRQHandler [WEAK]
                EXPORT   LPCOMP_IRQHandler [WEAK]
                EXPORT   SWI0_IRQHandler [WEAK]
                EXPORT   SWI1_IRQHandler [WEAK]
                EXPORT   SWI2_IRQHandler [WEAK]
                EXPORT   SWI3_IRQHandler [WEAK]
                EXPORT   SWI4_IRQHandler [WEAK]
                EXPORT   SWI5_IRQHandler [WEAK]
POWER_CLOCK_IRQHandler
RADIO_IRQHandler
UART0_IRQHandler
SPI0_TWI0_IRQHandler
SPI1_TWI1_IRQHandler
GPIOTE_IRQHandler
ADC_IRQHandler
TIMER0_IRQHandler
TIMER1_IRQHandler
TIMER2_IRQHandler
RTC0_IRQHandler
TEMP_IRQHandler
RNG_IRQHandler
ECB_IRQHandler
CCM_AAR_IRQHandler
WDT_IRQHandler
RTC1_IRQHandler
QDEC_IRQHandler
LPCOMP_IRQHandler
SWI0_IRQHandler
SWI1_IRQHandler
SWI2_IRQHandler
SWI3_IRQHandler
SWI4_IRQHandler
SWI5_IRQHandler
                B .
                ENDP
                ALIGN

; User Initial Stack & Heap,编译器预处理命令

                IF      :DEF:__MICROLIB                         ;#ifdef __MICROLIB

                EXPORT  __initial_sp                            ;堆栈的设置采用__MICROLIB库中的策略
                EXPORT  __heap_base
                EXPORT  __heap_limit

                ELSE                        ;#else

                IMPORT  __use_two_region_memory                 ;外部定义的两段存储模式函数
                EXPORT  __user_initial_stackheap                ;用户分配堆栈的地址

;寄存器R0,R2存储管理heap
;寄存器R1,R3管理statck

__user_initial_stackheap PROC

                LDR     R0, = Heap_Mem
                LDR     R1, = (Stack_Mem + Stack_Size)
                LDR     R2, = (Heap_Mem + Heap_Size)
                LDR     R3, = Stack_Mem
                BX      LR
                ENDP

                ALIGN

                ENDIF

                END

其中调用的是__main()函数,而非main()函数,因为C语言在__main()函数里封装了两个函数:

  • __scatterload():完成全局变量从非易失性存储器到易失性存储器的复制与初始化;完成了堆栈的初始化
  • __rt_entry():完成堆栈管理寄存器的配置

这两个函数完成C语言运行时的构建,在构建好之后进入main函数执行程序

C语言与嵌入式开发

ARM SOC的控制

  • 内存管理:全局与静态变量,堆栈初始大小的分配,malloc与free动态内存管理等
  • 存储器操作:数据的获取与存储,如果同一片芯片FLASH或者操作其他存储器
  • 外设管理:外设都是通过总线可以访问的寄存器,对寄存器的控制可以控制外设
  • 芯片固件升级:以nRF51822为例,详细见下图:

这里涉及到的技术点就是中断转发了,每一部分都可以按照正常的程序进行编写,MBR负责所有中断的转发,要注意这样转发会有us级的延迟。