ARM高手的历程(附N多资料)学会ARM足够

ARM高手的历程(附N多资料)学会ARM足够
ARM高手的历程(附N多资料)学会ARM足够

ARM的历史以及有哪些种类的内核就不必再赘述,我们就直接从ARM7,指令集V4讲起。

关于ARM到底属于什么类型的CPU,一直以来说法不一,有说他们是RISC (至少他们自己这么认为,Advanced Risc Machines),也有说他们是类RISC 的。

说类RISC的理由就是ARM的指令并不是定长的,而且并没有实现真正的流水线。当然这些区别现在已经变得越来越不重要,甚至两个领域都在互相渗透,比如MIPS的14K和24K系列已经引入了16位指令,而较新的ARM架构也已经开始支持乱序执行。

可能有人会反驳,怎么可以这么说呢?ARM7中实现了三级流水线!请注意,这里所说的是真正的流水线,如果能对MIPS有所了解或者看过MIPS阵营的经典著作《see mips run》,就能明白真正意义的流水线的含义。在RISC领域有一个概念叫做delay slot,也就是我们所说的延迟槽,出现这个概念的原因是因为在RISC中,绝大多数指令的执行时间是可预测的,就是一个周期,而有些指令譬如load,jmp之类的无法在一个周期内完成,这样就造成一个问题,在执行jmp,load时,处于译码阶段的指令就要多等一个周期,等前面的指令执行结束它才能进入执行阶段(早期的ARM就是采用这种方式)。而在真正的流水线架构中,遇到这种情况,可以把jmp,load之后的指令在jmp,load进入执行阶段准备但是没有真正执行的时候提前执行,这样就节省了之前被浪费的那个周期。处于延迟槽中被提前执行的指令必须是对后续指令无害的,这个由编译器来决定,当无法插入无害指令时,编译器会向其中插入NOP。

千万不要小看这个周期,因为在真正的执行中,内存读写以及跳转操作随处可见,这点效率的提升导致了MIPS在整数运算性能上的优势。

我的经历简单得很,上学时老师的项目和师弟帮忙找的私活,51,96,pic的简单控制电路和汇编程序,毕业后进入华为,开始学习的c和mips,当时负责基于MIPS/VxWorks的bsp和驱动,得感谢Broadcom的FAE送给我们的一本《See MIPS Run》,从这本书开始才真正把学过的计算机体系结构和软硬件结合到一起。再后来进入一家IC外企,做基于MIPS/Nucleus的bootloader,BSP和驱动,从这开始对操作系统有点接触,在华为时由于分得太细,没能够对VxWorks更多了解。再后来转战ARM,ucos,linux,固件,芯片验证等等,这才开始每天跟cpu核心打交道,也对操作系统更加深入,目前是要回到一个老东家那里继续做mips。

我运气好的地方是正好工作内容基本上都是我每个阶段想接触的东西,没有参加过任何任何培训,几本好书加上官方文档陪伴着走过这段职业生涯。所以很多时候别人问我怎么学我都不知道怎么回答,因为没有为了学而学过,都是工作需要。实际回过头来看看,两大块知识对于嵌入式至关重要,一个是计算机体系结构,一个是操作系统原理,囊括了我个人目前见到的所有东西,都脱不出这个范畴。至于各种接口,都是比较分散的知识点,需要花时间去学习掌握。

而支持ARM属于RISC的理由在于四点:

1. 使用了精简指令集,Reduced Instruction Set Computer,虽然ARM的指令相对其他RISC比还是算多的,但毕竟出于可接受范围,不像x86那样浩瀚无穷。RISC的目的就是让硬件尽量简单,更多的处理交给编译器和软件,这样就可以让kernel结构简单,比较容易实现低功耗高频率。

2. 实现了一定程度的流水线,学过51的人应该还记得51的工作方式,指令都是一条一条地取指译码执行,只有上一条指令执行结束,下一条指令才能进入取指阶段。

3.ARM核心中有较多的通用寄存器,总共有31个(不算状态寄存器),但是它们并不能同时存在,有些寄存器是模式专有的,只有处理器处于那种模式才能够对其进行操作,术语称为Banked,在固定模式下可见的通用寄存器的个数是16个。

4.采用了Load-Store的工作方式,所谓的Load-Store方式,也就是说一切的计算操作都只针对寄存器,内存中的数据需要先load到寄存器,进行计算之后再store回内存。而在cisc中,内存数据是可以直接参与计算的,这是双方一个巨大的差别。

个人所看过的好书,最推崇的是《See Mips Run》第二版,结合linux对mips 做了非常全面而且深刻的讲解,而你从这本书中得到的将不仅仅是linux和mips,涉及到了很多操作系统以及计算机体系结构的共通问题。推荐看英文版。

做软件开发到一定程度,不管你是做了项目管理还是研发,都会不可避免地用到脚本语言去写一些自动化的工具,因为身处芯片行业,所以用的就是perl,推荐读《perl语言入门》和《intermediate perl》就可以处理大多数问题,这一系列中的《精通perl》没有读,因为没到那个层次。

如果有兴趣有精力强烈推荐《精通正则表达式》这本书,尽管属于杂项,但是对于软件人员来说,读过这本书绝对令人心旷神怡,你在vi,ue,makefile甚至arm的scatter和gcc的ld中到处可以见到它的身影,读懂了正则表达式,就会明白这些工具中生涩难记的命令原来也是有规则可循,他们都在遵循着正则表达式。

那本《操作系统原理与设计》(好像是这么叫的)如果只是翻翻会很无聊,甚至你会觉得都不如《深入linux内核》好,而你要是能够一字一句的看进去,就会发现前者是理论的高度,后者只是在实现层面略作讲解。

至于软件工程方面的书,《人月神话》和《解析极限编程》对我启发更大,《代码大全》和《人件》就没有太多感觉了。

了解流水线对于软件人员来说,一个比较重要的意义就是当出现问题时,能够精确定位到产生问题的那句话。但是有些情况是例外的,体制上就不能保证精确定位,比如说有write buffer的时候,你的写指令即使是发生了错误,也是在数个周期之后才会激起异常,为了速度匹配问题,写数据会通过write buffer操作,发生异常的时刻已经离发出写指令的时刻不匹配了。

