如何练习C语言嵌入式系统编程——内存操作
作者:宋宝华-邮箱:[email][/email]
1. 数据指针
在嵌入式系统的编程中,经常需要对特定的内存单元中的内容进行读写,而汇编中有相应的MOV指令。 然而除了C/C++之外的其他编程语言基本上不具备直接访问绝对地址的能力。 在嵌入式系统的实际调试中,经常使用C语言指针来读写绝对地址单元的内容。 使用指针直接操作内存通常发生在以下情况:
(1)I/O芯片位于CPU的存储空间而不是I/O空间,寄存器对应特定的地址;
(2) 两个CPU通过双口RAM进行通信。 CPU需要向双口RAM的特定单元(称为邮箱)写入内容,以在另一个CPU中产生中断;
(3)读取ROM或FLASH特定单元中烧录的汉字和英文字库。
例如:
字符 *p = ( 字符 *);
*p=11;
上面程序的意思是在绝对地址+处写入11(80186使用16位段地址和16位偏移地址)。
使用绝对地址指针时,请注意指针递增和递减操作的结果取决于指针所指向的数据类型。 上面的例子中,p++后的结果是p=,如果p指向int,即:
int *p = (int *);
p++( 或 ++p) 的结果相当于:p = p+(int),p—( 或 —p) 的结果是 p = p-(int)。
同样,如果执行:
长整型 *p = (长整型 *);
那么p++(或++p)的结果相当于:p = p+(long int),p—(或-p)的结果是p = p-(long int)。
请记住:CPU 地址以字节为单位,C 语言指针根据它们指向的数据类型的长度递增和递减。 理解这一点对于使用指针直接操作内存非常重要。
2. 函数指针
首先,您需要了解以下三个问题:
(1)C语言中的函数名直接对应于该函数生成的指令代码在内存中的地址,因此可以将函数名直接赋值给指向该函数的指针;
(2)调用函数实际上相当于“传送指令+参数传送处理+返回位置到堆栈”。 本质上,核心操作就是将函数生成的目标代码首地址赋值给CPU的PC寄存器;
(3)因为函数调用的本质就是跳转到某个地址单元的代码去执行,所以你可以“调用”一个根本不存在的函数实体,晕? 请阅读以下内容:
请拿出你能找到的任何一本大学教科书《微型计算机原理》。 书上说,186 CPU启动后,跳转到绝对地址(对应C语言的指针,是段地址,是段内的偏移量)。 ,请看下面的代码:
(*) ( );/* 定义无参数、无返回类型 */
/* 函数指针类型 */
= ();/* 定义一个指向的函数指针 */
/* CPU启动后执行的第一条指令的位置*/
();/* 调用函数*/
在上面的程序中,我们根本没有看到任何函数实体,但是我们执行了这样一个函数调用:(),它实际上起到了“软重启”的作用,在CPU启动后跳转到第一行。 要执行的指令的位置。
请记住:函数只有一组指令; 您可以在没有函数体的情况下调用函数,这实际上只是更改地址并开始执行指令!
3. 数组与动态应用程序
嵌入式系统中的动态存储器应用比一般系统编程有更严格的要求。 这是因为嵌入式系统的内存空间往往非常有限,不经意的内存泄漏很快就会导致系统崩溃。
因此,请确保您的和免费的成对出现。 如果你写一个这样的程序:
字符*(空)
字符*p;
p = (char *)(…);
如果(p==NULL)
……;
…/* 对 p 的一系列操作*/
p;
在某处调用(),用完动态分配的内存后将其释放,如下:
字符 *q = ();
……
自由(q);
上面的代码显然是不合理的,因为它违反了与free成对出现的原则,即“谁申请谁就被释放”的原则。 不满足这个原则会导致代码的耦合性增加,因为用户在调用函数时需要知道其内部细节!
正确的做法是在调用处申请内存并传入函数,如下:
字符 *p=(…);
如果(p==NULL)
……;
(p);
……
免费(p);
p=空;
该函数接收参数p,如下:
无效(字符*p)
…/* 对 p 的一系列操作*/
基本上可以用更大的数组来代替动态内存分配方式。 对于编程新手来说,作者建议大家尽量使用数组! 嵌入式系统可以以宽广的胸怀接受缺陷,但不能“接受”错误。 毕竟,以最愚蠢的方式修炼神功的郭靖,比足智多谋却因政治错误而走上反革命道路的杨康要强。
给出原理:
(1)尽可能使用数组,数组不能越界访问(真理越界就是谬论,数组越界光荣地完成了一个混沌的嵌入式系统);
(2)如果使用动态申请,申请后一定要判断是否申请成功,而且要和free成对出现!
4.关键字常量
const 的意思是“只读”。 区分下面代码的功能非常重要,这也是常见的感叹。 如果你不知道其中的区别,并且已经在编程世界摸爬滚打了很多年,那么只能说是一场悲剧:
常量整型;
int 常量 a;
常量 int *a;
int * 常量 a;
int const * 一个常量;
(1)关键字const的作用是向阅读你代码的人传达非常有用的信息。 例如,在函数的形参前添加const关键字,表示该参数在函数体中不会被修改,是“输入参数”。 当有多个形式参数时,函数的调用者可以通过参数前是否有const关键字来清楚地识别哪些是输入参数,哪些是可能的输出参数。
(2)合理使用关键字const可以让编译器自然地保护那些不希望被改变的参数,防止它们被无意的代码修改,这样可以减少bug的发生。
const在C++语言中有着更丰富的含义,但是在C语言中它的意思只是:“一个只能读的普通变量”,可以称之为“不可改变的变量”(这句话看似拗口,其实是C语言中const本质最准确的表达),编译阶段需要的常量仍然只能用#宏来定义! 因此,下面的C语言程序是非法的:
常量 int 大小 = 10;
char a[SIZE];/* 非法:编译时不能使用变量*/
5.关键词
C语言编译器会对用户编写的代码进行优化,比如下面的代码:
整数a、b、c;
a = (0x100);/* 读取I/O空间中端口0x100的内容并存储到变量中*/
b = a;
a = (0x100);/* 再次读取I/O空间0x100端口的内容,存入a变量中*/
c = a;
它可能被编译器优化为:
整数a、b、c;
a = (0x100);/* 读取I/O空间中端口0x100的内容并存储到变量中*/
b = a;
c = a;
然而,这样的优化结果可能会导致错误。 如果在第一次读操作后,I/O空间中端口0x100的内容被另一个程序写入了新的值,那么第二次读操作读出的内容实际上与第一次不同。 ,b和c的值应该不同。 在变量 a 的定义之前添加关键字可以防止编译器进行类似的优化。 正确的做法是:
整数a;
变量可以在以下情况下使用:
(1)并行设备的硬件寄存器(比如状态寄存器,例子中的代码就属于此类);
(2) 将在中断服务程序中访问的非自动变量(即全局变量);
(3) 多线程应用程序中多个任务共享的变量。
6.处理CPU字长和内存位宽不一致的问题
正如背景部分提到的,本文特意选择了字长与CPU字长不一致的存储芯片,只是为了进行本节的讨论,解决CPU字长与内存位宽不一致的问题。 80186的字长为16,NVRAM的位宽为8。此时,我们需要提供一个对NVRAM读写字节和字的接口,如下:
字符字节;
;
/* 功能:读取NVRAM中的字节
* 参数: ,读取位置相对于NVRAM基地址的偏移
* 返回:读取到的字节值
*/
字节(字)
= (字节*)(NVRAM + * 2); /* 为什么偏移量是×2? */
*;
/* 功能:读取NVRAM中文字幕
* 参数: ,读取位置相对于NVRAM基地址的偏移
* 返回:读到的单词
*/
字(字)
字 wTmp = 0;
;
/* 读取高字节 */
= (字节*)(NVRAM + * 2); /* 为什么偏移量是×2? */
wTmp +=(*)*256;
/* 读取低字节 */
= (字节*)(NVRAM + (+1) * 2); /* 为什么偏移量是×2? */
wTmp +=*;
温度;
/* 功能:向NVRAM写入一个字节
* 参数: ,写入位置相对于NVRAM基地址的偏移
*,要写入的字节
*/
无效(字、字节)
……
/* 功能:向NVRAM写入一个字*/
* 参数: ,写入位置相对于NVRAM基地址的偏移
*wData,要写入的字
*/
无效(WORD,WORD wData)
……
子贡问:为什么偏移量要乘2?
子说:请看图1,16位80186和8位NVRAM之间的互连只能连接到地址线A1到A0。 CPU本身的A0不与NVRAM连接。 所以NVRAM的地址只能是偶地址,所以每次都是以2为单位前进!
图1 CPU与NVRAM地址线连接
子贡又问:那么为什么80186的地址线A0没有连接到NVRAM的A0呢?
孔子说:请读一下《论语》中的《微机原理》,里面描述了圣人的计算机构成之道。
总结
本文主要讲述嵌入式系统C语言编程中内存操作的相关技巧。 掌握并深入理解数据指针、函数指针、动态分配内存、const和关键字等相关知识是一个优秀的C语言程序员的基本要求。 当我们牢牢掌握了以上技能后,我们就已经学会了C语言的99%,因为C语言最本质的内涵就体现在内存操作上。
我们在嵌入式系统中之所以使用C语言进行编程,99%的原因是因为它强大的内存操作能力!
如果你热爱编程,请热爱C语言;
如果你热爱C语言,请热爱指针;
如果你喜欢指针,请喜欢指针!