Chinaunix首页 | 论坛 | 博客
  • 博客访问: 100296
  • 博文数量: 54
  • 博客积分: 1920
  • 博客等级: 上尉
  • 技术积分: 480
  • 用 户 组: 普通用户
  • 注册时间: 2009-05-08 13:46
文章分类

全部博文(54)

文章存档

2011年(1)

2010年(8)

2009年(45)

我的朋友

分类: LINUX

2009-05-12 12:36:17

Mapping memory
By
(08/11/04, 11:00:00 H EDT)
Memory-mapped I/O is something you can do reasonably well in standard C and C++.

Device drivers communicate with peripheral devices through device registers. A driver sends commands or data to a device by storing into its device register, or retrieves status or data from a device by reading from its device register.

Many processors use memory-mapped I/O, which maps device registers to fixed addresses in the conventional memory space. To a C or C++ programmer, a memory-mapped device register looks very much like an ordinary data object. Programs can use ordinary assignment operators to move values to or from memory-mapped device registers.

Some processors use port-mapped I/O, which maps device registers to locations in a separate address space, typically smaller than the conventional memory space. On these processors, programs must use special machine instructions, such as the in and out instructions of the Intel x86 processors, to move data to or from device registers. To a C programmer, port-mapped device registers don't look quite like ordinary data.

The C and C++ standards are silent about port-mapped I/O. Programs that perform port-mapped I/O must use some nonstandard, platform-specific language or library extensions, or worse, assembly code. On the other hand, memory-mapped I/O is something you can do reasonably well within the standard language dialects.

This month, I'll look at different approaches you can use to refer to memory-mapped device registers.

Device register types
Some device registers might occupy just a byte; others may occupy a word or more. In C or C++, the simplest representation for a single device register is as an object of an appropriately sized and signed integer type. For example, you might declare a one-byte register as a char or a two-byte register as an unsigned short.

For example, the ARM Evaluator-7T is a single-board computer with a small assortment of memory-mapped peripheral devices. The board's documentation refers to the device registers as special registers. The special registers span 64KB starting at address 0x03FF0000. The memory is byte-addressable, but each register is a four-byte word aligned to an address that's a multiple of four. You could manipulate each special register as if it were an int or unsigned int. Some programmers prefer to use a type that specifies the physical size of the register more overtly, such as int32_t or uint32_t. (Types such as int32_t and uint32_t are defined in the C99 header .)

I prefer to use a symbolic type whose name conveys the meaning of the type rather than its physical extent, such as:

typedef unsigned int special_register;

Special registers are actually volatile entities — they may change state in ways that the compiler can't detect. Therefore, the typedef should be an alias for a volatile-qualified type, as in:

typedef unsigned int volatile special_register;

Many devices interact through a small collection of device registers, rather than just one. For example, the Evaluator-7T uses five special registers to control the two integrated timers:

  • TMOD: timer mode register
  • TDATA0: timer 0 data register
  • TDATA1: timer 1 data register
  • TCNT0: timer 0 count register
  • TCNT1: timer 1 count register

You can represent the timer registers as a struct defined as:

typedef struct dual_timers dual_timers;
struct dual_timers
   {
    special_register TMOD;
    special_register TDATA0;
    special_register TDATA1;
    special_register TCNT0;
    special_register TCNT1;
    };

The typedef before the struct definition elevates the name dual_timers from a mere tag to a full-fledged type name. I'd rather spell TCNT0 as count0, but TCNT0 is the name used throughout the product documentation, so it's probably best not to change it.

In C++, I'd define this struct as a class with appropriate member functions. Whether dual_timers is a C struct or a C++ class doesn't affect the following discussion.

Positioning device registers
Some compilers provide language extensions that will let you position an object at a specified memory address. For example, using the TASKING C166/ST10 C Cross-Compiler's _at attribute you can write a global declaration such as:

unsigned short count _at(0xFF08);

to declare count as a memory-mapped device register residing at address 0xFF08. Other compilers offer #pragma directives to do something similar. However, the _at attribute and #pragma directives are nonstandard. Each compiler with such extensions is likely to support something different.

Standard C and C++ don't let you declare a variable so that it resides at a specified address. The common idiom for accessing a device register is to use a pointer whose value contains the register's address. For example, the timer registers on the Evaluator-7T reside at address 0x03FF6000. A program can access these registers via a pointer that points to that address. You can define that pointer as a macro, as in:

#define timers ((dual_timers *)0x03FF6000)

or as a constant pointer, as in:

dual_timers *const timers
    = (dual_timers *)0x03FF6000;