当发生异常的时候,犯罪分子和发生异常时所在模式的pc的匹配关系如下:

Data abort pc - 8,因为data abort只有进入执行阶段才能被发现,这时第三条指令已经被取指。

IRQ pc - 8,IRQ发生时刻不可预知,在当前指令执行完之后响应,所以pc也是第三条的地址。

FIQ pc - 8,原理同上。

Prefetch Abort pc - 4,这个最绕,是预取指时发生错误造成的,但是如果这条指令不进入执行阶段就不会造成异常,比如说前面一条是jmp。所以我猜测在它进入执行之前,kernel已经知道出错,pc不在更新,保持为它下一条指令的位置。

SWI和UNDEF和上一条一样,都是在译码阶段kernel已经知道会发生异常,不再更新pc,但是进入执行阶段才会激起异常,所以异常地址也都是pc-4.

今天来讨论一下对齐问题,在ARM7,即arm v4中,规定如果访问int类型数据时给出的指针地址的低2位不为0的话,系统会自动将低两位的1抹平,即强制四字节对其,这样的问题就是如果你的数据偏偏就是不对齐的,cpu拿到的数据就是不正确的,bug由此产生。

在ARM9(我接触的是arm926ejs)中,如果访问int型数据,低2位不为0,那么cpu直接挂住,一个data abort。

在ARM11之后的版本中支持了非对其访问,是在总线上拆分然后拼接来实现的,也就是说如果你访问int时给出的地址不是4字节对齐,那么总线上会出现两个int访问,然后把数据拼接起来送给cpu,这些对于cpu是不可见的,但是会导致速度下降,总线占用率上升。此功能可以通过修改cp15中来关闭,实现跟以前版本一样的对齐方式。

需要说明的是这里的int是4字节而不是2字节。另外一点是对齐问题并不是专指四字节对齐,很多初学者或者有一定经验的人都会犯这个错误,认为只有四字节对齐才会出问题。实际上对齐指的是数据边界对齐,也就是说long long数据要8字节对齐,int要4字节对齐,short要2字节对齐,byte自然是怎么对都齐了。

主流的ARM核心会包含很多种模式,除了定位为MCU的Cortex-M系列。这些模式包括如下几种:

1、中断模式(IRQ)

2、快速中断模式(FIQ),此模式在ARM7中并未实现

3、用户模式(USR)

4、系统模式(SYS)

5、软中端模式(SVC)

6、数据终止模式(ABT),包含了数据终止和预取中止

7、未定义指令模式(UNDEF)

8、监视模式(Monitor),这个模式是比较新的架构中才添加的,应用场景比较特殊,我还没有完全搞懂。

系统复位后CPU的模式是SVC模式,一般情况下会在切到用户程序之后切换到USR模式,但是最近有个同事跟我讲三星2440的例程中CPU是始终工作在

SVC模式下的,这种方式非常古怪。

保护模式的操作系统下,一般的原理是用户程序工作在USR模式下,当你要做系统调用的时候就激起软中断,软中断服务在判断你的请求合法之后调用内核函数,如果不合法就置上错误号,返回给用户程序。内核中的大部分函数工作在SYS模式下,USR模式和SYS模式公用相同的寄存器,只是特权等级不一样,所谓的不一样也主要是SYS模式下可以操作CPSR,而usr模式不允许。另外USR和SYS模式下是没有SPSR的,因为这两种模式不是异常模式,是常规的操作模式。

IRQ和FIQ模式就不用再多说了,做过单片机的人会觉得很熟悉。

ABT模式就是当发生了地址或者数据异常时进入的模式,这种模式既可以做为数据的保护方式(当你有MMU,MPU时),也可以实现虚拟地址的扩展,还有可以帮助我们保存bug现场,方便调试。一般裸奔或者非保护式系统中,ABT 模式的处理就是死循环,因为在这类系统中如果发生了ABT就是不可恢复的错误。

UNDEF是指CPU遇到了无法解析的指令,发生这种状况一般来说有以下几种场景:

1、在ARM/THUMB混编情况下,ARM状态下执行了THUMB指令,或者THUMB 状态下执行了ARM指令。

2、堆栈异常导致PC返回值异常,程序跑飞,执行了不存在的指令。

3、函数指针错误

4、有意的指令扩展,这种就需要在UNDEF异常处理中添加自己的指令扩展处理程序。

5、代码区被异常改写。

这里要补充一点,各种异常模式下的spsr是用来存储进入异常时前一种模式的cpsr,而且只有真正发生异常时才会把前一种模式的cpsr写入spsr,通过修改cpsr来切换模式并不会导致spsr的改写。

Cache逐渐成为现代CPU中越来越重要的角色,但到目前为止,依然有很多生产出来的ARM芯片是不带cache的,所以我们在这里只对cache做个简要的介绍。

Cache从其本质来说也是SRAM,只不过是离cpu最近的SRAM。Cache与外部(这里的内外是针对CPU的kernel而言,而不是针对SoC)SRAM最大的不同不在于速度,而在于它与cpu的交互方式,cpu访问cache的时候是直接通过其内部的cache控制器,而访问外部SRAM时首先要向总线发出请求,当总线仲裁器允许CPU访问总线后,CPU发出地址信息并等待SRAM返回数据。关于总线的详细信息可以到ARM官网上去下载AMBA的文档来学习。

TCM和cache并列为ARM核心的一级memory,TCM的设计为ARM提供了另外一种快速访问的方式,并且TCM的速度是恒定的,不像cache会有invalidate和flush等操作。在访问cache前你并不知道你所要访问的数据是否在cache中,而在访问TCM的时候,你所需要的东西一定在TCM内,这是因为你在配置TCM的时候已经设置好了它的地址范围,只有在访问这一段地址的时候才会访问TCM。

Cache和TCM的操作都是通过cp15寄存器指令来实现的,指令格式略显繁琐,而且无法用c语言来实现,不过还好,照着arm手册依样画葫芦就行了。

