聯合體在微控制器程式設計中的應用

Fireflycjd發表於2022-01-27

1、聯合體

之前的文章《列舉和結構體的結合》文中提到,結構體就像是打包封裝,把一些有共同特徵的變數封裝在內部。結構體是一種構造型別或複雜型別,它可以包含多個型別不同的成員。在C語言中,還有另外一種和結構體非常類似的語法,叫做聯合體(Union)(有些地方也叫做共用體)。

聯合體舉例如下

union data{
    char n;
    char ch;
    char f;
};
union data a, b, c;

結構體和聯合體的區別在於:結構體的各個成員會佔用不同的記憶體,互相之間沒有影響;而聯合體的所有成員佔用同一段記憶體,修改一個成員會影響其餘所有成員。

結構體佔用的記憶體大於等於所有成員佔用的記憶體的總和(存在位元組對齊問題,這裡不深入討論)。

聯合體佔用的記憶體等於最長的成員佔用的記憶體。聯合體中如果對新的成員賦值,就會把原來成員的值覆蓋掉。

//上文的聯合體定義
char data;
a.n = 0x0A;//雖然只修改了成員變數n,但是ch和f變數都會被改變
data = a.f;//那麼data的值就會被修改為0x0A

2、聯合體的應用

聯合體在一般中的程式設計應用中還是很少的,在微控制器程式設計中較多,在之前的文章《列舉和結構體的結合》將列舉和結構體的結合,下面的例子簡單說明下聯合體在結構體中的應用(我的日常開發中聯合體一般都是和結構體一起使用的)。

在做螢幕應用時牽扯到一個概念,螢幕(全綵)上一個畫素點是由於Red,Green,Blue三種顏色組成,在888模式下,每個畫素點都是由8個bit組成的,這個時候為了便是一個畫素點就需要用到結構體,這也對應前面的文章,結構體就像是打包封裝,把一些有共同特徵的變數封裝在內部。

typedef struct{
    uint8_t Red;
    uint8_t Green;
    uint8_t Blue;
    uint32_t Pix_Value;
}LCD_Pixvalue_S;

上面這樣的寫法是十分清晰的,訪問很方便,可以單獨訪整個畫素,也可以訪問畫素的某個顏色,有個問題那就是我操作紅色畫素,需要重新給Pix_Value成員賦值,如下

LCD_Pixvalue_S LCD_Pixvalue;
LCD_Pixvalue.Red = 0x12;
LCD_Pixvalue.Pix_Value = LCD_Pixvalue.Red<<16 | LCD_Pixvalue.Red<<8 |LCD_Pixvalue.Blue;

需要多一句程式碼,並且記憶體佔用也大。當然,直接用下面寫法,不會多佔用記憶體,但是訪問不方便。

typedef struct{
    uint32_t Pix_Value;
}LCD_Pixvalue_S;

那麼這個時候,使用聯合體和結構體組合起來,就可以既不會多佔記憶體,訪問也很方便。

typedef union{
    struct{
        uint8_t Red;
        uint8_t Green;
        uint8_t Blue;      
    }Pix;
    uint32_t Pix_Value;
}LCD_Pixvalue_S;

那麼就可以如下操作

LCD_Pixvalue_S LCD_Pixvalue;
uint32_t data;
LCD_Pixvalue.Pix_Value = 0x0012FF00;
LCD_Pixvalue.Pix.Red = 0x25;//單獨修改紅色
data = LCD_Pixvalue.Pix_Value;//data的值就為0x0012FF25

當然,結構體的定義你也可以寫到外面,其他地方可以使用,如下

typedef struct{
    uint8_t Red;
    uint8_t Green;
    uint8_t Blue;      
}Pix_s;
typedef union{
    Pix_s Pix;
    uint32_t Pix_Value;
}LCD_Pixvalue_S;

那麼關於記憶體佔用的問題,上面的定義方法,定義一個LCD_Pixvalue_S型別的變數佔用4個位元組。示例圖如下

 

 3、聯合體在串列埠開發中的應用

上面舉例LCD應用中聯合體的應用,這個是由於LCD一個畫素由紅綠藍3色組成,所以用聯合體很方便。在微控制器專案開發中,串列埠協議解析也可以利用到聯合體,十分方便。

在私有自定義協議中,合理定義協議,利用聯合體程式碼十分方便。下面的例子不講幀頭,幀尾部分,舉例聯合體在協議解析中的應用,靈活應用。

串列埠協議舉例如下

 那麼程式碼可以如下編寫

typedef union{
    struct{
        uint32_t cmdlen;
        uint8_t cmd;
        uint8_t cmdbuf[7];
        uint16_t crc16;
    }unit;
    uint8_t buffer[14];
}uart_buffer_s;
uart_buffer_s uart_buffer;
int main(void)
{
  uint8_t len;
  len = 0;
  uart_buffer.buffer[len++] = 0x12;
  uart_buffer.buffer[len++] = 0x34;
  uart_buffer.buffer[len++] = 0x56;
  uart_buffer.buffer[len++] = 0x78;
  uart_buffer.buffer[len++] = 0xAA;
  for(int i=0;i<7;i++)
  {
    uart_buffer.buffer[len++] = i;
  }
  uart_buffer.buffer[len++] = 0x11;
  uart_buffer.buffer[len++] = 0x22;
  while (1);
}

執行的結果如下

可以看到,我們只是往uart_buffer.buffer填充資料,模擬串列埠接收資料,接收完畢後,就自動解析出來我們自定義的cmdlen,cmd,cmdbuf和crc16。這裡需要注意的是16位和32位的資料型別是小端模式,關於小端模式請看之前的文章《C語言在STM32中的記憶體分配》,這個還是很方便的。

!!!但是!!!需要注意位元組對齊的問題,比如把上述cmdbuf修改為8個位元組,就會出問題,如下

 crc16就會出問題,這個就是位元組對齊的問題,不懂的同學自行百度一下,這裡不再重點講解。

 除了上述自定義協議解析時會用到聯合體,還可以解決浮點型float的讀取問題,float是佔用4個位元組的,如果將從串列埠接收的4個位元組轉換成float呢?聯合體就可以可以解決這個問題

下面示例程式碼,我們知道浮點數231.5的16進製表示為0x43678000。

typedef union{
    float data;
    uint8_t buffer[4];
}uart_buffer_s;
uart_buffer_s uart_buffer;
int main(void)
{
  uint8_t len;
  len = 0;
  uart_buffer.buffer[len++] = 0x00;
  uart_buffer.buffer[len++] = 0x80;
  uart_buffer.buffer[len++] = 0x67;
  uart_buffer.buffer[len++] = 0x43;
  while (1);
}

結果如下

 可以看到我們模擬從串列埠收到4個位元組,使用聯合體,不用我們額外寫程式碼,就可以自動轉化為float型。當然這個轉換也是小端模式的,小端模式詳解請看文章《C語言在STM32中的記憶體分配》。

相關文章