Either way you define timers, you can use it to reach the timer registers. For example, the TMOD register contains bits that you can set to enable a timer and clear to disable a timer. You can define the masks for those bits as enumeration constants:

enum { TE0 = 0x01, TE1 = 0x08 };

Then you can disable both timers using:

timers->TMOD &= ~(TE0 | TE1);

Weighing the alternatives
These two pointer definitions—the macro and the constant object—are largely interchangeable. However, they produce slightly different behavior and, on some platforms, generate slightly different machine code.

As I explained in an earlier column, the macro preprocessor is a distinct compilation phase. The preprocessor does macro substitution before the compiler does any other symbol processing. For example, given the macro definition for timers, the preprocessor transforms:

timers->TMOD &= ~(TE0 | TE1);

into:

((dual_timers *)0x03FF6000)->TMOD
    &= ~(TE0 | TE1);

Later compilation phases never see the macro symbol timers; they see only the source text after macro substitution. Many compilers don't pass macro names on to their debuggers, in which case macro names are invisible to the debugger.

Macros have an even more serious problem: macro names don't observe the scope rules that apply to other names. For example, you can't restrict a macro to a local scope. Defining a macro within a function, as in:

void timer_handler()
    {
    #define timers ((dual_timers *)0x03FF6000)
     ...
    }

doesn't make the macro local to the function. The macro is still effectively global. Similarly, you can't declare a macro as a member of a C++ class or namespace.

Actually, macro names are worse than global names. Names declared in inner scopes can temporarily hide names from outer scopes, but they can't hide macro names. Consequently, macros might substitute in places where you don't expect them to.

Declaring timers as a constant pointer avoids both of these problems. The name should be visible in your debugger, and if you declare it in a nonglobal scope, it should stay there.

On the other hand, with some compilers on some platforms, declaring timers as a constant pointer might—I emphasize might—produce slightly slower and larger code. The compiler might produce different code if you define the pointer globally or locally. It might produce different code if you compile the definition in C as opposed to C++. I'll explain what the differences are and why they occur in my next column.
本文译自Dan Saks的专栏文章。

内存映射I/O是能用标准C/C++做得相当好的一个东东。

设备驱动通过设备寄存器来与外围设备通信。驱动通过向设备寄存器中写入命令或者数据,或者通过读取设备寄存器来读取设备状态或者数据。

许多处理器使用内存映射I/O,就是把设备的寄存器映射到常规内存空间的固定地址。对于一个C/C++程序员来说,内存映射设备的寄存器看起来非常像一个普通的数据对象。程序可以使用普通的赋值操作符来向内存映射设备的寄存器读取或写入值。

有些处理器使用端口映射I/O,就是把设备的寄存器映射到一个单独的地址空间,这个地址空间一般来说要比常规内存小。在这些处理器上,程序必须使用特殊的机器指令来向内存映射设备的寄存器读取或写入值,比如在intel x86上使用in和out指令。对于一个C/C++程序员来说,端口映射的设备寄存器就不怎么像普通数据对象了。

C/C++的标准没有阐述端口映射I/O。执行端口映射I/O的程序必须使用不标准的,与特定平台相关的语言或者库扩展,或者更糟糕的是用汇编代码。另一方面,内存映射I/O是能用标准C/C++做得相当好的一个技术。

这篇文章将讨论不同的方法来访问内存映射设备的寄存器

设备寄存器类型
有些设备寄存器可能只有一个字节,其他的可能有一个字或者更多。在C/C++中,对于一个设备寄存器最简单的表示就是一个大小合适的,有符号的整型对象。例如,你可能将一个单字节的寄存器声明为char或者将一个双字节的寄存器声明为unsigned short。
例如,单板机ARM Evaluator-7T有一个小类映射内存外设。该板子的文档将设备寄存器引用为特殊寄存器。这些特殊的寄存器占有64k,从地址0x03FF0000开始。这段内存是基于字节寻址的,但是每个寄存器都是四字节的字,并且按照4字节对齐。可以把每个特殊寄存器当作一个int或unsigned int来操作。有些程序员更喜欢使用指定了物理类型大小的类型,如int32_t或uint32_t。(类似int32_t和uint32_t的类型定义在C99的头文件中)
 
笔者喜欢使用能表达类型的意义而不是物理长度的象征类型(symbolic type ),如:

typedef unsigned int special_register;

特殊寄存器实际上是volatile实体——这种实体可以用编译器无法检测到的方式改变自己的状态。因此typedef应该是一个volatile修饰的类型的别名:

typedef unsigned int volatile special_register;