基础知识的最后我们需要讲一下向量表,我相信做过单片机的朋友对向量这个词应该不陌生,也就是发生了这个中断或者异常的情况下,cpu(我认为MCU也是cpu)会把pc直接指向某个固定的地址,在那个地址有个跳转语句或者一小段处理(比如说mips为每种异常提供了128字节的处理空间)。而你的系统所有的异常处理的那个地址表也就是我们所说的向量表,ARM的向量表有两种选择,0x0起始或者0xffff0000起始,选择哪个地址作为可以通过修改CP15中的相应控制位来实现。一个例外就是Cortex-M3(M0和M4没接触过),它把很多传统ARM中CP15的system control寄存器拿到了直接可寻址的地址空间,可以像操作普通寄存器那样修改向量表起始地址。

至于为什么允许修改向量表地址,这个问题就跟操作系统的实现相关了,在linux 和windows中,0地址位于用户空间,意味着整个向量表都在用户空间的起始地址,当有异常发生时,cpu自动就去用户空间拿指令,这样就会导致用户空间可以轻易地获得cpu的特权(所以异常模式都是特权模式),而0xffff0000处于内核空间就不会导致这样的问题。所以在有保护式操作系统的应用环境中,cpu 启动时首先会默认0地址是向量表起始,而在系统启动前,启动代码会把向量表切换到高位,以保证操作系统的实现。

ARM的基础知识我们先讲这些,其他需要扩展的内容或者回头想到落下的东西再补充。

我们从复位开始讲起,复位又被分为很多种,什么上电复位,重启复位,冷复位热复位的,我们不考虑那么多,只从最根本的上电复位说,其他类型的当我们对cpu熟悉之后都可以自然地延伸。

复位之后pc指向一个固定的地址,也就是向量表的起始地址0x0,这个地址必须是有确定的内容-启动代码,为什么不用另外一种说法-这里必须是NV memory 呢,因为有些公司确实实现了0地址是SRAM的做法,他们为了允许客户不使用昂贵的ROM或者NOR flash,SOC中用逻辑实现了NAND flash的驱动,上电之后立即从NAND中读出前面几K的内容拷贝到SRAM的0地址,这样cpu去0地址取到的实际就是预先写到NAND中的内容而不是随机的SRAM复位内容。个人猜测他们的实现机制是这样,上电之后,SOC的逻辑将cpu的复位拉住,使cpu始终处于复位状态,同时去拷贝代码,当代码拷贝完成后松开cpu复位线,这时就保证了cpu能够读到确定内容。

下面是一段很典型的简单嵌入式系统的启动代码,没有MMU,没有cache,也没有外接RAM,所有代码数据都在SoC自带的SRAM中。我会将其中的重点一一讲解,而且启动的代码的讲解要配合上分散加载文件,才能显得更清晰。

这是scatter文件的内容,分散加载的具体语法和用法请参阅ARM链接器的官方文档:

ROM_LOAD 0x20000000 #说明加载域位于0x2000 0000

{

ROM_EXEC 0x20000000 #说明根执行域也是从0x2000 0000开始

{

vectors.o (Vect, +First) #vectors.o是生成文件的起始部分,保证向量表在0x0地址

* (+RO) #剩下的RO段放在vector的后面,包括代码和只读数据

}

RAM_1 +0

{

* (+RW,+ZI) #非零全局数据和bss段放在RO段之后

}

HEAP +16

{

init.o(Heap) #这段是HEAP段

}

STACK 0x2002FFFC

{

init.o(MyStacks) #系统RAM的最高地址是0x2002FFFF,所以stack 从此向下

}

}

根执行域必须同加载域是重合的,并且是处于开始位置的可执行代码,因为要用根执行域的代码来完成分散加载部分的其他工作。

这是Init.s的内容:

EXPORT Reset_Handler ;这里要让vectors.s看到,所以要export Reset_Handler

; 如果需要做地址空间转换的话,就会执行这段代码。为什么做地址空间转换呢,至少有两个理由的,首先0x0地址一般是bootloader的地址,bootloader 会用某种方式将runtime image加载进内存,一切处理好之后,bootloader会把控制权交给runtime image进行二次复位,两次复位的目的和执行的代码不一样,所以需要做一次地址空间转换。有的代码会做一些通用性处理让两次复位执行同样的函数,但是同样会有灵活性以及执行速度的需求要求向量表在SRAM中,所以在设计CPU时对于这里要仔细考虑好,加了没有任何害处,不加就可能日

后造成困扰。

IF EF: ROM_RAM_REMAP

LDR pc, =Instruct_2

Instruct_2

; Remap by setting Remap bit of the CM_ctl register

LDR r1, =CM_ctl_reg

LDR r0, [r1]

ORR r0, r0, #Remap_bit

STR r0, [r1]

ENDIF

这里是个比较绕的过程,我尽量解释,如果还是有不懂的地方,可以继续提出来讨论。

比如说这段启动代码在一块ROM中,如果需要做地址空间转换,IC design的人会在系统复位后赋予这个ROM两个地址,一个0x0,另外一个是类似于0x4000 0000这样的其他非零地址,第二个地址在做地址空间转换前后都可以访问,0x0只能在地址空间转换之前访问。当链接的时候,我们告诉链接器,这段代码在0x4000 0000,然后我们把这段代码固化到ROM中,这样在复位时,pc 指向0取到的是这段代码,而当执行完LDR pc,=Instruct_2之后,pc已经变成了0x4000 0004,而这个地址也是可以正确访问ROM的,这之后再去写地址转换寄存器也不会导致读不到正确的ROM内容了。

可以想象一下,如果不做前面所说的这个似乎多余的步骤,PC始终是按照0,4,8,c这个顺序执行下去,做完地址空间转换,拿的就是RAM的内容,必死无疑

; --- Initialize stack pointer registers

BL InitStack

IMPORT __main

; --- Now enter the C code

B __main ;

InitStack就不用废话了,操作cpsr切换到各种模式,然后设置各种模式下的堆栈,就可以允许跑c代码了。设置完之后调用ARM提供的__main库函数,如果你使用了这个库函数,那么你必须保证你的c代码中有main函数,__main 在做完分散加载,内存初始化,代码拷贝等工作之后默认跳到main函数。因为我只用过ARM官方的编译链接器,所以不知道其他人是怎么做的,不过我估计大概意思都差不多,以前用MIPS的时候用的GCC,也一样要自己写好LD(gcc 的分散加载描述文件),不过当时分散加载,代码拷贝和内存初始化都是自己用汇编完成的,不清楚gcc是否也提供了类似于arm这样方便的工具。为什么用B __main呢,因为这个函数是不会返回的,直接把控制权交给main(),而不是

