部落格
如何以Arduino使用ADS1115 ADC的連續模式
德州儀器ADS1115是一個16位元的類比數位轉換器(ADC)。這個ADC是基於ΔΣ架構,取樣率最高為860Hz。 ADS1115必須操作在連續模式(Continuous Mode)下才可以用最高860Hz的取樣率來做轉換。 在這篇文章中,我們會示範如何在Arduino環境下設定ADS1115使用連續模式,並且以860Hz取樣率做轉換。
在連續模式下,ADS1115會在上一次轉換完成後,馬上開始下一次的轉換。 在轉換完成時,也會將ALERT腳位拉低,藉此來告訴Arduino可以讀取轉換結果了。 我們在Arduino上會利用GPIO中斷(interrupt)的方式來讀取ALERT訊號,當我們偵測到ALERT由高變低時,就去讀取ADS1115的轉換結果。
這邊簡單介紹一下GPIO中斷的運作方式: 平常Arduino都在重複執行loop()中的程式,當Arduino偵測到GPIO中斷事件(例如ALERT由高變低)的時候,它會自動呼叫interrupt callback的函式。 在這個函式中,我們要寫一小段程式來處理這個中斷,例如改變一些變數的數值。 執行完callback的小程式之後,Arduino會跳回剛才在loop()中的位置。
本文使用Arduino Uno,它支援GPIO中斷的腳位只有2和3。 因此,依照以下表格來連接ADS1115和Arduino Uno的腳位。
ADS1115 | Arduino Uno |
---|---|
VDD | 5V |
GND | GND |
SCL | A5 |
SDA | A4 |
ADDR | not connected |
ALRT | D3 |
ADS1115是使用I2C通訊協定來做資料的交換;因此,我們將使用Adafruit的ADS1X15函式庫來處理與ADS1115的資料交換。
首先,我們要先建立一個Adafruit_ADS1115物件,同時我們也宣告一些之後會用到的變數。
#include <Adafruit_ADS1X15.h>
Adafruit_ADS1115 myADS;
unsigned long t0;
int16_t v;
String str;
接下來,我們要設定中斷處理。 newDataReady()是我們的中斷處理callback函式,當ALERT由高變低時,會自動執行這個callback函式。 我們會利用這個函式來設定new_data這個變數,之後在loop()中會根據這個變數來判斷要等待還是讀取ADS1115的轉換結果。 因為new_data這個變數是由中斷處理來改變,我們必須使用volatile的變數型態,才不會被編譯器優化掉。
const int intPin = 3;
volatile bool new_data = false;
void newDataReady() {
new_data = true;
}
在setup()中,我們必須做幾件事:
- 啟用序列埠通訊
- 啟用GPIO中斷處理
- 設定ADS1115使用連續模式
- 讀取初始時間
void setup() {
Serial.begin(115200);
// The convertion is ready on the falling edge of a pulse at the ALERT/RDY pin.
attachInterrupt(digitalPinToInterrupt(intPin), newDataReady, FALLING);
myADS.begin();
myADS.setGain(GAIN_FOUR); // +- 1.024V range
myADS.setDataRate(RATE_ADS1115_860SPS);
myADS.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, true);
// We should set "continuous" to true to reach optimal speed.
t0 = micros();
}
為了每秒傳輸860個轉換結果,我們需要選擇高一點的鮑率,例如115200。 ADS1115的ALERT腳位是連接到intPin,因此我們要將中斷處理利用attachInterrupt()接在這隻腳上,並且設定newDataReady()作為我們的callback函式。
接下來,我們要來設定ADS1115。 我們在這個範例中使用了四行來做設定。
- begin()只是用來做初始化。
- setGain()是用來設定類比訊號進入ADS1115的放大倍率,可以由2/3倍至16倍。 放大倍率的設定以及對應的輸入電壓範圍請參考datasheet。
- setDataRate()是用來設定取樣頻率,我們這邊使用最高的860Hz。 ADS1115的連續模式只可以操作在某幾個特定的頻率,請參考datasheet。
- startADCReading()是用來啟動ADS1115,第一個參數是選擇輸入的頻道,在這個範例中我們選擇了頻道0;第二個參數必須設為true來使用連續模式。
Adafruit ADS1X15函式庫還有許多其他的設定可以調整,詳細的內容可以參考它的Github Repo。
最後,在loop()中,當new_data為true時,我們就可以讀取ADS1115的轉換結果了。 記得要在讀取完ADS1115之後把new_data設為false,這樣子我們的中斷處理才能正常運作。 在這個範例中,我們順便把時間用微秒印出來檢查取樣頻率。
void loop() {
if(!new_data) return;
// Don't call the ADC until we receive a convertion complete signal.
v = myADS.getLastConversionResults();
str = micros() - t0;
str += " ";
str += v;
Serial.println(str);
new_data = false;
}
Download the source code for this example.
把程式碼上傳到Arduino之後,打開序列埠監控視窗,並且將鮑率設定為115200。 我們應該可以看到Arduino開始回傳資料。
Arduino傳回來的資料的第一個是時間,兩行之間大概差了1130微秒,對應到的取樣頻率是稍微超過860Hz。 第二個資料是ADS1115的轉換結果,會是一個介於-32768到+32767之間的數值。 因為ADS1115有一個模式是測量兩個輸入的電位差,所以可以有負的數值。 現在,我們成功讓ADS1115操作在連續模式,並且用Arduino以最高860Hz的取樣頻率來讀取轉換結果了!
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_OE:儲存該腳位做為輸出(1)還是輸入(0)。
- GPIO_OUT:儲存若該腳位做為輸出,要輸出的數值(0或1)。
- GPIO_IN:儲存該腳位的實際數值,作為輸入使用,只可以讀取。
所以,如果我們要一次設定多個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有:
- NOT(~):把輸入的0變1、1變0。
- AND(&):兩個輸入都是1,才會輸出1。換句話說,有一個是0,就會輸出0。
- OR(|):兩個輸入只要有一個是1,就會輸出1。換句話說,要兩個都是0才會輸出0。
- XOR(^):兩個輸入恰有一個是1的時候,才會輸出1。
我們直接來看一個例子,有一個8-bit的數值B7...B0,跟一個bit mask 0x0f:
- 用AND之後,只會留下bit mask裡面是1的位置,剩下的會變成0。
- 用OR之後,只會留下bit mask裡面是0的位置,剩下的會變成1。
- 用XOR之後,bit mask裡面是1的位置會不變,而bit mask裡面是1的位置會變相反。
所以說怎麼去改一個register然後只動到bit mask裡面是1的位置呢?
- 用bit mask的相反,跟原本的register做AND。這時候我們想要改的那些bit會變成0。
- 用bit mask跟想要寫進去的數值做AND,確保其他的bit都是0。
- 把第二步的結果跟第一步改好的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);
}
}