實例分析如何遠離漫天飛舞的全局變量

關注、星標嵌入式客棧,精彩及時送達

[ 導讀]大家好,首先歡迎來了很多新朋友!感謝關注小號,我將一如既往認真分享,廣交朋友,共同進步!前篇《由static來談談模塊封裝》基本實現了對外隱藏屬性,隱藏局部模塊函數,開放接口的功能。對於這個話題還有些點沒有深入探討:爲什麼要這樣做?以及這樣做的好處。或許很多剛剛開始用C或者其他面向對象編程語言(比如C++)的小夥伴們,常常在一個項目裏爲了圖省事,整了很多全局對象、全局變量滿天飛,這樣做其實是有很多弊端,本文來聊聊這個話題。

先談談全局變量的特點

全局變量(Global Variables):在計算機編程語言中,所謂全局變量是指具有全局作用域的變量,這意味着它在整個程序中是可見的,因此是可訪問的。所謂可訪問,是指全局可讀、全局可寫。在編譯語言中,全局變量通常是靜態變量,其範圍(生命週期)是程序的整個運行時。當然解釋性語言除外,解釋性語言包括命令行解釋器(比如python, Java script,shell等)中,全局變量通常在聲明時由解釋器動態分配,這是由於解釋性語言是讀取>解釋>執行模式,不像編譯性語言,運行前可預知變量屬性,解釋性語言讀取解釋前無從獲取變量屬性。

在C/C++編程語言中,全局變量的這種全局可見性特點,濫用全局變量會讓代碼表現當相當邪惡!如果使用全局變量,就意味着下面這些場景的存在:

  • 實際代碼可能有很多地方在讀、在寫全局變量

  • 全局變量在多線程或多任務間共享

  • 全局變量在常規代碼和中斷服務程序間共享

爲啥說全局變量很邪惡?

單片機裸機編程

或許你會說,我就這樣用?咋了?軟件也跑的很好啊?來看看這個場景:

一個超字寬的變量(比如16位單片機,字寬即位16位),正被一個常規代碼在寫變量數據域時且還沒寫完,啪嘰,來了箇中斷!中斷一來,CPU趕緊把手裏的活兒停下來,奔過去處理中斷了,不巧在中斷函數裏,該變量因業務需求有需要寫這個變量。

舉個栗子,還是以之前文章的傳感器爲例,實際應用中傳感器可能是下面這樣的數據結構來描述的:

#ifndef _SENSOR_H_
#define _SENSOR_H_
typedef struct _t_sensor{
   /* 測量值與測量範圍及單位有關 */
   float value;
   
   /* 測量範圍,根據採樣值映射  */
   float upper_range;
   float lower_range;
   /* 溫度單位 */
   unsiged char unit;
}T_SENSOR;
/*假定是一個溫度測量產品*/
extern T_SENSOR temperature;

#endif _SENSOR_H_

假定這個傳感器數據結構有這樣一些被訪問的可能:

  1. 上位機會改寫測量數據的範圍及單位,串口通信中斷服務程序直接寫這個全局變量中的上下限數據域

  2. LCD操作界面可改寫溫度上下限範圍。

  3. 測量更新模塊根據當前範圍及單位配置,將傳感器採集到的數據映射爲測量值。

這些需求用例,用圖描述一下:

比如用戶操作HMI界面正改寫溫度範圍,而此時遠程上位機也正改寫溫度範圍,按上面這個做法,可能出現哪些邪惡的後果呢?

  1. 通過LCD界面寫入上限爲300.5(假定原下限爲0),此時遠程串口報文收到,程序直接在中斷服務程序將範圍修改爲(-100,200.5),此時中斷返回,用戶可能接着修改下限爲-200,則最終設備內的溫度範圍可能既不是(-100,200.5)也不是(-200,300.5),而可能是(-200,200.5)。這是一個易理解的數據混亂的場景。

  2. 現實中如果使用的單片機是8位/16位單片機,一條指令無法完成操作一個32位立即數,有可能才完成一個浮點數中某幾個字節,此時就被中斷打斷寫入200,然後中斷返回後繼續寫入剩下字節,數據可能會變得非常詭異!利用http://www.speedfly.cn/tools/hexconvert/ 在線工具轉換浮點數到16進制:

0x43964000 /* 浮點數300.5的16進制*/
0x43488000 /* 浮點數200.5的16進制*/

假定中斷進入時,HMI界面程序寫入了0x4396前兩個字節,中斷返回時,上限改寫爲200.5(0x43488000),此時繼續執行後面兩個字節寫入,則上限變成爲(0x43484000),來看看這個數是多大?變成了200.25,這是不是很邪惡?

或許有的朋友會說,可以在LCD寫範圍時關中斷嘛。誠然,可以這麼做:

