1. 程式人生 > >C語言嵌入式系統程式設計修煉之三:記憶體操作

C語言嵌入式系統程式設計修煉之三:記憶體操作

資料指標

  在嵌入式系統的程式設計中,常常要求在特定的記憶體單元讀寫內容,彙編有對應的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語言指標以指向的資料型別長度作自增和自減。理解這一點對於以指標直接操作記憶體是相當重要的。

函式指標

  首先要理解以下三個問題:

  (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啟動後第一條要執行的指令的位置。

  記住:函式無它,唯指令集合耳;你可以呼叫一個沒有函式體的函式,本質上只是換一個地址開始執行指令!

陣列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應成對出現!
關鍵字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]; /* 非法:編譯階段不能用到變數 */

關鍵字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) 多執行緒應用中被幾個任務共享的變數。

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?

  子曰:請看圖1,16位80186與8位NVRAM之間互連只能以地址線A1對其A0,CPU本身的A0與NVRAM不連線。因此,NVRAM的地址只能是偶數地址,故每次以0x10為單位前進!


圖1 CPU與NVRAM地址線連線


  子貢再問:So why 80186的地址線A0不與NVRAM的A0連線?

  子曰:請看《IT論語》之《微機原理篇》,那裡面講述了關於計算機組成的聖人之道。

總結

  本篇主要講述了嵌入式系統C程式設計中記憶體操作的相關技巧。掌握並深入理解關於資料指標、函式指標、動態申請記憶體、const及volatile關鍵字等的相關知識,是一個優秀的C語言程式設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在記憶體操作中體現。

  我們之所以在嵌入式系統中使用C語言進行程式設計,99%是因為其強大的記憶體操作能力!

  如果你愛程式設計,請你愛C語言;

  如果你愛C語言,請你愛指標;

  如果你愛指標,請你愛指標的指標!

相關推薦

C語言嵌入式系統程式設計修煉:記憶體操作

資料指標  在嵌入式系統的程式設計中,常常要求在特定的記憶體單元讀寫內容,彙編有對應的MOV指令,而除C/C++以外的其它程式語言基本沒有直接訪問絕對地址的能力。在嵌入式系統的實際除錯中,多借助C語言指標所具有的對絕對地址單元內容的讀寫能力。以指標直接操作記憶體多發生在如下

C語言嵌入式系統程式設計修煉

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

C語言嵌入式系統程式設計修煉軟體架構篇

轉載來自下面的網頁:http://dev.yesky.com/131/2017631_2.shtml 模組劃分  模組劃分的"劃"是規劃的意思,意指怎樣合理的將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求。C語言作為一種結構化的程式設計語言,在模組的劃分上主要

[讀書筆記2]《C語言嵌入式系統程式設計修煉

第3章 螢幕操作   3.1 漢字處理   現在要解決的問題是,嵌入式系統中經常要使用的並非是完整的漢字型檔,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的LCD上沒有必要提供顯示"電子郵件"的功能;一個提供漢字顯示功能的空調的LCD上不需要顯示一條"短訊息",諸如此類。但

[讀書筆記3]《C語言嵌入式系統程式設計修煉

第五章 效能優化   5.1 使用巨集定義   在C語言中,巨集是產生內嵌程式碼的唯一方法。對於嵌入式系統而言,為了能達到效能要求,巨集是一種很好的代替函式的方法。     寫一個"標準"巨集MIN ,這個巨集輸入兩個引數並返回較小的一個:   錯誤做法: #define MIN(

C語言學習及應用筆記C語言const關鍵字及其使用

在C語言程式中,const關鍵字也是經常會用到的一個關鍵字,那麼使用const關鍵字的目的是什麼呢?事實上,在程式中使用const關鍵字的主要目的就是為了向使用者傳遞設計者的一些意圖。 事實上,無論我們是使用const關鍵字宣告變數還是宣告引數,其目的都是為了告訴使用者這個

50.Linux/Unix 系統程式設計手冊(下) -- 虛擬記憶體操作

1.mprotect() 修改一塊虛擬記憶體區域上的保護資訊 如果一個程序在訪問一塊記憶體區域時違背了記憶體保護,核心會發送 SIGSEGV 訊號。 2.記憶體鎖: mlock() 和 mlockall() 將一塊虛擬記憶體區域鎖進實體記憶體,從而防止它被交換出去。

C語言嵌入式系統程式設計時的注意事項1

