C语言嵌入式系统编程修炼之内存操作.pdf
C 语言嵌入式系统编程修炼之内存操作 C 语言嵌入式系统编程修炼之内存操作 数据指针 数据指针 在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应 的 MOV 指令, 而除 C/C以外的其它编程语言基本没有直接访问绝对地址的能力。 在嵌入式系统的实际调试中, 多借助 C 语言指针所具有的对绝对地址单元内容的 读写能力。以指针直接操作内存多发生在如下几种情况 1 某 I/O 芯片被定位在 CPU 的存储空间而非 I/O 空间, 而且寄存器对应于 某特定地址; 2 两个 CPU 之间以双端口 RAM 通信, CPU 需要在双端口 RAM 的特定单元 (称 为 mail box)书写内容以在对方 CPU 产生中断; 3 读取在 ROM 或 FLASH 的特定单元所烧录的汉字和英文字模。 譬如 unsigned char *p unsigned char *0 xF000FF00; *p11; 以上程序的意义为在绝对地址 0 xF00000 xFF0080186 使用 16 位段地址和 16 位偏移地址写入 11。 在使用绝对地址指针时, 要注意指针自增自减操作的结果取决于指针指向的 数据类别。上例中 p后的结果是 p 0 xF000FF01,若 p 指向 int,即 int *p int *0 xF000FF00; p或p的结果等同于p psizeofint,而 p-或-p的结果是 p p-sizeofint。 同理,若执行 long int *p long int *0 xF000FF00; 则 p或p的结果等同于p psizeoflong int ,而 p-或-p的结 果是 p p-sizeoflong int。 记住CPU 以字节为单位编址,而 C 语言指针以指向的数据类型长度作自增 和自减。理解这一点对于以指针直接操作内存是相当重要的。 函数指针函数指针 首先要理解以下三个问题 (1)C 语言中函数名直接对应于函数生成的指令代码在内存中的地址,因 此函数名可以直接赋给指向函数的指针; (2)调用函数实际上等同于“调转指令参数传递处理回归位置入栈“, 本质上最核心的操作是将函数生成的目标代码的首地址赋给 CPU 的 PC 寄存器; (3)因为函数调用的本质是跳转到某一个地址单元的 code 去执行,所以可 以“调用“一个根本就不存在的函数实体,晕请往下看 请拿出你可以获得的任何一本大学 微型计算机原理 教材, 书中讲到, 186 CPU 启动后跳转至绝对地址 0 xFFFF0(对应 C 语言指针是 0 xF000FFF0,0 xF000 为段地址,0 xFFF0 为段内偏移)执行,请看下面的代码 typedef void *lpFunction ; /* 定义一个无参数、无返回类型的 */ /* 函数指针类型 */ lpFunction lpReset lpFunction0 xF000FFF0; /* 定义一个函数指 针,指向*/ /* CPU 启动后所执行第一条指令的位置 */ lpReset; /* 调用函数 */ 在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了 这样的函数调用lpReset,它实际上起到了“软重启“的作用,跳转到 CPU 启 动后第一条要执行的指令的位置。 记住函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质 上只是换一个地址开始执行指令 数组 vs.动态申请数组 vs.动态申请 在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求, 这是因 为嵌入式系统的内存空间往往是十分有限的, 不经意的内存泄露会很快导致系统 的崩溃。 所以一定要保证你的malloc和free成对出现, 如果你写出这样的一段程序 char * functionvoid char *p; p char *malloc; ifpNULL ; /* 一系列针对 p 的操作 */ return p; 在某处调用 function,用完 function 中动态申请的内存后将其 free,如 下 char *q function; freeq; 上述代码明显是不合理的,因为违反了 malloc 和 free 成对出现的原则,即 “谁申请,就由谁释放“原则。不满足这个原则,会导致代码的耦合度增大,因为 用户在调用 function 函数时需要知道其内部细节 正确的做法是在调用处申请内存,并传入 function 函数,如下 char *pmalloc; ifpNULL ; functionp; freep; pNULL; 而函数 function 则接收参数 p,如下 void functionchar *p /* 一系列针对 p 的操作 */ 基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推 荐你尽量采用数组 嵌入式系统可以以博大的胸襟接收瑕疵, 而无法“海纳“错误。 毕竟, 以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的 杨康。 给出原则 (1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数 组越过界限就光荣地成全了一个混乱的嵌入式系统); (2) 如果使用动态申请, 则申请后一定要判断是否申请成功了, 并且 malloc 和 free 应成对出现 关键字 const关键字 const const 意味着“只读“。区别如下代码的功能非常重要,也是老生长叹,如果 你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲 哀 const int a; int const a; const int *a; int * const a; int const * a const; (1)关键字 const 的作用是为给读你代码的人传达非常有用的信息。例如, 在函数的形参前添加 const 关键字意味着这个参数在函数体内不会被修改, 属于 “输入参数“。在有多个形参的时候,函数的调用者可以凭借参数前是否有 const 关键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。 (2)合理地使用关键字 const 可以使编译器很自然地保护那些不希望被改 变的参数,防止其被无意的代码修改,这样可以减少 bug 的出现。 const 在 C语言中则包含了更丰富的含义,而在 C 语言中仅意味着“只能 读的普通变量“,可以称其为“不能改变的变量“(这个说法似乎很拗口,但却最 准确的表达了 C 语言中 const 的本质),在编译阶段需要的常数仍然只能以 define 宏定义故在 C 语言中如下程序是非法的 const int SIZE 10; char aSIZE; /* 非法编译阶段不能用到变量 */ 关键字 volatile关键字 volatile C 语言编译器会对用户书写的代码进行优化,譬如如下代码 int a,b,c; a inWord0 x100; /*读取 I/O 空间 0 x100 端口的内容存入 a 变量*/ b a; a inWord 0 x100; /*再次读取 I/O 空间 0 x100 端口的内容存入 a 变 量*/ c a; 很可能被编译器优化为 int a,b,c; a inWord0 x100; /*读取 I/O 空间 0 x100 端口的内容存入 a 变量*/ b a; c a; 但是这样的优化结果可能导致错误, 如果 I/O 空间 0 x100 端口的内容在执行 第一次读操作后被其它程序写入新值, 则其实第 2 次读操作读出的内容与第一次 不同,b 和 c 的值应该不同。在变量 a 的定义前加上 volatile 关键字可以防止 编译器的类似优化,正确的做法是 volatile int a; volatile 变量可能用于如下几种情况 1 并行设备的硬件寄存器(如状态寄存器,例中的代码属于此类); 2 一个中断服务子程序中会访问到的非自动变量也就是全局变量; 3 多线程应用中被几个任务共享的变量。 CPU 字长与存储器位宽不一致处理CPU 字长与存储器位宽不一致处理 在背景篇中提到,本文特意选择了一个与 CPU 字长不一致的存储芯片,就是 为了进行本节的讨论,解决 CPU 字长与存储器位宽不一致的情况。80186 的字长 为 16,而 NVRAM 的位宽为 8,在这种情况下,我们需要为 NVRAM 提供读写字节、 字的接口,如下 typedef unsigned char BYTE; typedef unsigned int WORD; /* 函数功能读 NVRAM 中字节 * 参数wOffset,读取位置相对 NVRAM 基地址的偏移 * 返回读取到的字节值 */ extern BYTE ReadByteNVRAMWORD wOffset LPBYTE lpAddr BYTE*NVRAM wOffset * 2; /* 为什么偏移要 2 */ return *lpAddr; /* 函数功能读 NVRAM 中字 * 参数wOffset,读取位置相对 NVRAM 基地址的偏移 * 返回读取到的字 */ extern WORD ReadWordNVRAMWORD wOffset WORD wTmp 0; LPBYTE lpAddr; /* 读取高位字节 */ lpAddr BYTE*NVRAM wOffset * 2; /* 为什么偏移要2 */ wTmp *lpAddr*256; /* 读取低位字节 */ lpAddr BYTE*NVRAM wOffset 1 * 2; /* 为什么偏移要2 */ wTmp *lpAddr; return wTmp; /* 函数功能向 NVRAM 中写一个字节 *参数wOffset,写入位置相对 NVRAM 基地址的偏移 * byData,欲写入的字节 */ extern void WriteByteNVRAMWORD wOffset, BYTE byData /* 函数功能向 NVRAM 中写一个字 */ *参数wOffset,写入位置相对 NVRAM 基地址的偏移 * wData,欲写入的字 */ extern void WriteWordNVRAMWORD wOffset, WORD wData 子贡问曰Why 偏移要乘以 2 子曰请看图 1,16 位 80186 与 8 位 NVRAM 之间互连只能以地址线 A1 对其 A0,CPU 本身的 A0 与 NVRAM 不连接。因此,NVRAM 的地址只能是偶数地址,故每 次以 0 x10 为单位前进 图 1 CPU 与 NVRAM 地址线连接 子贡再问So why 80186 的地址线 A0 不与 NVRAM 的 A0 连接 子曰请看IT 论语之微机原理篇,那里面讲述了关于计算机组成 的圣人之道。 总结总结 本篇主要讲述了嵌入式系统 C 编程中内存操作的相关技巧。 掌握并深入理解 关于数据指针、函数指针、动态申请内存、const 及 volatile 关键字等的相关 知识,是一个优秀的 C 语言程序设计师的基本要求。当我们已经牢固掌握了上述 技巧后,我们就已经学会了 C 语言的 99,因为 C 语言最精华的内涵皆在内存操 作中体现。 我们之所以在嵌入式系统中使用 C 语言进行程序设计,99是因为其强大的 内存操作能力 如果你爱编程,请你爱 C 语言; 如果你爱 C 语言,请你爱指针; 如果你爱指针,请你爱指针的指针