void hmi_operate()
{
    /*關中斷*/
    _disable_interrupt();
    /*改寫溫度範圍*/
    ....
    /*開中斷*/
    _enable_interrupt();
}

但是如果這個全局變量有很多地方在改寫,爲了數據安全,勢必就到處開/關中斷,這樣做的壞處:

  • 經常開關中斷,勢必影響中斷響應,會有概率丟失異步中斷處理(比如串口按字節接收中斷,可能就會漏收字節),程序不健壯,工作不穩定。

  • 到處訪問改寫,不易調試,羣魔亂舞,代碼也不易維護。想加點東西,改點東西可能隨處都是坑,一不小心就掉坑裏去了!

  • 初學者甚至不會用struct將相關的數據包在一起,其結果是代碼裏到處都是基本類型的全局變量。一些簡單的業務邏輯實現變成一個複雜的代碼,數據信息流向一團亂麻。

裸機程序策略

對於上面這樣一個應用場景,怎麼解決這種混亂的現象呢。這裏分享一下我的思路,這裏將主要的串口以及測量模塊的設計思路用UML圖描述一下大體思路:

如此一來,外部就看不到全局變量了,只需要調用對應的set/get方法即可實現讀寫訪問,由於是裸機前後臺程序,數據流向就變的非常清晰了。main函數的主循環大致就可能是這樣:

void main(void)
{
   /*模塊初始化*/
   init_uart();
   init_temperature();
   ....
   
   while(1)
   {
       interprete_uart();
       /*可能是週期性調用*/
       if(timer_100ms)
       {
          timer_100ms = 0;
          update_temperature();
       }        
         
       ....
   }   
}

那麼uart協議解析要怎麼做呢?

void interprete_uart(void)
{
    if(rx_msg.flag)
    {
        rx_msg.flag = false;
        /*報文完整性檢查*/
        ...
            
        /*設置溫度配置*/
        set_upper_range(xxx);
        set_lower_range(xxx);
        set_unit(xxx);
    }
    
    if(tx_msg.flag)
    {
        tx_msg.flag = false;
        start_send();
    }
}

static start_send(T_UART_MSG *pMsg)
{
    /*負責底層操作,啓動中斷傳輸*/
}

/*提供應答數據接口*/
void reply_temperature_setting(T_SENSOR sensor)
{
    /*解析傳入參數並封裝應答報文*/
}

如此一來,數據流向將變得很清晰,串口接收到數據更新範圍配置時,也無需開關中斷了,從應用角度幾乎見不到全局變量。當然這樣做的代價就是會增加一些棧開銷。但是這種代價還是值得的。

對於測量模塊的set函數思路稍做說明:

int set_upper_range(float range)
{
    T_SENSOR temp = temperature;
    temp.upper_range = range;
    /*實現範圍合理性檢查*/
    if(check_range(temp))
    {
        /*兩個結構體變量可以直接賦值*/
        temperature = temp;
        return 0;
    }
    else
    {
       return -1;
    }
}

int set_unit(E_UNIT unit)
{
    if(unit>=E_UNIT_F)
        return -1;
    adjust_range(&temperature,unit);
    temperature.unit = unit;    
}

上述代碼旨在分享個人的一些思路,其中或有不夠嚴謹的地方,但通過這樣的設計思路,應能大幅度遠離滿天飛的全局變量。

多任務/多線程環境

上面描述其實本質上描述了裸機程序裏,普通模式運行程序與中斷服務程序對於臨界資源的競爭。事實上現在不管是單片機,還是處理器,大多都是基於一個操作系統進行應用開發。甚至還可能是多核芯片,這裏就存在併發競爭訪問資源的問題。

臨界資源:各任務/線程採取互斥的方式,實現共享的資源稱作臨界資源。屬於臨界資源的硬件串口打印、顯示等,軟件有消息緩衝隊列、變量、數組、緩衝區等。多任務/線程間應採取互斥方式,從而實現對這種資源的共享。

多任務/多線程情況下在寫模塊時,只需要封裝進保護機制即可。常見的保護機制有關中斷、信號量、互斥鎖等。在Linux內核中爲應對多核併發訪問還有自旋鎖機制。由於篇幅所限,本文就不做展開了,先挖個坑,以後有機會再分享吧。

總結一下

在前文介紹static文章的基礎上,相對更深入的介紹了爲何需要隱藏屬性以及開放接口的做法。以及如何遠離邪惡的全局變量漫天飛舞的不良設計風格。

辛苦原創總結,如果覺得有價值也請幫忙點贊/在看/轉發支持,不勝感激!

END

往期精彩推薦,點擊即可閱讀

數學之美:判定兩個隨機信號序列的相似度

由static來談談模塊封裝

void 型指針的高階用法,你掌握了嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章