C语言嵌入式系统编程修炼之道——内存操作篇 原创21cnbao2005-10-19 22:06:00评论(0)

简介:


C语言嵌入式系统编程修炼之道——内存操作篇

作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email]

1.数据指针

在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的 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 *)0xF000FF00;
*p=11;
以上程序的意义为在绝对地址 0xF0000+0xFF00(80186 使用 16 位段地址和 16 位偏移地址 ) 写入 11
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中 p++ 后的结果是 p= 0xF000FF01 ,若 p 指向 int ,即:
int *p = (int *)0xF000FF00;
p++( ++p) 的结果等同于: p =  p+sizeof(int) ,而 p—( —p) 结果 p =  p-sizeof(int)
同理,若执行:
long int *p = (long int *)0xF000FF00;
p++( ++p) 的结果等同于: p =  p+sizeof(long int)  ,而 p—( —p) 结果 p =  p-sizeof(long int)
记住: CPU 以字节为单位编址,而 C 语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作内存是相当重要的。

2.函数指针

首先要理解以下三个问题:
1 C 语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
2 )调用函数实际上等同于“调转指令+参数传递处理+回归位置入栈”,本质上最核心的操作是将函数生成的目标代码的首地址赋给 CPU PC 寄存器;
3 )因为函数调用的本质是跳转到某一个地址单元的 code 去执行,所以可以“调用”一个根本就不存在的函数实体,晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到, 186 CPU 启动后跳转至绝对地址 0xFFFF0 (对应 C 语言指针是 0xF000FFF0 0xF000 为段地址, 0xFFF0 为段内偏移)执行,请看下面的代码:
typedef  void  (*lpFunction) ( );    /*  定义一个无参数、无返回类型的  */
/*  函数指针类型  */
lpFunction lpReset =   (lpFunction)0xF000FFF0;    /*  定义一个函数指针,指向 */
/* CPU 启动后所执行第一条指令的位置  */
lpReset();                           /*  调用函数  */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用: lpReset() ,它实际上起到了“软重启”的作用,跳转到 CPU 启动后第一条要执行的指令的位置。
记住: 函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!

3.数组vs.动态申请

在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的 malloc free 成对出现,如果你写出这样的一段程序:
char * function(void)
{
  char *p;
  p = (char *)malloc(…);
  if(p==NULL)
…;
                           /*  一系列针对 p 的操作  */
return p;
}
在某处调用 function() ,用完 function 中动态申请的内存后将其 free ,如下:
char *q = function();
free(q);
上述代码明显是不合理的,因为违反了 malloc free 成对出现的原则,即“谁申请,就由谁释放”原则。不满足这个原则,会导致代码的耦合度增大,因为用户在调用 function 函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入 function 函数,如下:
char *p=malloc(…);
if(p==NULL)
…;
function(p);
free(p);
p=NULL;
而函数 function 则接收参数 p ,如下:
void function(char *p)
{
                      /*  一系列针对 p 的操作  */
}
基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推荐你尽量采用数组!嵌入式系统可以以博大的胸襟接收瑕疵,而无法“海纳”错误。毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的杨康。
给出原则:
1 )尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数组越过界限就光荣地成全了一个混乱的嵌入式系统);
2 )如果使用动态申请,则申请后一定要判断是否申请成功了,并且 malloc free 应成对出现!

4.关键字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 a[SIZE];  /*  非法:编译阶段不能用到变量  */

5.关键字volatile

C 语言编译器会对用户书写的代码进行优化,譬如如下代码:
int a,b,c;
a = inWord(0x100);  /* 读取 I/O 空间 0x100 端口的内容存入 a 变量 */
b = a;
a = inWord (0x100);  /* 再次 读取 I/O 空间 0x100 端口的内容存入 a 变量 */
c = a;
很可能被编译器优化为:
int a,b,c;
a = inWord(0x100);  /* 读取 I/O 空间 0x100 端口的内容存入 a 变量 */
b = a;
c = a;
但是这样的优化结果可能导致错误,如果 I/O 空间 0x100 端口的内容在执行第一次读操作后被其它程序写入新值,则其实第 2 次读操作读出的内容与第一次不同, b c 的值应该不同。在变量 a 的定义前加上 volatile 关键字可以防止编译器的类似优化,正确的做法是:
volatile int a
volatile 变量可能用于如下几种情况:
(1)  并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);
(2)  一个中断服务子程序中会访问到的非自动变量 ( 也就是全局变量 )
(3)  多线程应用中被几个任务共享的变量。

6.CPU字长与存储器位宽不一致处理

