close
Introduction to the Volatile Keyword
認識關鍵字Volatile
The use of volatile is poorly understood by many programmers. This is notsurprising, as most C texts dismiss it in a sentence or two.
很多程式設計師對於volatile的用法都不是很熟悉。這並不奇怪,很多介紹C語言的書籍對於他的用法都閃爍其辭。
Have you experienced any of the following in your C/C++ embedded code?
‧Code that works fine-until you turn optimization on
‧Code that works fine-as long as interrupts are disabled
‧Flaky hardware drivers
‧Tasks that work fine in isolation- yet crash when another task is enabled
在你們使用C/C++語言開發嵌入式系統的時候,遇到過以下的情況麼?
‧一打開編譯器的編譯最佳化選項,程式碼就不再正常工作了;
‧中斷似乎總是程序異常的元兇;
‧硬件驅動工作不穩定;
‧多任務系統中,單個任務工作正常,加入任何其他任務以後,系統就崩潰了。
If you answered yes to any of the above, it's likely that you didn't use the C keyword volatile.
You aren't alone. The use of volatile is poorly understood by many programmers. This is not surprising,
as most C texts dismiss it in a sentence or two.
如果你曾經向別人請教過和以上類似的問題,至少說明,你還沒有接觸過C語言關鍵字volatile的用法。
這種情況,你不是第一個遇到。很多程式設計師對於volatile都幾乎一無所知。
大部分介紹C語言的文獻對於它都閃爍其辭。
volatile is a qualifier that is applied to a variable when it is declared.
It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby.
The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.
Volatile是一個變數聲明限定詞。
它告訴編譯器,它所修飾的變數的值可能會在任何時刻被意外的更新,即便與該變數相關的上下文沒有任何對其進行修改的語句。
造成這種“意外更新”的原因相當複雜。在我們分析這些原因之前,我們先回顧一下與其相關的語法。
Syntax
語法
To declare a variable volatile, include the keyword volatile before or after the datatype in the variable definition.
For instance both of these declarations will declare foo to be a volatile integer:
要想給一個變數加上volatile限定,只需要在變數類型聲明附之前/後加入一個volatile關鍵字就可以了。
下面的兩個實例是等效的,它們都是將foo聲明為一個“需要被實時更新”的int型變數。
volatile int foo;
int volatile foo;
Now, it turns out that pointers to volatile variables are very common.
Both of thesedeclarations declare foo to be a pointer to a volatile integer:
同樣,聲明一個指向volatile型變數的指標也是非常類似的。
下面的兩個聲明都是將foo定義為一個指向volatile integer型變數的指標。
volatile int * foo;
int volatile * foo;
Volatile pointers to non-volatile variables are very rare (I think I've used them once),
but I'd better go ahead and give you the syntax:
一個Volatile型的指標指向一個非volatile型變數的情況非常少見(我想,我可能使用過一次),
儘管如此,我還是要給出他的語法:
int * volatile foo;
And just for completeness, if you really must have a volatile pointer to a volatilevariable, then:
最後一種形式,針對你真的需要一個volatile型的指標指向一個volatile型的情形:
int volatile * volatile foo;
Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo),
consult Dan Sak's column, "Top-Level cv-Qualifiers in Function Parameters" (February 2000, p. 63).
順便說一下,如果你想知道關於“我們需要在什麼時候在什麼地方使用volatile”和“為什麼我們需要volatile放在變數類型後面(例如,int volatile * foo)”
這類問題的詳細內容,請參考Dan Sak`s的專題,“Top-Level cv-Qualifiers in Function Parameters”。
Finally, if you apply volatile to a struct or union, the entire contents of the struct/union are volatile.
If you don't want this behavior, you can apply the volatile qualifier to the individual members of the struct/union.
最後,如果你將volatile應用在結構或者是聯集(union)上,那麼該結構/聯集(union)內的所有內容就都帶有volatile屬性了。
如果你並不想這樣(牽一發而動全身),你可以僅僅在結構體/聯集中的某一個成員上單獨使用該限定。
Use
使用
A variable should be declared volatile whenever its value could change unexpectedly.In practice, only three types of variables could change:
‧Memory-mapped peripheral registers
‧Global variables modified by an interrupt service routine
‧Global variables within a multi-threaded application
當一個變數的內容可能會被意想不到的更新時,一定要使用volatile來聲明該變數。通常,只有三種類型的變數會發生這種“意外”:
‧在內存中進行地址映射的設備暫存器;
‧在中斷處理程序中可能被修改的全域變數;
‧多排程應用程序中的全域變數;
Peripheral registers
設備暫存器.
Embedded systems contain real hardware, usually with sophisticated peripherals.
These peripherals contain registers whose values may change asynchronously to the program flow.
As a very simple example, consider an 8-bit status register at address 0x1234.
It is required that you poll the status register until it becomes non-zero. The nave and incorrect implementation is as follows:
嵌入式系統的硬件實體中,通常包含一些複雜的外圍設備。
這些設備中包含的暫存器,其值往往隨著程序的流程同步的進行改變。
在一個非常簡單的例子中,假設我們有一個8位的狀態暫存器映射在地址0x1234上。
系統需要我們一直監測狀態暫存器的值,直到它的值不為0為止。通常錯誤的實現方法是:
UINT1 * ptr = (UINT1 *) 0x1234; // Wait for register to become non-zero.
while (*ptr == 0); // Do something else.
This will almost certainly fail as soon as you turn the optimizer on, since the compilerwill generate assembly language that looks something like this:
一旦你打開了最佳化選項,這種寫法肯定會失敗,編譯器就會生成類似如下的彙編程式碼:
mov ptr, #0x1234
mov a, @ptr
loop
bz loop
The rationale of the optimizer is quite simple:
having already read the variable's value into the accumulator (on the second line), there is no need to reread it, sincethe value will always be the same.
Thus, in the third line, we end up with an infiniteloop.
To force the compiler to do what we want, we modify the declaration to:
最佳化的工作原理非常簡單:
一旦我們我們將一個變數讀入暫存器中(參照程式碼的第二行),如果(從變數相關的上下文看)變數的值總是不變的,那麼就沒有必要(從內存中)從新讀取他。
在程式碼的第三行中,我們使用一個無限循環來結束。
為了強迫編譯器按照我們的意願進行編譯,我們修改指標的聲明為:
UINT1 volatile * ptr = (UINT1 volatile *) 0x1234; //The assembly language now looks like this:對應的彙編程式碼為:
mov ptr, #0x1234
loop
mov a, @ptr
bz loop
The desired behavior is achieved.
我們需要的功能實現了!
Subtler problems tend to arise with registers that have special properties.
For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.
對於一些較為特殊的暫存器,(我們上面提到的方法)會導致一些難以想像的錯誤。
事實上,很多設備暫存器在讀取一次以後就會被清除。這種情況下,多餘的讀取操作會導致意想不到的錯誤。
Interrupt service routines
中斷處理程序
Interrupt service routines often set variables that are tested in main line code.
Forexample, a serial port interrupt may test each received character to see if it is an ETX character (presumably signifying the end of a message).
If the character is anETX, the ISR might set a global flag. An incorrect implementation of this might be:
中斷處理程序經常負責更新一些在主程序中被查詢的變數的值。
例如,一個串行通訊中斷會檢測接收到的每一個字節是否為ETX信號(以便來確認一個消息幀的結束標誌)。
如果其中的一個字節為ETX,中斷處理程序就是修改一個全域標誌。一個錯誤的實現方法可能為:
int etx_rcvd = FALSE;
void main()
{
...
while (!ext_rcvd)
{ // Wait }
...
}
interrupt void rx_isr(void)
{
...
if (ETX == rx_char) {
etx_rcvd = TRUE;
}
...
}
With optimization turned off, this code might work . However, any half decentoptimizer will "break" the code.
The problem is that the compiler has no idea that etx_rcvd can be changed within an ISR.
As far as the compiler is concerned, the expression !ext_rcvd is always true, and,
therefore , you can never exit the while loop. Consequently, all the code after the while loop may simply be removed by theoptimizer.
If you are lucky, your compiler will warn you about this.
If you are unlucky (or you haven't yet learned to take compiler warnings seriously), your code will fail miserably.
Naturally, the blame will be placed on a "lousy optimizer."
在編譯最佳化選項關閉的時候,程式碼可能會工作的很好。但是,即便是任何半吊子的最佳化,也會“破壞”這個程式碼的意圖。
問題就在於,編譯器並不知道etx_rcvd會在中斷處理程序中被更新。
在編譯器可以檢測的上下文內,表達式!ext_rcvd總是為真,所以,你就永遠無法從循環中跳出。
因此,該循環後面的程式碼會被當作“不可達到”的內容而被編譯器的最佳化選項簡單的刪除掉。
如果你比較幸運,你的編譯器也許會給你一個相關的警告;
如果你沒有那麼幸運(或者你沒有註意到這些警告),你的程式碼就會導致嚴重的錯誤。
通常,就會有人抱怨“該死的最佳化選項”。
The solution is to declare the variable etx_rcvd to be volatile. Then all of yourproblems (well, some of them anyway) will disappear.
解決這個問題的方法很簡單:將變數etx_rcvd聲明為volatile。然後,所有的(至少是一部分症狀)那些錯誤症狀就會消失。
Multi-threaded applications
多排程應用程序
Despite the presence of queues, pipes, and other scheduler-aware communications mechanisms in real-time operating systems, it is still fairly common for two tasks toexchange information via a shared memory location (that is, a global). When you add a pre-emptive scheduler to your code, your compiler still has no idea what a context switch is or when one might occur. Thus, another task modifying a shared
global is conceptually identical to the problem of interrupt service routines discussed previously. So all shared global variables should be declared volatile. Forexample:
在實時操作系統中,除去序列、管道以及其他調度相關的通訊結構,在兩個任務之間採用共享的內存空間(就是全域共享)實現數據的交換仍然是相當常見的方法。
當你將一個優先權調度器應用於你的程式碼時,編譯器仍然不知道某一程序段分支選擇的實際工作方式以及什麼時候某一分支情況會發生。
這是因為,另外一個任務修改一個共享的全域變數在概念上通常和前面中斷處理程序中提到的情形是一樣的。
所以,(這種情況下)所有共享的全域變數都要被聲明為volatile。
例如:
int cntr;
void task1(void)
{
cntr = 0;
while (cntr == 0) {
sleep(1);
}
...
}
void task2(void)
{
...
cntr++;
sleep(10);
. ..
}
This code will likely fail once the compiler's optimizer is enabled.
Declaring cntr to be volatile is the proper way to solve the problem.
一旦編譯器的最佳化選項被打開,這段程式碼的執行通常會失敗。
將cntr聲明為volatile是解決問題的好辦法。
Final thoughts
反思
Some compilers allow you to implicitly declare all variables as volatile.
Resist this temptation, since it is essentially a substitute for thought. It also leads to potentiallyless efficient code.
一些編譯器允許我們隱含的聲明所有的變數為volatile 。
最好抵制這種便利的誘惑,因為它很容易讓我們“不動腦子”,而且,這也常常會產生一個效率相對較低的程式碼。
Also, resist the temptation to blame the optimizer or turn it off.
Modern optimizersare so good that I cannot remember the last time I came across an optimization bug.
In contrast, I come across failures to use volatile with depressing frequency.
所以,我們又詛咒編譯最佳化或者簡單的關掉這一選項來抵制這些誘惑。
現在的編譯最佳化已經相當聰明,我不記得在編譯最佳化中找到過什麼錯誤。
與之相比,為了解決一些錯誤,我卻常常使用瘋狂數量的volatile。
If you are given a piece of flaky code to "fix," perform a grep for volatile.
If grepcomes up empty, the examples given here are probably good places to start lookingfor problems.
如果你恰巧有一段程式碼需要去修正,先搜索一下有沒有volatile關鍵字。
如果找不到volatile,那麼這個程式碼很可能會是一個很好的實例來檢測前面提到過的各種錯誤。
Nigel Jones is a consultant living in Maryland. When not underwater, he can be found slaving away on a diverse range of embedded projects. He can be reached atNAJones@compuserve.com.
Nigel Jones在馬里蘭從事顧問工作。除了為各類嵌入式項目開發充當顧問,他平時的一大愛好就是潛水。你可以通過發送郵件到NAJones@compuserve.com與其取得聯繫。
(本文為學習交流之用,不可以用作商業用途,版權為原作者所有,翻譯傻孩子2007年2月。)
認識關鍵字Volatile
The use of volatile is poorly understood by many programmers. This is notsurprising, as most C texts dismiss it in a sentence or two.
很多程式設計師對於volatile的用法都不是很熟悉。這並不奇怪,很多介紹C語言的書籍對於他的用法都閃爍其辭。
Have you experienced any of the following in your C/C++ embedded code?
‧Code that works fine-until you turn optimization on
‧Code that works fine-as long as interrupts are disabled
‧Flaky hardware drivers
‧Tasks that work fine in isolation- yet crash when another task is enabled
在你們使用C/C++語言開發嵌入式系統的時候,遇到過以下的情況麼?
‧一打開編譯器的編譯最佳化選項,程式碼就不再正常工作了;
‧中斷似乎總是程序異常的元兇;
‧硬件驅動工作不穩定;
‧多任務系統中,單個任務工作正常,加入任何其他任務以後,系統就崩潰了。
If you answered yes to any of the above, it's likely that you didn't use the C keyword volatile.
You aren't alone. The use of volatile is poorly understood by many programmers. This is not surprising,
as most C texts dismiss it in a sentence or two.
如果你曾經向別人請教過和以上類似的問題,至少說明,你還沒有接觸過C語言關鍵字volatile的用法。
這種情況,你不是第一個遇到。很多程式設計師對於volatile都幾乎一無所知。
大部分介紹C語言的文獻對於它都閃爍其辭。
volatile is a qualifier that is applied to a variable when it is declared.
It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby.
The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.
Volatile是一個變數聲明限定詞。
它告訴編譯器,它所修飾的變數的值可能會在任何時刻被意外的更新,即便與該變數相關的上下文沒有任何對其進行修改的語句。
造成這種“意外更新”的原因相當複雜。在我們分析這些原因之前,我們先回顧一下與其相關的語法。
Syntax
語法
To declare a variable volatile, include the keyword volatile before or after the datatype in the variable definition.
For instance both of these declarations will declare foo to be a volatile integer:
要想給一個變數加上volatile限定,只需要在變數類型聲明附之前/後加入一個volatile關鍵字就可以了。
下面的兩個實例是等效的,它們都是將foo聲明為一個“需要被實時更新”的int型變數。
volatile int foo;
int volatile foo;
Now, it turns out that pointers to volatile variables are very common.
Both of thesedeclarations declare foo to be a pointer to a volatile integer:
同樣,聲明一個指向volatile型變數的指標也是非常類似的。
下面的兩個聲明都是將foo定義為一個指向volatile integer型變數的指標。
volatile int * foo;
int volatile * foo;
Volatile pointers to non-volatile variables are very rare (I think I've used them once),
but I'd better go ahead and give you the syntax:
一個Volatile型的指標指向一個非volatile型變數的情況非常少見(我想,我可能使用過一次),
儘管如此,我還是要給出他的語法:
int * volatile foo;
And just for completeness, if you really must have a volatile pointer to a volatilevariable, then:
最後一種形式,針對你真的需要一個volatile型的指標指向一個volatile型的情形:
int volatile * volatile foo;
Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo),
consult Dan Sak's column, "Top-Level cv-Qualifiers in Function Parameters" (February 2000, p. 63).
順便說一下,如果你想知道關於“我們需要在什麼時候在什麼地方使用volatile”和“為什麼我們需要volatile放在變數類型後面(例如,int volatile * foo)”
這類問題的詳細內容,請參考Dan Sak`s的專題,“Top-Level cv-Qualifiers in Function Parameters”。
Finally, if you apply volatile to a struct or union, the entire contents of the struct/union are volatile.
If you don't want this behavior, you can apply the volatile qualifier to the individual members of the struct/union.
最後,如果你將volatile應用在結構或者是聯集(union)上,那麼該結構/聯集(union)內的所有內容就都帶有volatile屬性了。
如果你並不想這樣(牽一發而動全身),你可以僅僅在結構體/聯集中的某一個成員上單獨使用該限定。
Use
使用
A variable should be declared volatile whenever its value could change unexpectedly.In practice, only three types of variables could change:
‧Memory-mapped peripheral registers
‧Global variables modified by an interrupt service routine
‧Global variables within a multi-threaded application
當一個變數的內容可能會被意想不到的更新時,一定要使用volatile來聲明該變數。通常,只有三種類型的變數會發生這種“意外”:
‧在內存中進行地址映射的設備暫存器;
‧在中斷處理程序中可能被修改的全域變數;
‧多排程應用程序中的全域變數;
Peripheral registers
設備暫存器.
Embedded systems contain real hardware, usually with sophisticated peripherals.
These peripherals contain registers whose values may change asynchronously to the program flow.
As a very simple example, consider an 8-bit status register at address 0x1234.
It is required that you poll the status register until it becomes non-zero. The nave and incorrect implementation is as follows:
嵌入式系統的硬件實體中,通常包含一些複雜的外圍設備。
這些設備中包含的暫存器,其值往往隨著程序的流程同步的進行改變。
在一個非常簡單的例子中,假設我們有一個8位的狀態暫存器映射在地址0x1234上。
系統需要我們一直監測狀態暫存器的值,直到它的值不為0為止。通常錯誤的實現方法是:
UINT1 * ptr = (UINT1 *) 0x1234; // Wait for register to become non-zero.
while (*ptr == 0); // Do something else.
This will almost certainly fail as soon as you turn the optimizer on, since the compilerwill generate assembly language that looks something like this:
一旦你打開了最佳化選項,這種寫法肯定會失敗,編譯器就會生成類似如下的彙編程式碼:
mov ptr, #0x1234
mov a, @ptr
loop
bz loop
The rationale of the optimizer is quite simple:
having already read the variable's value into the accumulator (on the second line), there is no need to reread it, sincethe value will always be the same.
Thus, in the third line, we end up with an infiniteloop.
To force the compiler to do what we want, we modify the declaration to:
最佳化的工作原理非常簡單:
一旦我們我們將一個變數讀入暫存器中(參照程式碼的第二行),如果(從變數相關的上下文看)變數的值總是不變的,那麼就沒有必要(從內存中)從新讀取他。
在程式碼的第三行中,我們使用一個無限循環來結束。
為了強迫編譯器按照我們的意願進行編譯,我們修改指標的聲明為:
UINT1 volatile * ptr = (UINT1 volatile *) 0x1234; //The assembly language now looks like this:對應的彙編程式碼為:
mov ptr, #0x1234
loop
mov a, @ptr
bz loop
The desired behavior is achieved.
我們需要的功能實現了!
Subtler problems tend to arise with registers that have special properties.
For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.
對於一些較為特殊的暫存器,(我們上面提到的方法)會導致一些難以想像的錯誤。
事實上,很多設備暫存器在讀取一次以後就會被清除。這種情況下,多餘的讀取操作會導致意想不到的錯誤。
Interrupt service routines
中斷處理程序
Interrupt service routines often set variables that are tested in main line code.
Forexample, a serial port interrupt may test each received character to see if it is an ETX character (presumably signifying the end of a message).
If the character is anETX, the ISR might set a global flag. An incorrect implementation of this might be:
中斷處理程序經常負責更新一些在主程序中被查詢的變數的值。
例如,一個串行通訊中斷會檢測接收到的每一個字節是否為ETX信號(以便來確認一個消息幀的結束標誌)。
如果其中的一個字節為ETX,中斷處理程序就是修改一個全域標誌。一個錯誤的實現方法可能為:
int etx_rcvd = FALSE;
void main()
{
...
while (!ext_rcvd)
{ // Wait }
...
}
interrupt void rx_isr(void)
{
...
if (ETX == rx_char) {
etx_rcvd = TRUE;
}
...
}
With optimization turned off, this code might work . However, any half decentoptimizer will "break" the code.
The problem is that the compiler has no idea that etx_rcvd can be changed within an ISR.
As far as the compiler is concerned, the expression !ext_rcvd is always true, and,
therefore , you can never exit the while loop. Consequently, all the code after the while loop may simply be removed by theoptimizer.
If you are lucky, your compiler will warn you about this.
If you are unlucky (or you haven't yet learned to take compiler warnings seriously), your code will fail miserably.
Naturally, the blame will be placed on a "lousy optimizer."
在編譯最佳化選項關閉的時候,程式碼可能會工作的很好。但是,即便是任何半吊子的最佳化,也會“破壞”這個程式碼的意圖。
問題就在於,編譯器並不知道etx_rcvd會在中斷處理程序中被更新。
在編譯器可以檢測的上下文內,表達式!ext_rcvd總是為真,所以,你就永遠無法從循環中跳出。
因此,該循環後面的程式碼會被當作“不可達到”的內容而被編譯器的最佳化選項簡單的刪除掉。
如果你比較幸運,你的編譯器也許會給你一個相關的警告;
如果你沒有那麼幸運(或者你沒有註意到這些警告),你的程式碼就會導致嚴重的錯誤。
通常,就會有人抱怨“該死的最佳化選項”。
The solution is to declare the variable etx_rcvd to be volatile. Then all of yourproblems (well, some of them anyway) will disappear.
解決這個問題的方法很簡單:將變數etx_rcvd聲明為volatile。然後,所有的(至少是一部分症狀)那些錯誤症狀就會消失。
Multi-threaded applications
多排程應用程序
Despite the presence of queues, pipes, and other scheduler-aware communications mechanisms in real-time operating systems, it is still fairly common for two tasks toexchange information via a shared memory location (that is, a global). When you add a pre-emptive scheduler to your code, your compiler still has no idea what a context switch is or when one might occur. Thus, another task modifying a shared
global is conceptually identical to the problem of interrupt service routines discussed previously. So all shared global variables should be declared volatile. Forexample:
在實時操作系統中,除去序列、管道以及其他調度相關的通訊結構,在兩個任務之間採用共享的內存空間(就是全域共享)實現數據的交換仍然是相當常見的方法。
當你將一個優先權調度器應用於你的程式碼時,編譯器仍然不知道某一程序段分支選擇的實際工作方式以及什麼時候某一分支情況會發生。
這是因為,另外一個任務修改一個共享的全域變數在概念上通常和前面中斷處理程序中提到的情形是一樣的。
所以,(這種情況下)所有共享的全域變數都要被聲明為volatile。
例如:
int cntr;
void task1(void)
{
cntr = 0;
while (cntr == 0) {
sleep(1);
}
...
}
void task2(void)
{
...
cntr++;
sleep(10);
. ..
}
This code will likely fail once the compiler's optimizer is enabled.
Declaring cntr to be volatile is the proper way to solve the problem.
一旦編譯器的最佳化選項被打開,這段程式碼的執行通常會失敗。
將cntr聲明為volatile是解決問題的好辦法。
Final thoughts
反思
Some compilers allow you to implicitly declare all variables as volatile.
Resist this temptation, since it is essentially a substitute for thought. It also leads to potentiallyless efficient code.
一些編譯器允許我們隱含的聲明所有的變數為volatile 。
最好抵制這種便利的誘惑,因為它很容易讓我們“不動腦子”,而且,這也常常會產生一個效率相對較低的程式碼。
Also, resist the temptation to blame the optimizer or turn it off.
Modern optimizersare so good that I cannot remember the last time I came across an optimization bug.
In contrast, I come across failures to use volatile with depressing frequency.
所以,我們又詛咒編譯最佳化或者簡單的關掉這一選項來抵制這些誘惑。
現在的編譯最佳化已經相當聰明,我不記得在編譯最佳化中找到過什麼錯誤。
與之相比,為了解決一些錯誤,我卻常常使用瘋狂數量的volatile。
If you are given a piece of flaky code to "fix," perform a grep for volatile.
If grepcomes up empty, the examples given here are probably good places to start lookingfor problems.
如果你恰巧有一段程式碼需要去修正,先搜索一下有沒有volatile關鍵字。
如果找不到volatile,那麼這個程式碼很可能會是一個很好的實例來檢測前面提到過的各種錯誤。
Nigel Jones is a consultant living in Maryland. When not underwater, he can be found slaving away on a diverse range of embedded projects. He can be reached atNAJones@compuserve.com.
Nigel Jones在馬里蘭從事顧問工作。除了為各類嵌入式項目開發充當顧問,他平時的一大愛好就是潛水。你可以通過發送郵件到NAJones@compuserve.com與其取得聯繫。
(本文為學習交流之用,不可以用作商業用途,版權為原作者所有,翻譯傻孩子2007年2月。)
全站熱搜
留言列表