本文說明何謂函數指標(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)的程式碼之外,也具備良好的可讀性與可擴充性,為專案增加競爭力!
張貼留言
留個言吧