在背景篇中提到,本文特意选择了一个与 CPU 字长不一致的存储芯片,就是为了进行本节的讨论,解决 CPU 字长与存储器位宽不一致的情况。 80186 的字长为 16 ,而 NVRAM 的位宽为 8 ,在这种情况下,我们需要为 NVRAM 提供读写字节、字的接口,如下:
typedef unsigned char BYTE;
typedef unsigned int  WORD;
 
 
/*  函数功能:读 NVRAM 中字节
  参数: wOffset ,读取位置相对 NVRAM 基地址的偏移
  返回:读取到的字节值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
  LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2);  /*  为什么偏移要×2? */
                               
  return  *lpAddr;
}
 
 
/*  函数功能:读 NVRAM 中字
  参数: wOffset ,读取位置相对 NVRAM 基地址的偏移
  返回:读取到的字
*/
extern WORD ReadWordNVRAM(WORD 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 WriteByteNVRAM(WORD wOffset, BYTE byData)
{
  
}
 
 
/*  函数功能:向 NVRAM 中写一个字  */
* 参数: wOffset ,写入位置相对 NVRAM 基地址的偏移
*      wData ,欲写入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
  
}
子贡问曰:Why偏移要乘以2?
子曰:请看图116801868NVRAM之间互连只能以地址线A1对其A0,CPU本身的A0NVRAM不连接。因此,NVRAM的地址只能是偶数地址,故每次以2为单位前进!
1 CPUNVRAM地址线连接
子贡再问:So why 80186的地址线A0不与NVRAMA0连接?
子曰:请看《IT论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。

总结

本篇主要讲述了嵌入式系统C编程中内存操作的相关技巧。掌握并深入理解关于数据指针、函数指针、动态申请内存、const volatile 关键字 等的相关知识,是一个优秀的C语言程序设计师的基本要求。当我们已经牢固掌握了上述技巧后,我们就已经学会了C语言的99%,因为C语言最精华的内涵皆在内存操作中体现。
我们之所以在嵌入式系统中使用C语言进行程序设计,99%是因为其强大的内存操作能力!
如果你爱编程,请你爱C语言;
如果你爱C语言,请你爱指针;

如果你爱指针,请你爱指针的指针!


 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120789,如需转载请自行联系原作者



相关文章
|
3月前
|
IDE 编译器 开发工具
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
在本文中,我们系统地讲解了常见的 `#pragma` 指令,包括其基本用法、编译器支持情况、示例代码以及与传统方法的对比。`#pragma` 指令是一个强大的工具,可以帮助开发者精细控制编译器的行为,优化代码性能,避免错误,并确保跨平台兼容性。然而,使用这些指令时需要特别注意编译器的支持情况,因为并非所有的 `#pragma` 指令都能在所有编译器中得到支持。
298 41
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
|
1月前
|
监控 关系型数据库 MySQL
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
【01】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-硬件设备实时监控系统运营版发布-本产品基于企业级开源项目Zabbix深度二开-分步骤实现预计10篇合集-自营版
37 0
|
3月前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
88 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
3月前
|
存储 编译器 C语言
【C语言】C语言的变量和声明系统性讲解
在C语言中,声明和定义是两个关键概念,分别用于告知编译器变量或函数的存在(声明)和实际创建及分配内存(定义)。声明可以多次出现,而定义只能有一次。声明通常位于头文件中,定义则在源文件中。通过合理组织头文件和源文件,可以提高代码的模块化和可维护性。示例包括全局变量、局部变量、函数、结构体、联合体、数组、字符串、枚举和指针的声明与定义。
104 12
|
3月前
|
机器学习/深度学习 人工智能 缓存
【AI系统】推理内存布局
本文介绍了CPU和GPU的基础内存知识,NCHWX内存排布格式,以及MNN推理引擎如何通过数据内存重新排布进行内核优化,特别是针对WinoGrad卷积计算的优化方法,通过NC4HW4数据格式重排,有效利用了SIMD指令集特性,减少了cache miss,提高了计算效率。
118 3
|
3月前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
3月前
|
存储 编译器 C语言
【C语言】数据类型全解析:编程效率提升的秘诀
在C语言中,合理选择和使用数据类型是编程的关键。通过深入理解基本数据类型和派生数据类型,掌握类型限定符和扩展技巧,可以编写出高效、稳定、可维护的代码。无论是在普通应用还是嵌入式系统中,数据类型的合理使用都能显著提升程序的性能和可靠性。
103 8
|
3月前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
138 6
|
3月前
|
机器学习/深度学习 人工智能 算法
【AI系统】内存分配算法
本文探讨了AI编译器前端优化中的内存分配问题,涵盖模型与硬件内存的发展、内存划分及其优化算法。文章首先分析了神经网络模型对NPU内存需求的增长趋势,随后详细介绍了静态与动态内存的概念及其实现方式,最后重点讨论了几种节省内存的算法,如空间换内存、计算换内存、模型压缩和内存复用等,旨在提高内存使用效率,减少碎片化,提升模型训练和推理的性能。
165 1
|
4月前
|
C语言 开发者
C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧
本文深入探讨了C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧,并通过案例分析展示了其应用,展望了未来的发展趋势,旨在帮助读者提升程序质量和开发效率。
133 5