C語言最終奧義|Function pointer 函數指標在大型命令列中的應用

本文說明何謂函數指標(function pointer),以及使用函數指標的時機。並使用簡單的範例來說明什麼是函數指標,接著建立對應大量指令的函數表(Function Table)來實現PMBus中的256種功能,實現只要一行程式碼就可以執行多種不同的程式的結構。{alertInfo}

目錄

    何謂函數指標Function Pointer

    首先,函數指標顧名思義就是指向函數的指標,但是它存放的不是資料,而是函數(function)的組語指令。假如現在有一個指標叫做ptr,在一般存放資料的指標中,ptr是指向變數的存放位置。但是如果ptr是一個函數指標,則是指向整個函數第一個指令的起始位置。因此,我們對函數指標的操作,不能夠像一般指標中進行如*(prt+1)的解址(dereference)操作,這樣會造成CPU 在執行程式的時候產生錯誤順序而故障。下圖說明在F280035C中的一般指標與函數指標的差異。


    從範例中可以看到假如ptr=0x80000,進行*ptr解址操作可以得到uint16_t格式的voltage變數,內容是0x1234,若進行*(ptr+1)操作可以得到0x8001位址的資料current,內容是x5678。

    但如果ptr是函數指標ptr=0x83f26,直接進行*ptr操作編譯器會報錯。雖然可以暴力把ptr從函數指標的類型強制轉換成uint32_t*型態讀到0x28A901A4,但這對我們而言沒有意義,我們讀到組語指令也不能進行應用。再白話一點,今天媽媽請你去買10個蘋果,"買"是指令,"10"是資料。最後到櫃檯結帳的時候要計算付多少錢,但大腦卻讀到"買"這件事,對結帳這件事而言是沒有意義的。這裡有個應用StackoverFlow-Decode function pointer in C說明解碼函數指標會遇到的事情。

    函數指標常見的應用就是用一函數代替多個函數,達到精簡程式碼的需求。像是計算機進行加減乘除的運算,本來需要四個switch的case,現在只要執行一行就好。Neso Academy-Application of Function Pointers in C中說明如何實作這個例子。


    函數指標範例

    上一段說明什麼是函數指標,接下來就讓我們對函數指標進行實際的宣告與應用。現在有兩個函式分別讀取輸入電壓與輸入電流的程式,他們分別回傳uint16_t格式的3與110。

    uint16_t read_input_voltage(void){ return 110; } 
      uint16_t read_input_current(void) {return 3;}

    接著定義一個函式指標*function_ptr, 他的資料型態與回傳值型態是uint16_t,調用的參數型態是void

    uint16_t (*function_ptr)(void); 

    這時候function_ptr就可以自由的當成讀取電壓的函數,也可以當作讀取電流的函數被使用者調用,用法如下:

      function_ptr = read_input_voltage;		// assign function_ptr as read_input_voltage function
      //function_ptr = read_input_current;		// assign function_ptr as read_input_cutrrent
      read_value = (*function_ptr)();		// read back the value depending what function_ptr is

    指派函數指標有兩種方法,一種是令function_ptr = &read_input_voltage,另一種是function_ptr = read_input_voltage。這兩種對函數指標而言是相同的宣告,因為函數名稱本身代表的就是函數的起始位址。在這裡筆者就直接使用函數名稱的指派方法避免搞混,使用函數指標時要注意的就是函數的回傳值。

    函數表Function Table

    人機介面或命令列需要使用者輸入的命令來做對應的動作,當命令不多的時候可以用簡易的if/else或者switch case讀取命令,並執行對應的功能。但是當指令數量非常多的時候(如PMBus總共有256種指令)的時候,用switch case會讓程式碼非常冗長且喪失可擴充性。這時函數指標就派上用場!常見的就是使用多個函數指標組成函式表(Function Table),在依據指令內容呼叫函式表內的函數指標,就可以達到快速響應的命令功能。函式表是存放許多函示指標的矩陣,可以依據不同指令讀取函示指標的內容來執行相對應的功能。但關於函示指標有幾點要注意,首先要確定函數回傳值的格式,近量讓相同回傳格式的函數放在同一個表格內,避免型態轉換產生的錯誤;以及切記函數指標不能使用遞增/遞減運算。

    函數表在PMBus中的應用

    在需要大量搜尋指令與多種不同功能的應用中,同時也有規範或定義的場合,函數表就是最適合應用的工具。像是各種命令列、通訊過程、或者通訊指令等等。筆者這次拿前陣子分析過的PMBus為例,他是已經定義好的電源通訊介面,有大約117種指令,如果使用switch case實作應該會很花時間,同時速度會被拖慢。但若使用函數表並搭配指令搜尋的演算法,就能夠快速找到指令對應的函數來進行通訊處理,加強系統功能。以下是簡易的PMBus與Function Table的應用概念程式碼:

      
      	// define 10 different function inside the function table
        #define READWORD_CMD_NUM	10
        
      	// declare function table for PMBus read word transaction
        uint16_t (*PMBusReadWordFuncTbl[READWORD_CMD_NUM])(void);
        
      	// locate function pointer in function table
      	void PMBus_read_word_func_init(void){
    	//function table index		command function pointer		PMBus command
        PMBusReadWordFuncTbl[0] 	= set_fan_command_1;			//(0x3B) fan command 1
        PMBusReadWordFuncTbl[1] 	= set_fan_command_2;			//(0x3C) fan command 2
        PMBusReadWordFuncTbl[2] 	= set_over_current_warn_limit;	//(0x4A) Iout_oc_warn
        PMBusReadWordFuncTbl[3] 	= set_over_temp_warn_limit;		//(0x51) OT_warn_limit
        PMBusReadWordFuncTbl[4] 	= read_status_word;				//(0x79) Status_word
        PMBusReadWordFuncTbl[5] 	= read_input_voltage;			//(0x88) read input voltage
        PMBusReadWordFuncTbl[6] 	= read_input_current;			//(0x89) read input current
        PMBusReadWordFuncTbl[7] 	= read_output_voltage;			//(0x8B) read output voltage
        PMBusReadWordFuncTbl[8] 	= read_output_current;			//(0x8C) read output current
        PMBusReadWordFuncTbl[9] 	= read_temperature_1;			//(0x8D) read temperature 1
        PMBusReadWordFuncTbl[10] 	= read_temperature_2;			//(0x8E) read temperature 2
        }
        ...
        // In initalize phase of the main code, locate function table first
        // Otherwise the program will go to 0x0000 address and execute nothing!
        PMBus_read_word_func_init();
        ...
        
        // use function table when received command
        uint16_t PMBus_data;		// PMBus command function execute result
        uint16_t command;
        command = buffer[0];		// get first data in PMBus rx buffer as command
        PMBus_data = PMBusReadWordFuncTbl[command]();
        ...
        
        // transmit back the execute result
        buffer[0] = (PMBus_data >> 8) & 0xFF;		// transmit low byte data
        buffer[1] = PMBus_data & 0xFF;				// transmit high byte data
    

    上述例子中,先定義一個大小為READWORD_CMD_NUM的矩陣名稱是PMBusReadWordFuncTbl,裡頭存放的是回傳uint16_t 型態的函數指標,接著PMBus_read_word_func_init()替矩陣中每一個元素放入函數指標,並對應到相對的PMBus指令。要調用時,只需要將指令讀取出來放入command,並執行PMBus_read_word_func[command](),就可以得到函數的回傳值。在把回傳值放入PMBus_data就完成一次函數表的運用。

    結論

    這篇文章說明何謂函數指標,並詳細的舉例函數指標的宣告與執行。接著用嵌入式系統常用的通訊介面PMBus作為範例,延伸說明大量函數指標構成的函數表要如何實際應用在專案中。除了PMBus,舉凡需要命令列的功能都非常適合使用函數指標來實作,不僅可以大量減少巢狀結構(Nesting)的程式碼之外,也具備良好的可讀性與可擴充性,為專案增加競爭力!

    Post a Comment

    留個言吧

    較新的 較舊