交还给调用它的初始化代码。

InitStack

MOV R0, LR

;Build the FIQ stack

MSR CPSR_c, #0xd1

LDR SP, StackFiq

;Build the IRQ stack

MSR CPSR_c, #0xd2

LDR SP, StackIrq

;Build the DATAABORT stack

MSR CPSR_c, #0xd7

LDR SP, StackAbt

;Build the UDF stack

MSR CPSR_c, #0xdb

LDR SP, StackUnd

;Build the SVC stack

MSR CPSR_c, #0xd3 ;/*uCOS starts with SVC mode */

LDR SP, StackSvc

;Build the SYS stack

; MSR CPSR_c, #0xd3 ;

; LDR SP, =StackUsr

;Return

MOV PC, R0

;must be 8 byte aligned

FIQ_STACK_LEGTH EQU 128

IRQ_STACK_LEGTH EQU 2048

ABT_STACK_LEGTH EQU 128

UND_STACK_LEGTH EQU 128

SVC_STACK_LEGTH EQU 2048

StackAbt DCD top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH-FIQ_STACK_LEGTH-SVC_STACK_LEGTH

StackSvc DCD top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH-FIQ_STACK_LEGTH

StackFiq DCD top_of_stack -UND_STACK_LEGTH - IRQ_STACK_LEGTH

StackIrq DCD top_of_stack-UND_STACK_LEGTH

StackUnd DCD top_of_stack

AREA MyStacks, DATA, NOINIT

top_of_stack SPACE 4 ;此处的space定义可以不用管,只是为了确定top_of_stack位置

EXPORT bottom_of_heap

AREA Heap, DATA, NOINIT

bottom_of_heap SPACE 1 ;道理同上

END

以上部分我给大家看的是runtime image部分的启动代码,下面再看一下bootloader部分的启动代码,略微有些差别。

因为绝大多数情况下,bootloader中我们不想启用中断,也绝不会去处理异常,所以在bootloader中就没有去写中断向量表。但是,没有中断向量表并不意味着cpu就不去响应异常,如果你的程序中有bug,比如说读写了不存在的地址或者函数指针错误,这样一定还是会激起异常,cpu照常去0xc(prefetch abort)或者0x10去拿指令执行,即使这里已经不是中断向量表了,但是cpu依然按照内置的逻辑去执行,所以bootloader要小心再小心,那种固化在SoC内部的bootloader,一旦出问题,你生产出来的芯片就是块石头。

下面是一款bootloader的scatter文件,scatter的解析可以参照上一个例子。LOAD_ROM 0x10000000 0x002000

{

EXEC_ROM 0x10000000 0x002000

{

init.o (init, +First) #可以看到,我们在这里没有vectors.o,因为不需要

* (+RO)

}

RAM 0x30080000 0x7000

{

* (+RW,+ZI)

}

STACKS 0x30088000 0x1000

{

stack.o (+ZI)

}

}

这是相应的init.s的内容:

ENTRY

; --- Perform ROM/RAM remapping, if required

IF EF: ROM_RAM_REMAP

; On reset, an aliased copy of ROM is at 0x0.

; Continue execution from 'real' ROM rather than aliased copy

LDR pc, =Instruct_2

Instruct_2

LDR r1, =Remap_ctl_reg

LDR r0, =Remap_value

STR r0, [r1]

ENDIF

Reset_Handler

IMPORT top_of_stacks ; defined in stack.s and located by scatter file

LDR r0, =top_of_stacks

MSR CPSR_c, #Mode_SVC:OR:I_Bit:OR:F_Bit ; No interrupts

SUB sp, r0, #Offset_SVC_Stack

IMPORT load_firmware

B load_firmware ;

END

我们可以看出来,基本上runtime的启动文件基本相同,只是最后跳转的地方不一样。但是,这只是假象,由于scatter文件的不同,两个工程生成的可执行文件有着根本的不同,runtime那个在0地址储存的是向量表,供cpu在发生异常时取用,而bootloader的在0地址直接就是顺序执行的初始化语句,因为bootloader只关心一件事,把runtime image以某种方式加载进来,如果有需要的话还要解压,当这些做完之后再把控制权交给runtime image中的可执行域就行了,至于以后的异常处理等等问题不在它考虑范围内。

还有一个不同是我们用了B load_firmware,而不是B __main,这样做的一个好处是链接时就不会把ARM的库函数包进来,能节省1K左右的ROM空间,而坏处就是你要仔细对待你的代码,因为缺少了一个初始化函数,所以内存中的值就没有人给你赋值,也就是说你的代码中不允许有带初值的全局变量,这一点切记,很多人在这上面吃亏。

从bootloader到main函数之间一般要经历如下的步骤:

1、地址空间转换(可选)

2、初始化供bootloader的c代码使用的栈空间

3、等待从主机侧下载镜像文件或者直接从自己挂载的存储器拿。

4、解析校验镜像,没问题的话将它放到预定的位置,有需要的话还得解压。

5、将pc指向镜像文件的入口,即镜像文件的reset handler,二次复位。

6、进行板级初始化的第一步骤,包括硬件,堆栈等等。

7、控制权交给分散加载解释函数,完成软件系统的初始化。

8、模式切换(从svc到sys,最好有这一步)

9、控制权交给main函数。

需要注意的是,在一般情况下,整个这个过程中断是关闭的,除非你有特殊需要。记住,这个部分越简单越直观就越好。

现在开始进入大家都熟悉的部分,大多数人工程的开始点,main函数,在main 函数中主要进行以下工作:

1、完成硬件初始化

2、软件数据结构的初始化

3、任务初始化

4、打开中断。

关于打开中断要多说两句,ARM中断的使能必须至少有两级(Cortex-M除外,不属于传统ARM),中断控制器的使能和CPSR中I位的使能,而CPSR的使能才是最根本的使能,很多新手会卡在这儿,不停地问为啥我初始化了中断控制器却没有中断发生。在ARM架构中,中断控制器是cpu之外的东西,它只是总线上一个可读写的模块,中断状态寄存器一般情况下会用32个状态位表示最多32个中断源,不过这不是限制,可以做几级中断控制,这样就可以表达更多的信息。所有已经使能的中断源的状态位或之后,连接到CPU最根本的中断异常使能,如果此时cpu的中断异常也是使能的,就会激起cpu的异常响应,这段描述听着有点别扭,学过数电的人应该还可以理解。

