[譯]映射設備寄存器到內存

分類:

本文譯自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的頭文件<stdint.h>中)
 
筆者喜歡使用能表達類型的意義而不是物理長度的象徵類型(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 [email protected].
 
譯註:這篇文章介紹了端口映射I/O和內存映射I/O的概念及區別,以及如何訪問位於特定地址上的寄存器。好像與UNIX系統上的mmap()無關。因此該文章更適合於寫驅動的朋友參考。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章