Raspberry Pi Pico - 如何一次設定多個GPIO (Mask Set)

在Raspberry Pi Pico程式中,我們通常會使用gpio_set()來設定一個腳位的數值。 像是在簡單的Blink程式中,我們會這樣寫:

    while(true) {
        gpio_set(led_pin, 1);
        sleep_ms(1000);
        gpio_set(led_pin, 0);
        sleep_ms(1000);
    }

如果我們要一次設定更多隻腳的話,直覺上我們可能會寫更多行的gpio_set()

    gpio_set(pin1, value1);
    gpio_set(pin2, value2);
    // ...
    gpio_set(pinN, valueN);

這樣子如果要設定N隻腳的話,就要寫N行程式,有點沒效率。 其實有更聰明的方法可以只用一行就達到一樣的目的,這個方法叫做mask set。 在實作之前,我們需要了解一下GPIO的數值(0和1)是怎麼被設定的。

GPIO Register

以RP2040為例,我們總共有30個GPIO可以使用。 這些GPIO的狀態可以是0或是1,因此很適合用一個32-bit register來存他們全部人的狀態。 在這個register中,每一個bit會對應到一隻腳,如下圖所示。

除此之外,還需要把每個GPIO分別是輸出還是輸入存下來,這個資料也是存在另一個32-bit register中。 在RP2040上,控制GPIO的基本操作的register有三個:

所以,如果我們要一次設定多個GPIO的數值的話,我們只要把要設定的0和1排好,寫進GPIO_OUT register就好了!

Bit Mask

不知道讀者有沒有發現,上面介紹的直接寫進GPIO_OUT register的方法有一個小問題,那就是寫一次30隻腳的值都會被改掉。 假如我們只想要改變其中幾隻腳怎麼辦?這時候就要用bit mask。 Bit mask是一串0和1,因為RP2040的register是32-bit,所以每一個bit mask也有32個bit。 Bit mask中1的地方代表這隻腳的數值是我們想要改的。 例如我們假如想要改GPIO2到GPIO8的話,對應到的bit mask就會是0b00000000_00000000_00000001_11111100。 但是打一整串0和1太麻煩了,所以在程式裡面我們都直接打十六進位,剛才的bit mask就會變成0x000001FC。 (大家一定要熟悉二進位跟十六進位的轉換啊啊啊!)

有了這個bit mask之後我們就可以透過一些bitwise operation來做到只改變bit mask中是1的位置。

Bitwise operation

Bitwise的意思是一個bit一個bit來做,也就是說這些operation只會動到兩個register中相同位置的bit,不會被其他位置影響到。 重要的bitwise operation有:

我們直接來看一個例子,有一個8-bit的數值B7...B0,跟一個bit mask 0x0f:

所以說怎麼去改一個register然後只動到bit mask裡面是1的位置呢?

  1. 用bit mask的相反,跟原本的register做AND。這時候我們想要改的那些bit會變成0。
  2. 用bit mask跟想要寫進去的數值做AND,確保其他的bit都是0。
  3. 把第二步的結果跟第一步改好的register做AND,這樣就可以把我們想改的那些bit設定成新的數值了!

一樣我們來看一個例子,假如我們想要把一個8-bit register的最後4個bit改掉的話,利用上面的流程會是這樣:

不過pico-sdk有幫我們寫好一個function,來把這三個步驟整合在一起,實作的時候我們會直接利用。

實作

我們來利用GPIO mask set來實作驅動七段顯示器(Seven Segment Display)。 七段顯示器有七個輸入ABCDEFG,我們分別接到GPIO8...GPIO2。 因此,我們的bit mask就是:

const uint32_t SSD_mask = 0x000001FC;

我們想要讓七段顯示器顯示十六進位的0~F,因此需要把每個數字對應到的ABCDEFG數值先寫下來。 為了方便理解和debug,我們將GFEDCBA依序存在最低的七個bit,其中1代表亮、0代表不亮。

const uint32_t SSD[16] = {      // 7-segment display code
    0x7E, 0x30, 0x6D, 0x79,     // 0111 1110, 0011 0000, 0110 1101, 0111 1001, 
    0x33, 0x5B, 0x5F, 0x70,     // 0011 0011, 0101 1011, 0101 1111, 0111 0000, 
    0x7F, 0x73, 0x77, 0x1F,     // 0111 1111, 0111 0011, 0111 0111, 0001 1111,
    0x4E, 0x3D, 0x4F, 0x47      // 0100 1110, 0011 1101, 0100 1111, 0100 0111
};

接下來,我們來初始化GPIO和設定方向(輸出或輸入)。 和設定數值一樣,可以透過bit mask的方式來一次初始化多個GPIO,用到的function是gpio_init_mask()。 這個function的輸入是一個bit mask,而它會把bit mask中是1的腳位都初始化。 同樣,我們也可以一次設定多個GPIO的方向,用到的function是gpio_set_dir_out_masked()。 這個function的輸入一樣是一個bit mask,而它會把bit mask中是1的腳位都設為輸出。

    gpio_init_mask(SSD_mask);
    gpio_set_dir_out_masked(SSD_mask);

最後是真正在設定七段顯示器的內容的迴圈,我們做一個簡單的每秒數一個數字的功能。 我們只要用gpio_set_masked()這個function,就可以做到一次改變多個GPIO的數值的效果。 這個function有兩個輸入,第一個是bit mask,第二個是數值。 Bit mask裡面是1的腳位,就會被改成數值裡面對應位置的0或是1。 不過因為我們剛才是把要寫到七段顯示器的資料放在最低的7個bit,但我們是把七段顯示器接到GPIO8...GPIO2。 因此,我們需要用<<把數值往左移兩個bit。 最後的迴圈會長這樣:

    while(true) {
        for(int i = 0; i < 16; i++) {
            gpio_put_masked(SSD_mask, SSD[i] << 2);    
                // shift data left by 2 bits to align to pins 2-8
            sleep_ms(1000);
        }
    }

Download the source code for this example.