当把任务,中断,优先级,以及消息队列,任务堆栈等都配置好以后,就可以跑一些简单的操作系统了,类似于ucos这类普通操作系统的任务切换只要依靠二个途径,

1)任务主动让出cpu而导致低优先级任务得到执行

2)中断导致高优先级任务就绪,退出中断时高优先级任务获得执行权

因为ucos不存在同级优先级的任务,所以也就不存在时间片到时切换这种模式(术语称为Round-Robin,或者简写为RR),这样做是为了使操作系统的实现简单化,但是在某些特定的场合确实让人很别扭,需要仔细设计以规避带来的问题。

Main函数在设置完任务并调用了os_start()之类的函数后就退出了舞台,正常情况下不会再返回到main函数,main函数不属于操作系统,操作系统运转起来之后就不需要它了,过河拆桥。

而完整的保护式操作系统则是一个完全不一样的概念,为什么叫保护式操作系统呢,个人理解至少有两个原因:

1)每个进程只能看到自己的地址空间,其他进程的数据和代码都是不可见,每个进程都拥有自己的世界。

2)用户进程不能直接调用内核函数或者读写内核数据,必须通过API调用这种间接方式向内核请求,在获得允许的情况下才能执行。

可能有朋友会问,既然用户空间不能执行内核函数,那如何进行API调用呢?这种情况我们就需要SWI即软中断的帮助,从cpu的角度来看,用户空间调用系统API的流程如下:

1)按照寄存器参数传递规则,用户空间首先将需要的服务号,参数等填入各个寄存器或者数据结构,此时工作于usr模式。

2)激起SWI异常

3)CPU进入SVC模式,检查服务号,参数等是否合法,若不合法则置失败标识原因等,退出到用户模式,如若合法则会调用相应的系统服务函数。4)执行结束之后从SVC模式填好返回值,返回到用户模式。

这种执行过程,保证了内核在没有bug的情况下,不会被恶意用户程序轻易获得cpu特权从而为所欲为。当然没有bug的软件是不存在的。

ARM的状态切换是一个比较容易出问题的地方,ARM为了实现效率和成本的平衡,允许cpu在32位指令编码(ARM)和16位指令编码(THUMB)之间切换,因为确实有些简单指令完全没必要用32位来实现,而在某些对性能要求比较严格的场合,由于16位编码的限制,又导致了性能的下降,此时cpu就可以切换到32位去执行。公平地说ARM的这个设计是相当成功的,虽然我更喜欢MIPS。在以控制为主的系统中可以大量使用THUMB编码,只有在大量计算的模块使用ARM编码,实现起来非常方便,在ARMCC编译器下,只需要打开interworking选项,在需要ARM编码的模块标注#pragma ARM,在需要THUMB 编码的模块标注#pragma THUMB就搞定了。C语言编码级别的ARM/THUMB 态转换由编译器链接器插入一些称为veneer的小模块来解决,这些小模块在c 层面上不可见,是armcc为了方便实现长跳转以及状态切换加入的。状态切换从cpu指令级别来看,常用的有两种方式:

1. 通过某些特殊指令将spsr恢复到cpsr来实现状态切换

2. 通过类似于blx或者bx这样的跳转指令,当目的地址的最低位为1时,切换到THUMB态,若其为0则应该切换到ARM态,这时你可能会问即使是THUMB态也是16位对齐,怎么会最低位为0?不用担心,这个由链接器来处理,它会把所有标记为THUMB态的函数地址的最低位置1,这样就不用担心了。当你编写自己的汇编语言代码,并且系统中存在ARM/THUMB切换时,你就用非常小心,这个时候全靠你自己了,因为不论是在ARM态下执行THUMB代码,还是在THUMB态下执行ARM代码,都会遭遇一个问题,undefined instruction,

也就是说,cpu不认识那些指令了。某著名的嵌入式os的2.89版就有这样的问题,在做中断处理的时候没有考虑到ARM/THUMB混编的情况,导致某些情况下,cpu在ARM态执行THUMB代码。等有空把他们本来的代码和我修改的部分贴出来比较下大家就能看明白了。当时联系了他们的FAE,也发给了他们的总部,不过一直没见修改,不知道现在怎么样了。

昨天调了一整天的bootloader,脑袋都快木了,跳出了一个又一个的坑,并且目标板上没有提供Jtag接口,大大增加了调试难度,不过也由此想到了一些话题。首先的一个话题是动态加载,以前的根bootloader和用于升级的bootloader使用两套代码,自然Makefile也是两套,可是既然都是bootloader,那么实际上大部分代码都是相同的,使用两套代码以及Makefile就等于是两个工程,这样会造成日后维护量的上升以及发生错误的可能性。按照多位软件大师的对于软件质量的看法,在确定的软件需求之下,当你尽可能地让自己的代码简单,你也就极大可能地减少了bug的数量。而且我也坚信一点,整个过程中如果需要人为参与的工作量更少,日后出现弱智性错误的几率就越小。

出于这些考虑,我接到这个需求之后就考虑把原有工程合并,但是首先需要解决的一个问题就是要解决动态加载的问题,因为根bootloader是直接可读写的,所以升级用的bootloader不能使用与它重叠的地址空间,而如果使用同样的Makefile和分散加载(ARM链接器里面是scatter,GNU的链接器里面叫做ld 或者lds文件),这样的话就出现了一个问题,升级用的bootloader和根bootloader编译时是一样的,执行时却在不一样的地址。这就要求升级用bootloader必须有一种灵活的机制,使自己能够在不同的地址上都能执行,直观点说就是当pc从入口跳入执行之后,其内部的跳转都是相对跳转,并且不使用固定地址的全局变量等。实际上c语言级别的实现很简单,只需要在编译选项中加入一个开关,-fpic就行,开始时我想到了这点但是还是执行有问题,后来又查汇编部分,发现在跳转时使用了jal指令,由于这几年做arm做多了,已经遗忘了MIPS中实现了单指令的绝对跳转,多次放过这条指令,后来去查手册才知道应该改成b或者bal,于是第一个坑算是解决了。不懂汇编急死人啊。

