分類:
設備驅動通過設備寄存器來與外圍設備通信。驅動通過向設備寄存器中寫入命令或者數據,或者通過讀取設備寄存器來讀取設備狀態或者數據。
許多處理器使用內存映射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。
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不影響下面的討論。
安置設備寄存器
unsigned short count _at(0xFF08);
這用於把count聲明爲放置在0xFF08上的內存映射設備寄存器。其他的編譯器提供#pragma指示來做相同的事。然而,_at屬性和#pragma指示不是標準的。每個有類似擴展的編譯器更可能支持一些不同的東西。 標準C/C++不會讓你聲明一個放置在特定地址上的變量。訪問設備寄存器的一個通用習慣是用指針,該指針的值就是寄存器的地址。如:Evaluator-7T板上的定時器寄存器放置在地址0x03FF6000。程序可以通過指向該地址的指針訪問這些寄存器。可以定義這樣的指針爲一個宏:
#define timers ((dual_timers *)0x03FF6000)
或者定義爲一個const指針:
用其中任一種方式來定義定時器,就能夠使用它來訪問定時器寄存器。如:TMOD寄存器含有允許激活和禁用定時器的位,可以設置或者清除該位來達到目的。因此可以用枚舉爲這些位定義一些掩碼:
enum { TE0 = 0x01, TE1 = 0x08 };
同時禁用這兩個定時器
timers->TMOD &= ~(TE0 | TE1);
比較兩者
這兩個關於指針的定義——宏和const對象——很大程度上是可以互換的。然而,它們在行爲上有少許不同,而且在某些平臺上會產生少許不同的機器碼。
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