C語言是一門通用計算機程式語言,應用廣泛。C語言的設計目標是提供一種能以簡易的方式編譯、處理低階儲存器、產生少量的機器碼以及不需要任何執行環境支援便能執行的程式語言。 儘管C語言提供了許多低階處理的功能,但仍然保持著良好跨平臺的特性,以一個標準規格寫出的C語言程式可在許

C/C++ socket程式設計教程:Windows下的socket程式

本節講解 Windows 下 Socket ,學習 Linux Socket 的讀者可以跳過。 伺服器端程式碼 server.cpp: #include <stdio.h> #include <winsock2.h> #pragma c

C語言可變引數程式設計Avg、Myprinf…

 我們要訪問未命名的可變引數,首先必須在可變引數函式中宣告va_list型別的變數。呼叫va_start並傳入兩個引數:第一個引數為va_list型別的變數,第二個為省略號前最後一個有名字的引數的名稱,接著每一呼叫va_arg就會返回下一個引數,va_arg的第一個引數為va_list,第二個引數為返回的型

基於 Linux 和 MiniGUI 的嵌入式系統軟體開發指南——對話方塊和控制元件程式設計

簡介: 本文講述 MiniGUI 中的對話方塊和控制元件程式設計。首先講解 MiniGUI 中的控制元件類和控制元件例項的關係,並舉例說明控制元件子類化的概念及應用;其次講解 MiniGUI 對話方塊的程式設計技術,包括對話方塊模板的定義和對話方塊回撥函式的程式設計;最後解

C語言創建符號常量的種方法;printf()和scanf()函數

c1、#indefine pi 3.1415926形式的宏定義2、const int MONTHS=12;這使得MONTHS成為一個只讀值。3、enum 枚舉類型4、printf()和scanf()函數使我們能夠與程序通信,他們被稱為輸入/輸出函數,(I/O函數)5、字符串、浮點數輸出的類型及介紹:實例程序:

深入理解C語言的預編譯指令include

get http npe target info pdb tfs mar 語言 慫b促64u父猩84卵ml0http://www.facebolw.com/space/2101977 0俜垂屹17該性膠1http://tushu.docin.com/hmd622 6PD

C中異步IO淺析:深入理解異步IO的基本數據結構

c 異步io libaio 一個函數庫或一段代碼的數據結構之間的關系,既展示了數據的行蹤,同時又隱含了函數的調用順序和使用方法。libaio內部的多個數據結構尤其如此,哪怕我們找不到文檔或者幫助手冊,只要深刻領悟頭文件中定義的數據結構及其內在聯系,再加一點代碼的驗證,就可以達到對libaio的A

1014 C語言程序設計教程(第版)課後習題6.4

content += 教程 print ons ont c語言程序設計 lld cnblogs 題目描述 求Sn=1!+2!+3!+4!+5!+…+n!之值,其中n是一個數字。 輸入 n 輸出 和 樣例輸入 5 樣例輸出 153 1 #include "stdio.h"

1013: C語言程序設計教程(第版)課後習題6.3

其中a是一個數字 blog += color turn sam c語言程序 [] c語言 題目描述 求Sn=a+aa+aaa+…+aa…aaa(有n個a)之值,其中a是一個數字。 例如:2+22+222+2222+22222(n=5),n由鍵盤輸入。 輸入 a 輸出 和 樣

1024: C語言程序設計教程(第版)課後習題7.3

c語言程序 print clas 程序 scanf col class pri printf 題目描述 求一個3×3矩陣對角線元素之和。 輸入 矩陣 輸出 主對角線 副對角線 元素和 樣例輸入 1 2 3 1 1 1 3 2 1 樣例輸出 3 7 1 #include

1046: C語言程序設計教程(第版)課後習題10.4

con n) 順序 調整 style char ++ 輸入數據 include 題目描述 有n個整數,使前面各數順序向後移m個位置,最後m個數變成前面m個數,見圖。寫一函數:實現以上功能,在主函數中輸入n個數和輸出調整後的n個數。 輸入 輸入數據的個數n n個整數 移動的位

【藍橋杯】第六屆國賽C語言B組 1.積分迷(水題)

水題 urn class %d names 風鈴 需要 藍橋 std 小明開了個網上商店,賣風鈴。共有3個品牌:A,B,C。為了促銷,每件商品都會返固定的積分。 小明開業第一天收到了三筆訂單:第一筆:3個A + 7個B + 1個C,共返積分:315第二筆:4個A + 10個

系統學習redis——redis數據類型string類型及操作

例子 原來 等於 code 上一個 test 類型 一個 字符 redis數據類型介紹 redis數據類型有string類型、hash類型、list類型、set類型、zset類型等。 string類型及操作 string是最簡單的類型,一個key對應一個value,stri