之后几天一直在于GCC做斗争,以后可能要长期使用这种编译器了,所以即使我再不喜欢它,也要开始了解了。

GCC对MIPS的编译做了一些特殊的处理,有一些专门为MIPS的编译选项,这在GNU的官方网站上都可以查得到,我在这里简单说明一下,虽然不是ARM,但是日后会和ARM做比较。

在对MIPS的编译中,不能简单地使用-fpic来达到位置无关的目的,最重要的一个开关是-mabicalls(使能System V r4调用格式),使用了这个开关,并且再搭配上-mshare或者-fpic才能达到位置无关的目的,而在链接时是否使用位置无关选项并不重要,即使选择了静态链接,依然能够给出位置无关代码。

还有一点需要注意的是如果你的工程是类似于bootloader这样的汇编代码和c 代码混编的工程,就一定要注意处理好汇编与c代码间调用的接口,在SVr4针对MIPS的寄存器调用规则中,t9寄存器保存当前函数的地址,这一点要切记切

记。所以在汇编代码跳往c代码之前,要把t9设置好,最好用如下格式的跳转:la t9, start

jalr t9

也一定要注意在这两句之前,要把gp寄存器的值设为你动态加载时image的首地址,la t9,start这句话会分解为几句汇编代码,利用gp寄存器的值查找GOT 的位置。同时也要注意更新GOT的值,因为编译完的结果都只是相对于0地址的值,你要把这个值更新为你动态加载后的地址。这些内容不只是MIPS相关,所有系统的动态加载都会使用差不多的方法,而ARM的编译器在处理这些问题的时候还是稍微聪明点,小范围内跳转都是直接使用相对偏移跳转,不会借助于GOT,MIPS的专业编译器没有使用过。

今天在公司的邮件中,几个大牛关于如何设计中断控制器有了争论,所以觉得关于中断的话题,还是有必要再多说点,毕竟这对于系统的性能和稳定性至关重要。RISC架构中,包括ARM和MIPS,一般情况下采用的清中断机制是软件手动清理中断标志,而不是像早期CISC架构中所采用的响应中断后硬件自动清理的方法,至于CISC现在用什么机制,已经完全没有概念了。所以在写ARM或者MIPS的中断处理程序时,一定要注意在退出中断前清理中断标志位,否则就会在退出后再马上回来,形成一个死循环。

在ARM的中断相应流程中,一旦kernel开始响应一个中断,就会自动将CPSR 中的I位置位,也就是说此时不会再响应其他中断,此时没有中断嵌套一说,除非软件在ISR中主动将I位清零,但是由于ARM多种模式机制的一个问题,在某种极限情况下会导致简单嵌套处理无**常工作,如果想实现嵌套需要写很多额外的处理代码。请注意,这里的ARM不包括M系列,完全不一样的机制。ARM公司提供了VIC模块,就是vectored interrupt controller,向量中断控制器,一定程度上实现了中断优先级处理。其机制是每种中断初始化的时候会将自己这个中断的ISR地址通知VIC模块,当有中断产生时,VIC模块会取此时有中断产生的最高优先级中断的ISR地址填到一个寄存器中,在异常级别的中断处理只需要简单的一句话LDR PC, #Vector_ISR,直接将这个寄存器的内容写入到pc就实现了ISR的调用,既实现了优先级管理又加快了中断响应速度。但是要注意,即使实现了VIC,如果不做特殊处理,依然没有中断嵌套,也就是当你响应中断时,即使有更高优先级中断进来,也要等你的ISR执行完再说。

ARM核心中断响应的伪代码大致如下,这是我从软件人员的角度总结的,真正的ic设计是并行的,在判断中断状态时是把所有中断线或在一起连接到异常级别的中断线:

if(I bit in cleared)

{

if(0 != interrupt status)

{

assert interrupt line to invoke exception.

}

}

至于中断采取沿中断还是电平中断跟软件人员关系不大,一般情况下硬件人员更喜欢采用电平方式,因为电平方式就不存在中断采不到的问题了。

在RISC体系中,进入中断后的现场保护一般是由软件来完成的,如果是C代码,编译器就会替你决定将哪些寄存器入栈,而如果你的中断处理函数是汇编级别的,那就要处理哪些寄存器需要保护了,而且由于中断的执行非常频繁,你要是把所有寄存器都入栈将会是一个比较大的开销,正好合适是最好的。有人可能要说,不管入栈多少个寄存器,我只需要一条stm就行了,很快的,这只是个错觉,stm指令的执行还是一个个内存单元的写操作,入栈多少个寄存器,总线上就会有多少个针对内存单元的写操作(对于DRAM搭建的系统来说,这可是很慢的),当然stm有可能是burst写,比你多条指令写还是快一点的。

cpu在响应中断时,会将中断产生时的PC写入终端模式下的lr寄存器,并由硬件自动将cpsr中的中断使能关闭,寄存器组切换到中断模式,寄存器组的切换中最重要的是sp的切换,在某种模式下就会使用当前模式的栈指针,所以要在bootloader或者bsp中初始化所有必要的模式的sp寄存器。

下面讲解一些为什么arm实现中断嵌套会比较麻烦,我们首先描述一下一般情况下其他架构怎么做。

1、中断产生,ISR开始服务中断

2、此时有更高级别中断产生,将当前ISR必要信息入栈,响应更高级别中断

3、高优先级中断处理完毕,将低优先级中断信息恢复,继续处理低优先级中断。而由于arm架构的特殊问题,导致在某种极端情况下,这种处理方式会出现致命错误,请看如下描述:

1、中断int0产生,ISR0开始服务中断

2、修改crpsr,使能中断,使之可以中断嵌套

3、ISR0运行到某处进行一个函数调用func0,由于此时在中断模式下,所以在函数调用的时候会将函数调用的返回值填入到中断模式下的lr寄存器。

4、在执行bl或者blx的瞬间,另外一个中断int1产生,此时int1中断会等待bl或者blx执行完毕,也就是被调用函数连一句都没有被执行到时就抢走cpu,进入ISR1,而此时硬件会自从将lr值修改为子函数中的地址。

