1、 CORTEX-M3 汇编 语言 实践编程 基于 STM32F103 系列 MCU 2015-1-9 三两二锅头 Cortex-M3 汇编语言 实践编程 - 1 - 写在前面 : 接触 Cortex-M3 已经有一段时间 了 ,大大小小也做了几个项目 , 可以说对这个系列的片子有了一定的了解 。 相对于 8 位 单片机来说 CM3 给我 的感觉 实在 是强了太多,其中比较 明显的感觉是存储上的扩充,这让我在编程的时候不必为了节省几 十 个字节 的 内存而大费周章 。还记得 当初用 ATmega16 写一个 项目的时候 ,申请 了一个比较大的缓冲区之后要好多个模块 共 用,搞得程序结构非常 乱
2、 而且还容易出错, 当然 这还只是一个小惊喜 。 更大的 优势 在于 CM3 先进 高效的中断机制以及它丰富的外围接口 和 强大 的 片内功能 ,这些让我在开发的过程中深切的体会到 CM3 相对于 8 位 单片机的优越性。而相对于 高端 的ARM 芯片 ( ARM7、 ARM9 等 )来讲 CM3 又以它精巧灵活的 特性 让我深深的喜欢上了这个系列的芯片,尤其是 新的指令集 给人感觉耳目一新,让原来繁杂的芯片初始化工作最终浓缩到数十行汇编代码中, CM3 以它的精简和易用再一次吸引了我。 经过一段 时间的学习和使用,个人觉得 如果 想 把 一款芯片用的得心应手一些必要的理论知识还是值得花时间去
3、学习和研究的,所以在工作之余就有了这个文档。 本文 主要从汇编语言的角度去阐述和学习 Cortex-M3 的 体系结构以及基本工作原理, 实现 对一些 片内功能的配置与应用 同时 还包括一些简单的外设应用 。 本文 重点不在于深究汇编指令码,而是 通过 使用汇编语言 让 读者从计算机的角度出发去思考问题, 了解 计算机的工作原理和步骤, 所以汇编指令 码 的细节内容在这里则不会 深入 讨论。 声明 : 本文 内容完全 属于 个人学习总结以及个人的理解和看法 , 在学习过程中借鉴过很多资料包括来源于网络的资料, 如果 文中内容让您感觉不适请联系作者删除。 由于 作者水平有限文中难免出现错误,如遇
4、到错误烦请您 不吝 指正,并欢迎 您与作者进行交流沟通 。 作者默认您对 CM3 有一定的了解,并且 具备基于 CM3 芯片的 C 语言开发经验。 文中 使用的芯片是 STM32F103ZE,软件开发环境用的是 *(因为涉及到版权问题本来 考虑在 Linux 平台下进行编写代码 但是 考虑到大家 可能 很少有人用 Linux 开发程序,所以选了和大家一样的开发环境)。 下面 作者将从一个空的汇编工程展开本文。 Cortex-M3 汇编语言 实践编程 - 2 - 1 第一个 汇编工程 我们 创建工程的时候 IDE 会 提示我们是否 导入启动文件,而这个启动文件就是用汇编语言编写的, 在这里 简单
5、说一下汇编工程的创建步骤: 新建一个 工程 创建 工程的 步骤和平时创建工程一样 , 之余工作目录之类的东西相信各位自己也都有自己的习惯这里就不再赘述。 Figure 1-1 新建工程 工程名字 自己随便 写一个 就行 Figure 1-2 工程 名字 选择 对应芯片,在出 现是否创建启动文件时选择不创建。 Figure 1-3 Cortex-M3 汇编语言 实践编程 - 3 - 配置 工程 修改 工程目录以及配置 目标文件 输出位置,保证工程目录 文件 便于管理。 Figure 1-4 创建 工程目录 单击 途中 1 号 位置 单击 2 号 位 选择 obj 目录 4 好位置 选中创建 HE
6、X 文件 Figure 1-5 配置目标文件输出 位置 Cortex-M3 汇编语言 实践编程 - 4 - 按照图 中序号顺序完成后续配置 修改部分 工程 信息, 1、 2 两处 改成自己对应的工程名与组名 Figure 1-6 修改 工程 信息 Cortex-M3 汇编语言 实践编程 - 5 - 新建 一个文本文件, 并 保存为汇编源程序文件 Figure 1-7 新建 文件 Figure 1-8 保存文件 将保存好的 文件添加到工程 , 添加完毕效果如下: Figure 1-9 添加 到 工程 Cortex-M3 汇编语言 实践编程 - 6 - 编写程序 Figure 1-10 第一段 汇
7、编程序 对于 上面的这段程序需要详细的解释一下: 1. 正文 第 1 行以 分号( ;)开始,在 ARM 汇编里表示 单行注释 , ARM 汇编不支持多行注释 。 2. 第三 行 代码的 内容在 ARM 汇编中被称之为伪代码,这行代码的意思是 定义 一个段 section, 段是汇编语言 组织代码 的基本单位, 功能 与 C 语言中的函数类似,代码写在段的外面也允许,但是会出一些问题。这一行 代码 中 AREA 伪指令 表示声明一个段 , RESET 是段的名字 ,在 当前平台上 RESET 段 也是系统默认的入口 , 所以 在 代码中 有且 只有一个 RESET 段。 CODE 表示短的属性
8、 , 代表当前段为代码段 , 和 CODE 功能 类似的还有 DATA、 STACK、 HEAP 等分别表示数据段、栈和堆。 READONLY 是 当前段的访问 属性 表示 只读 , 还有 READWRITE 表示可读可写。 3. 第 4 行 代码只有一个单词 ENTRY,表示程序入口 ,即 CPU 会从 ENTRY 标记的这一行代码处开始执行程序 ,类似 与 C 语言中的 main 函数, 但是 与 C 语言不同的事 ARM 汇编允许一个程序有多个入口,但是用户需要 指定 一个入口作为 主Cortex-M3 汇编语言 实践编程 - 7 - 入口 。 关于 入口的定义你还可以在 MDK 参考
9、手册上找到如下解释:Figure 1-11ENTRY 官方 解释 4. 第 6 行 代码 “START PROC”与 第 12 行 代码 “ENDP”通常成对 出现,表示定义 一个子程序,其中 START 是子程序名或者叫做标号 , 在 ARM 汇编当中代表当前子程序的入口地址,相当于 C 语言中的函数名的作用。同时 大家 应该注意到这个START 的 位置是顶格写的 , 这是因为在 ARM 汇编 中规定 : 标号必须要顶格写,而指令、伪指令、伪操作等指令码( 或者称 为关键字 例如 MOV、 ADD 等 属于指令、 LDR、 ADR 等 属于伪指令、 DCD、 IF/ENDIF 等属于 伪操
10、作) 在 书写的时候需要有前导空格,一般我们用一个或者多个 TAB 代替。 例如 我们把第 8 行 内容变成 如下形式 :Cortex-M3 汇编语言 实践编程 - 8 - 编译程序 我们 会发现如下错误 :事实上 编译器是把 第 8 行 指令的 MOV 当成 标号来处理 , 而 把 R0 当成 的指令操作码 , 因为无论是指令、伪指令、还是伪操作中都不包含 R0 这条指令码,所以编译器会 报出 上述错误。 同理 如果把 第 6 行的 START 加上 前导空格也会报出类似的错误,在这里就不重复展示了。 5. 第 7 行 EXPORT START 是一条 链接属性 声明语句,表示 START
11、这个 子程序 (确切 的说是这个标号或叫 标识符 )在其他汇编语言文件中也可以被调用 , 但是如果其他文件中想调用 START 还需要将这个标号进行导入 , 导入的方法是 ”IMPORT START“, EXPORT 与 IMPORT 是 一对操作符,用 EXPORT 声明的标号只有通过 IMPORT 导入以后才能正常使用。 6. 最后 14 行 END 表示当前汇编语言源文件的结束, ARM 汇编编译器在编译期对预处理过 的汇编语言源文件进行逐行扫描,当遇到 END 关键字的时候表示当前文件扫描结束,编译器将不会处理写在 END 之后的任何内容。 经过 上面的介绍我们得出一些结论, 这些 结
12、论是关于编写一个 ARM 汇编程序的必须因素,它们是: 1. 一个 汇编语言程序至少要有一个代码段,否则芯片将不知道执行什么内容。 2. 一个 汇编语言程序至少要有一个入口,否则芯片将不 知道该从什么地方开始执行程序。 3. 一个 汇编语言源文件应该以 END 进行 标记结束。 4. 在 当前 IDE 环境下系统默认的段名字叫 RESET, 这里没有明确说 RESET 段必须是代码段,所以也可以是其他 属性 的段。 Cortex-M3 汇编语言 实践编程 - 9 - 编译 、调试程序 接下来 我们把 刚刚 写好的程序进行编译 , 我们会发现这里有个警告: Figure 1-12 第一个 程序
13、编译警告 双击 这个警告 会 跳出如下内容: Figure 1-13 警告对应内容 自习 查看我们会发现这个以 .sct 结尾 的文件实际是编译器在 编译 期生成的一个中间文件,警告内容说无法匹配 *(InRoot$Sections)这个 段, 实际 上这个段就是编译器 在编译 期 给 C 语言中的 main 函数 定义 的一个别名,因为我们没有写 .C 文件 更没有写 main 函数所以编译器自然就不能匹配到 main 函数了 , 这个问题属于编译器的原因造成的 , 即编译器在编译程序的时候需要有 main 函数,否则将会报出警告。处理 的 办法是在工程当中添加一个 C 语言源文件并写入一
14、个空的 main 函数。 Cortex-M3 汇编语言 实践编程 - 10 - Figure 1-14 将 上述 写好 的文件保存并添加到工程后编译: Figure 1-15 这个时候 又 变成 了 另外 一个警告,意思是有多个程序入口点被定义,但是没有指定一个主入口,原因是 main 函数本身 也 是一个程序的默认入口与我们子 汇编 文件中写的 ENTRY 意思是一样的,所以我们需要 指定 一个主入口 或者只保留一个入口就可以了, 因为 程序中不需要很多入口所以只保留 main 删除 ENTRY。 Figure 1-16 Cortex-M3 汇编语言 实践编程 - 11 - 修改 后再次编译
15、就没有错误了,但是只有这几行代码芯片还是无法正常工作的,所以暂时还不能使用设备,那我们只好借助仿真器来调试一下看一看程序的结果,首先配置调试目标 , 需要去掉 Run to main 这个 选项,让程序从汇编代码开始执行 : Figure 1-17 调试 配置 点击 调试选项 则 会出现以下情况 , 看看 反汇编 器以及寄存器的值,完全不知道程序执行到哪里了 。 Figure 1-18 Cortex-M3 汇编语言 实践编程 - 12 - 点击 复位 之后 可以看到以下界面 , 鼠标选中一行代码 , 反汇编器会自动跳到对应的位置。 Figure 1-19 首先 观察 2 号 标记 ,也就是 P
16、C 寄存器 的值 是 0x0102F04E, 同时 注意我们在程序第 7行 的位置已经添加了断点, 断点处代码 对应的地址应该是 0X08000000,图中 3 号 所标记的地址, 但是 这个 PC 的 地址很明显 说明 是程序跑飞了, 也就是 说程序根本没有按照我们预想的顺序去执行。 再 观察 3 和 4 说明 在地址 0x8000000 处存放的正好是我们编写的第一行代码 ”MOV R0, #1“, 而这条汇编指令对应的二进制机器码是 0xF04F0001。 实际 上 一般来讲系统代码都应该是从 0x00 地址处 开始的这里为什么是 0x08000000 呢? 那是因为 这里 的地址( 0
17、x08000000)是硬件的物理地址, 而 那个 0x00 地址 只是逻辑地址,二者之间是有一个映射关系的, IDE 在这里已经帮我们把映射之后的地址显示出来了,所以我们在这里看到的实际的物理地址( 0x08000000)而不是逻辑地址( 0x00000000)。 这一点 也可以通过下图来证明 Cortex-M3 汇编语言 实践编程 - 13 - Figure 1-20 图中 的配置选项里 分别 给出了内存中只读存储 区 ( READONLY, 经常用来存放代码或者常量) 与 读写存储 区 ( READWRITE, 用于 存放数据、变量等) 的 起始地址与大小, 这个 地址是系统默认给出的我们
18、一般不会做修改。 我们 的代码段 的 访问熟 属性 是READONLY, 那第一行代码的地址自然是从 0X08000000 处 开始。 如果我们 再观察一下CM3 的存储空间分配则不难发现 0X08000000 恰好 属于代码段,而 0X20000000 恰好 是SRAM 的起始地址。 Figure 1-21 Cortex-M3 汇编语言 实践编程 - 14 - 到这里 我们仍然没有解决一个问题,那就是为什么程序会跑飞了呢? CM3 的启动过 程 如果 想 彻底 解决程序跑飞的问题 我们 就需要好好的研究一下 CM3 在 启动的时候都做了哪些 事情 。 一般来讲 芯片启动后都是从 0x0000
19、0000 地址处 开始 执行 ,然后 直接进入系统的 RESET 异常处理 , 在 RESET 处理中可能做一些堆栈初始化、寄存器复位等工作 , 处理完毕之后最终跳转到 C 语言编写的 main 函数 当中执行 C 语言程序。 CM3 也不 例外 , 同样 是从0x00000000 地址处 开始执行程序,但是它和 其他 的 芯片有一点差别,那就是 Cortex-M3 放在 0x00000000 地址处 的并不是 RESET 的入口地址而是其他的东西,是什么 呢?请看 下表( 表格 出自 Cortex-M3 权威指南 英文版第 40 页 ): Figure 1-22Cortex-M3 向量表 通
20、过 观察上表我们不难发现, CM3 的向量表 0x00 地址 处存放的并不是 RESET 程序 的入口而是系统主堆栈的入口地址,而 0x04 地址处 存放的才是真正的 RESET 程序 入口地址。 Cortex-M3 汇编语言 实践编程 - 15 - 原来 CM3 启动时虽然是从 0x00000000 地址处 开始但是并没有直接进入 RESET 程序,而是先把存放 在 0x00000000 地址 处的 数据赋值给主堆栈指针 ( 也就是 MSP 寄存器 ,因为 主堆栈和线程堆栈是绑定地址的, 两个寄存器 的名字都是 R13,只是在处理器模式进行切换的时候 由硬件 自动切换堆栈, 所以 我们在编写
21、程序的时候无论是访问主堆栈 还是访问线程堆栈 实际上操作的都是 R13) , 然后 再把 0x04 地址 处的值赋给 PC。 芯片 这么 做的目的是因为默认开机后芯片处于特权级别下的 Handler 模式,该模式下有一些操作必须要用的主堆栈( MSP),所以系统在启动的时候必须要先设置主堆栈,然后再进入 RESET过程。 那 如何来证明上面所说的 是 正确的呢? 我们 先来看这样一张图: Figure 1-23 内存查看器 Figure 1-24 中 显示的 是 0X08000000 处 的内存信息 ,这个地址是 存储器的 物理 地址,也是经过映射之后的 0x00000000 地址处 。再 结
22、合 Figure 1-25 中 3 号 和 4 号 旁边的信息可以确定上图中红色矩形框中所选中的数据正是指令 ”MOV R0, #1“和 ”MOV R1, #2“的 二进制机器码 。 而 R13( SP) 里面 的内容是 0x0001F04F(是 0x08000000 处 的内容,小端模式) , R15( PC) 的内容 是 0x0102F04E( 是 0x08000004 处 的内容,小端模式 。 Thumb2 指令 至少是半字对齐的, 所以 PC 寄存器的 最低有效位 总是 读回 0, 所以我们看到的 PC 值是0x0102F04E 而不是内存中存放的 0x0102F04F) 。 根据 上
23、面的解释, PC 在程 序开始前被 赋值 为 0x0102F04E,在 这个地址里存放的内容我们根本不知道是什么东西 , 所以程序跑飞也就成了必然事件。 解释 了程序为什么会跑飞那接下来就来解决这个问题 。 其实 程序 跑飞的主要原因就是在系统启动的时候 PC 被赋予了错误的值,想解决这个问题其实很简单,把正确的值丢给 PC 就行了。 那么 如何才能把正确的值给 PC 呢? 这里 我们就不得不提到 CM3 的异常向量表也就是Figure 1-26 中 所展示的内容,这张表中标记处了 CM3 在遇到各种系统异常时候对应的处理程序的入口地址,当然也包括了 RESET( 系统 复位) 时候 的入口
24、。所以解决 PC 赋值 最 合适 的办法就是 在我们 的程序中构建一张和 Figure 1-27 内容 一模一样的表,而且这张表的 起始地址 应该是 逻辑 的 0x00000000。 因为 编译器在链接期间会默认 把命名 为 ”RESET“的 段 放到 镜像 的 起始地址 处 也就是 0x00 地址 ,所以我们 需要 在 RESET 段 中构建 向量表 。 因为 也Cortex-M3 汇编语言 实践编程 - 16 - 没有 比 start_up.s 里面 定义的更好的结构和书写方式, 所以 这里直接借用 IDE 自动生成的向量表 部分 的代码 。 这个 向量表 应该是 只读 属性,而且 应该
25、是一个数据段 (因为向量 表中存放的都是一些程序的入口,类似于一个函数指针数组 或 转移表,所以是数据段)所以之前写的代码就 要 作一番改动了,具体如下: Figure 1-28 代码 实现 向量表 上图 中 的代码 就是 系统向量表 的实现 。 其中 第 4 行 是向量表的定义 , 向量表直接处于RESET 段 的最开始处 , 段的属性为只读数据段 , 这个段在编译器链接程序的时候将会被放在镜像文件的最开始处。第 5、 6、 7 行表示将向量表 的属性( 起始地址 、大小) 进行 全局声明,第 24 行 是在向量表的尾部添加 部分 空数据用来保护表结构 不被 意外修改 。表中 的数据要么是异
26、常处理子程序的入口要么是数据段的入口,所以既然这里用到了这些标号那在程序中就要有对应的定义。接下来 补全 这些定义 , 先添加系统堆栈定义: Figure 1-29 系统 堆栈 定义 Cortex-M3 汇编语言 实践编程 - 17 - 在 RESET 段 之前添加 堆栈定义 , 这 里需要对 MSP 和 PSP 进行分别定义,而且要注意这个段的属性是可读可写的数据段, NOINIT 表示数据段是未初始化的或初始化为零。其只包含零初始化的空间保留命令 SPACE 或 DCB、 DCD、 DCDU、 DCQ、 DCQU、 DCW 或 DCWU。可以决定在链接时 AREA 是未初始化的还是零初始化
27、的 。 这里 要 注意 ,上图中 第 10 行 和第 18 行 中 两个 MSP_TOP, 第 9 行 表示定义一个大小为MSP_SIZE 的连续空间,因为 CM3 的堆栈操作方式固定为 递减堆栈 ,所以栈顶应该在数据之后也就是第 10 行 的位置 。 第 18 行 是 向量表 的首 个元素也就是 MSP 的入口地址,这里应该对应的就是我们之前定义的 MSP 的栈顶即 MSP_TOP。接下来 是异常处理子程序入口定义 , 这里只列出部分代码: Figure 1-30 异常处理子程序 上图 中 Reset_Handler 这个 子程序就是系统启动后开始执行代码的位置了,这里的 |.TEXT|又是
28、 一个默认 设置 , 这个 标号 是 系统 默认 代码段 的 名字 。 一般 在 这个 子程序 里 做一些 必要的 初始化 工作 之后 就可以 跳到 C 语言 的 main 函数 执行 了 , 但是 我们 现在 是 写汇编 程序 所以 暂时 先不进行 跳转 。 那么 接下来 就可以 把 之前 的 代码 写在 Reset_Handler 中 测试 一下 了 , 程序 中 添加 如下 代码 : Figure 1-31 编写 测试 代码 Cortex-M3 汇编语言 实践编程 - 18 - 为了 不让 程序 跑飞 , 在 50 行 处 添加 死循环 , 编译 : Figure 1-32 编译程序 调试 : Figure 1-33 调试 代码 点击 单步 执行 , 三行 代码 执行 完毕 后 对应 寄存器 中 的 内容 已经 发生 了 变化 , 到此 我们 的 第一个 汇编 程序 就 结束 了 。