许多设备通过一个小的设备寄存器集合来交互,而不是仅仅用一个。例如,Evaluator-7T板使用5个特殊寄存器来控制两个集成定时器:
    * TMOD:时间模式寄存器
    * TDATA0:定时器0数据寄存器
    * TDATA1: 定时器1数据寄存器
    * TCNT0: 定时器0计数寄存器
    * TCNT1: 定时器1计数寄存器
可以用一个如下定义的结构体来表示这些定时器寄存器

typedef struct dual_timers dual_timers;
struct dual_timers
{
    special_register TMOD;
    special_register TDATA0;
    special_register TDATA1;
    special_register TCNT0;
    special_register TCNT1;
};//译注:注意此处顺序,每个成员大小及对齐,因为后续的访问依赖于此。

在struct定义前的typedef使得dual_timers由一个不完整的类型变成了一个完整的类型。笔者更愿意使用count0来标示TCNT0,但是TCNT0是整个产品文档所使用的名字,因此最好不要改变它。

在C++中,笔者将该struct定义为一个有恰当的成员函数的class。无论dual_timers是C的struct还是C++的class不影响下面的讨论。

安置设备寄存器
一些编译器提供了语言的扩展,这些扩展使你可以把一个对象放在在一个特定的内存地址。如:使用TASKING C166/ST10 C交叉编译器的_at属性就可以写出下面的全局声明:

unsigned short count _at(0xFF08);

这用于把count声明为放置在0xFF08上的内存映射设备寄存器。其他的编译器提供#pragma指示来做相同的事。然而,_at属性和#pragma指示不是标准的。每个有类似扩展的编译器更可能支持一些不同的东西。 标准C/C++不会让你声明一个放置在特定地址上的变量。访问设备寄存器的一个通用习惯是用指针,该指针的值就是寄存器的地址。如:Evaluator-7T板上的定时器寄存器放置在地址0x03FF6000。程序可以通过指向该地址的指针访问这些寄存器。可以定义这样的指针为一个宏:

#define timers ((dual_timers *)0x03FF6000)

或者定义为一个const指针:
dual_timers *const timers = (dual_timers *)0x03FF6000;
//译注:此处可以参考我的文章《指针是通往地狱的捷径》

用其中任一种方式来定义定时器,就能够使用它来访问定时器寄存器。如:TMOD寄存器含有允许激活和禁用定时器的位,可以设置或者清除该位来达到目的。因此可以用枚举为这些位定义一些掩码:

enum { TE0 = 0x01, TE1 = 0x08 };

同时禁用这两个定时器
timers->TMOD &= ~(TE0 | TE1);

比较两者
这两个关于指针的定义——宏和const对象——很大程度上是可以互换的。然而,它们在行为上有少许不同,而且在某些平台上会产生少许不同的机器码。
 
笔者在先前的一个专栏里解释过,预处理器是一个截然不同的编译阶段。预处理器在编译器做其它符号处理之前执行宏替换。例如,给定timers的宏定义,预处理器把
timers->TMOD &= ~(TE0 | TE1);
翻译成:
((dual_timers *)0x03FF6000)->TMOD &= ~(TE0 | TE1);

其后的编译阶段永远都看不到timers的符号宏(dual_timers),他们仅仅看到替换后的文本。许多编译器不会将宏名传递给他们的调试器,这样宏名在调试器中是不可见的。

使用宏还会带来一个更严重的问题:宏名不遵守作用域规则。例如,不能将一个宏限制在一个局部的作用域。在函数中定义宏:
void timer_handler()
    {
    #define timers ((dual_timers *)0x03FF6000)
     ...
    }
不能使该宏成为局部宏。该宏的作用于是全局的。同样,不能把一个宏定义为C++类或者名字空间的一个成员。
 
实际上,宏名比全局名字还要糟糕。内层域的名字会暂时覆盖掉外层的名字,但是不能覆盖掉宏名。从而导致在你不希望发生替换的时候,宏替换发生了。

把timers声明为一个const指针能避免上述问题。该名字在调试器中可见,不会有作用域的问题。另一方面,使用某些平台的某些编译器,这样的声明可能——笔者强调“可能”——会使代码变得稍微慢点,程序大小稍微大点。将该指针定义为全局的或者局部的可能会导致编译器生成不同的代码。编译在C中而不是C++中的定义可能会生成不同的代码。笔者将在下一篇专栏文章中解释为什么。

Dan Saks Saks is president of Saks & Associates, a C/C++ training and consulting
company. You can write to him at .
 
译注:这篇文章介绍了端口映射I/O和内存映射I/O的概念及区别,以及如何访问位于特定地址上的寄存器。好像与UNIX系统上的mmap()无关。因此该文章更适合于写驱动的朋友参考。
阅读(978) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~