5、于是问题产生了,ISR0中调用func0的返回地址lr在保存之前就被中断isr1给覆盖了,这导致func0中记录下来的lr是个错误的地址,程序因此也就会乱套。

这从根本上来说是arm在设计时的一个瑕疵,mips用了一个非常聪明的设计解决了这个问题,而arm不知道出于什么原因这么多年来始终没有修改这个问题,而只是用一种比较繁琐的方式去规避这个问题,明天继续讲mips的解决手段和arm的规避方法。

在MIPS中,对于这个问题的处理实际上非常简单,就是如果异常发生在跳转指令执行的时候,那么在处理完异常返回之后会重新执行一遍这个跳转指令,也就是说如果异常发生在跳转指令上,那么跳转指令就被抛弃了,看似效率低,而正是由于这个简单的重新执行,避免了寄存器被复写的问题。为什么这种设计就会防止上面那种问题的发生呢?我们可以模拟一下这个流程:

1、中断int0产生,ISR0开始服务中断

2、ISR中使能中断,使之可以中断嵌套

3、ISR0运行到某处进行一个函数调用func0,对于arm来说,由于此时在中断

模式下,所以在函数调用的时候会将函数调用的返回值填入到中断模式下的lr 寄存器。而对于mips来说,没有模式一说,都是使用通用寄存器,所以会将返回地址填入ra。

4、在执行bl或者blx的瞬间,另外一个中断int1产生,此时int1中断会等待跳转指令执行完毕,也就是被调用函数连一句都没有被执行到时就抢走cpu,进入ISR1,而此时硬件会自动将lr值修改为ISR0中跳转语句+8的位置。对于MIPS 来说就是将ra值修改。

5、对于ARM来说,ISR0中调用func0的返回地址lr在保存之前就被中断isr1给覆盖了,程序因此也就会乱套。而对于MIPS来说则完全没有问题,因为架构保证在跳转语句执行时发生异常的话,跳转语句就还要被执行一遍,从根本上消除了这个问题。

ARM对这个问题的一种规避方法如下流程:

1. 中断响应进入ISR0

2. 在ISR0切换到SYS模式,然后再使能中断

3. 在SYS模式下执行相应处理

4. 关闭中断,回到中断模式,退出中断,恢复现场

这样的话,即使发生了嵌套,也只能当ISR0执行在SYS模式下时嵌套,由于此时ISR0使用的是SYS模式下的lr寄存器,而响应INT1时cpu自动更新的是INT模式的lr寄存器,不会导致lr寄存器内容的混乱。这个地方相当绕,如果有疑问可以提出来讨论,我所讲述的也只是我自己的理解。

HEX文件和BIN文件是我们经常碰到的2种文件格式。下面简单介绍一下这2种文件格式的区别:

1 - HEX文件是包括地址信息的,而BIN文件格式只包括了数据本身

在烧写或下载HEX文件的时候,一般都不需要用户指定地址,因为HEX 文件内部的信息已经包括了地址。而烧写BIN

文件的时候,用户是一定需要指定地址信息的。

3 - BIN文件格式

对二进制文件而言,其实没有”格式”。文件只是包括了纯粹的二进制数据。

4 - HEX文件格式

HEX文件都是由记录(RECORD)组成的。在HEX文件里面,每一行代表一个记录。记录的基本格式为:

+---------------------------------------------------------------+

| RECORD | RECLEN | LOAD | RECTYPE | INFO or DATA | CHKSU

M |

| MARK ':' | | OFFSET | | | |

+---------------------------------------------------------------+

| 1-byte | 1-byte | 2-byte | 1-byte | n-byte | 1-byte |

+---------------------------------------------------------------+

记录类型包括:

'00' Data Rrecord:用来记录数据,HEX文件的大部分记录都是数据记录 '01' End of File Record: 用来标识文件结束,放在文件的最后,标识HEX 文件的结尾

'04' Extended Linear Address Record: 用来标识扩展线性地址的记录

'02' Extended Segment Address Record: 用来标识扩展段地址的记录在上面的后2种记录,都是用来提供地址信息的。每次碰到这2个记录的

时候,都可以根据记录计算出一个“基”地址。

对于后面的数据记录,计算地址的时候,都是以这些“基”地址为基础的。

数据记录的具体格式:

+---------------------------------------------------------------+

| RECORD | RECLEN | LOAD | RECTYPE | INFO or DATA | CHKSU M |

| MARK ':' | | OFFSET | '00' | | |

+---------------------------------------------------------------+

| 1-byte | 1-byte | 2-byte | 1-byte | n-byte | 1-byte |

+---------------------------------------------------------------+

看个例子:

:020*********FA

:10000400FF00A0E314209FE5001092E5011092E5A3

:00000001FF

对上面的HEX文件进行分析:

第1条记录的长度为02,LOAD OFFSET为0000,RECTYPE为04,说明该记录为扩展段地址记录。数据为0000,校验和为

FA。从这个记录的长度和数据,我们可以计算出一个基地址,这个地址为0X0000。后面的数据记录都以这个地址为基

地址。

第2条记录的长度为10(16),LOAD OFFSET为0004,RECTYPE为00,说明该记录为数据记录。

数据为FF00A0E314209FE5001092E5011092E5,共16个BYTE。这个记录的校验和为A3。此时的基地址为0X0000,加上OFFSET,

这个记录里的16BYTE的数据的起始地址就是0x0000 + 0x0004 = 0x0004.

第3条记录的长度为00,LOAD OFFSET为0000,TYPE = 01,校验和为FF。说明这个是一个END OF FILE RECORD,标识

文件的结尾。

在上面这个例子里,实际的数据只有16个BYTE:FF00A0E314209FE5001092E5011092E5,其起始地址为0x4

4 - HEX文件和BIN文件大小有区别

HEX文件是用ASCII来表示二进制的数值。例如一般8-BIT的二进制数值0x3F,用ASCII来表示就需要分别表示字符'3'

和字符'F',每个字符需要一个BYTE,所以HEX文件需要 > 2倍的空间。

对一个BIN文件而言,你查看文件的大小就可以知道文件包括的数据的实际大小。而对HEX文件而言,你看到的文件

大小并不是实际的数据的大小。一是因为HEX文件是用ASCII来表示数据,二是因为HEX文件本身还包括别的附加信息。

要想详细了解HEX文件格式,请GOOGLE INTEL HEX,就能找到详细的PDF 文档。

(一)2440专区:

优秀论坛专区:

第一:https://www.360docs.net/doc/2517703680.html,/金牌论坛,不解释

第二:https://www.360docs.net/doc/2517703680.html,/

第三:https://www.360docs.net/doc/2517703680.html,/

第四:https://www.360docs.net/doc/2517703680.html,/bbs/

第五:https://www.360docs.net/doc/2517703680.html,/bbs/forum-70-1.html

转帖专区:

【转贴】2440使用常见问题解析: https://www.360docs.net/doc/2517703680.html,/thread-353-1-1.html

【转帖】让您爱不释手的专业串口调试软件:https://www.360docs.net/doc/2517703680.html,/thread-282-1-1.html

【转帖】嵌入式入门笔记:https://www.360docs.net/doc/2517703680.html,/thread-272-1-1.html

【转帖】软硬件协同设计技术:https://www.360docs.net/doc/2517703680.html,/thread-315-1-1.html

【转帖】北大嵌入式开发讲义:https://www.360docs.net/doc/2517703680.html,/thread-321-1-1.html

【转帖】Windows CE开发初步:https://www.360docs.net/doc/2517703680.html,/thread-235-1-1.html

下载专区:

【PDF下载】飞凌2440开发板技术手册:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1y.html 【PDF下载】JLINK用户手册:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1y.html

【PDF下载】摄像头芯片DATASHEET:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1y.html 【RAR下载】UCOS-II:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1y.html

【PPT下载】ARM相关:https://www.360docs.net/doc/2517703680.html,/thread-391-1-1.html

【PPT下载】Linux相关:https://www.360docs.net/doc/2517703680.html,/thread-391-1-1.html

图文专区:

【源码文章】Bootloader源码: https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpw1.html

【源码文章】2440下的流水灯实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpw3.html

【源码文章】2440下的CAN总线实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpw7.html

【源码文章】2440下的fork实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpw7.html

【源码文章】2440下的IIC实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpwb.html

【源码文章】2440下的看门狗实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpwb.html

【源码文章】2440下的USB实验:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpwf.html

【源码文章】2440下的触摸屏测试:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1y.html

【源码文章】2440下的温度传感器:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hpwi.html

【源码文章】Linux文件系统移植详解:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hq1u.html 【源码文章】Linux内核移植详解1:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hisp.html

Linux内核移植详解2:https://www.360docs.net/doc/2517703680.html,/s/blog_6536a9450100hisp.html

视频专区:

【在线视频】《嵌入式系统综述》:https://www.360docs.net/doc/2517703680.html,/programs/view/KL0y2VqgZOs/

【在线视频】《嵌入式学习基础》:https://www.360docs.net/doc/2517703680.html,/programs/view/W6XpvmGZfG4/

【在线视频】《嵌入式环境》:https://www.360docs.net/doc/2517703680.html,/programs/view/W6XpvmGZfG4/

【在线视频】《嵌入式BootLoader》:https://www.360docs.net/doc/2517703680.html,/programs/view/1kY_23UccC0/

【在线视频】《嵌入式ARM学习-上》:https://www.360docs.net/doc/2517703680.html,/programs/view/BWy4tXo4o5A/《嵌入式ARM学习-下》:https://www.360docs.net/doc/2517703680.html,/programs/view/dFxXjq8nEm0/

【在线视频】《嵌入式ARM精华理论》:https://www.360docs.net/doc/2517703680.html,/programs/view/dFxXjq8nEm0/

ARM C语言嵌入式系统编程修炼

这本书交了许多编程技巧,包括软件构架,内存操作等。对嵌入式C的编写很有帮助

不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。而与之相比,C 语言-- 一种" 高级的低级" 语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,一次又一次感受到 C 语言的精妙,沉醉于 C 语言给嵌入式开发带来的便利。

https://www.360docs.net/doc/2517703680.html,

ARM最新资料[c语言嵌入式语法]+.rar (316.85 KB, 下载次数: 1828) https://www.360docs.net/doc/2517703680.html,/forum.php?mod=attachment&aid=MTI0NTI2fDJlZjliNzM5f DEzNjQ1MTg0OTd8NDU2NDR8Mzc3OTUx

分享ARM 课件有想学习看

嵌入式资料.rar

https://www.360docs.net/doc/2517703680.html,/forum.php?mod=attachment&aid=M jAyMTZ8ODdhZTExYmR8MTM2NDUxODY4OHw0NTY0NHw xNjM5MDQ%3D

5.分享—周立功《深入浅出ARM7》上、下

https://www.360docs.net/doc/2517703680.html,/icview-167484-1-1.html

6.【申精】精品嵌入式资料导航

https://www.360docs.net/doc/2517703680.html,/icview-160537-1-1.html

7.共享我研究生三年的项目资料··2012最给力!

https://www.360docs.net/doc/2517703680.html,/icview-294800-1-1.html

8.国嵌加密视频提供

https://www.360docs.net/doc/2517703680.html,/icview-259691-1-1.html

9.【飞凌资料】最好的Linux资料--一日千里如旋风

https://www.360docs.net/doc/2517703680.html,/icview-202316-1-1.html

10.Linux零基础初级教程

https://www.360docs.net/doc/2517703680.html,/icview-178539-1-1.html

11.自己写的《u-boot-1.2.0移植手册》,70多页

https://www.360docs.net/doc/2517703680.html,/icview-148714-1-1.html

12.刚到发个ARM新手入门

https://www.360docs.net/doc/2517703680.html,/icview-154202-1-1.html

13.三星S3C2440中文技术手册-原创翻译

https://www.360docs.net/doc/2517703680.html,/icview-136231-1-1.html

14.HEX文件和BIN文件格式的区别

https://www.360docs.net/doc/2517703680.html,/icview-126904-1-1.html

15.S3C6410裸机开发教程--RVDS环境搭建+调试方法+程序实例讲解

https://www.360docs.net/doc/2517703680.html,/icview-238682-1-1.html

相关主题
相关文档
最新文档