本篇文章為系列文第三篇:
- 感測器介紹篇 - DHT11 與 MH-Z14A
- 資料溝通篇 - UART
- Arduino 踩雷篇
- (未上映)WiFi 篇:為了省下 Debug 的時間,我額外購買了 ESP32 開發版,本身已經含有 WiFi 跟藍芽功能
- (未上映)MQTT 篇:為了把資料傳給其他設備,使用了 MQTT 這個輕薄短小的通訊協定
- (未上映)Grafana / Web 篇:資料存在資料庫當然要用炫炮的方式顯示出來啊!這裡使用了 Grafana + Promethus 以及 Svelte 來顯示資料。
這篇會聊聊在 Arduino 以及語言上不熟悉而踩到的一些地雷。
CPU 讀取資料順序不同(Big Endian 與 Little Endian)
因為 CPU 指令集架構不同,所以讀資料的順序也會不一樣,中文稱作位元組順序(Endianness)。從高位元組開始讀取的話叫做 Big-Endian,從低位元組開始讀取的話則叫做 Little Endian,透過下面的圖解釋應該會更清楚:
Arduino 是以 Little Endian 作為讀取順序,在這裡可以找到解答。除了上面提到的記憶體之外,只要是讀取資料的順序不同,都可以用 Big Endian 跟 Little Endian 來描述。
記憶體由於方便管理的關係,OS 會將記憶體的空間切成固定的大小,所以如果儲存連續資料時空間不夠的話就會將資料放到下個空間裡,要取用值的時候再把它們加起來。
之所以會遇到這個問題,是因為當初耍小聰明直接將 ppm
設為一個 unsigned short
指標,這樣一來應該就會自動幫我們計算出正確的數字才對,不用在自己手動寫 bit shift,範例像下面這樣子:
struct Co2Result {
byte startByte;
byte command;
byte high;
byte low;
unsigned short ppm;
};
Co2Result *result = malloc(sizeof(Co2Result));
result->startByte = response[0];
result->command = response[1];
result->high = response[2];
result->low = response[3];
result->ppm = &result->high;
因為 short 的大小為 2byte,所以將指標指向 result->high
的話向後讀取 2byte,效果應該等同於 result->high<<8 + result->low
才對。
測試後發現結果會是一個非常奇怪的數字,我想是因為位元讀取順序的不同而導致的。乖乖用 bit shift 之後就可以拿到正確的數字了:
result->ppm = (result->high << 8) + result->low;
C 的資料結構會依平台不同
舉例來說,像是 int
就可能會是 2 bytes 或 4 bytes,一般的 long
有可能是 8 bytes 也有可能是 4 bytes。在像是 Java 等語言中不會出現資料型別在其他平台上有不同的結果,因為資料型別的定義是來自於程式語言實作本身。所以在 C/C++ 當中會出現像 uint8_t
這種型別,讓平台可根據需要定義不同大小的資料型別。
雖然對習慣寫 C 的人來說應該是家常便飯,不過在求算資料結構的大小時,應該使用 sizeof
來計算而不應該假設每個平台當中的資料型別大小都是固定的。
善用 byte
資料型別
在 Arduino 當中提供了一個資料型別叫做 byte
。背後的定義跟 unsigned char
一樣,只是時常搞不懂 uint8_t
char
int
的差別,在這邊一次釐清。
byte
跟unsigned char
是一樣的,背後都是指派 8bit 的記憶體給變數。唯一的差別只是unsigned char
的含意給使用者的感覺比較廣泛,而byte
更清楚表達了這個數字是在 0 ~ 255 之間(unsigned), arduino 推薦使用 byte 作為 consistency。uint8_t
跟byte
還有unsigned char
其實都是差不多的概念,有點像 alias。在 C/C++ 當中,不同平台可能會將 int 定義為 8bit 或 16bit,所以通常會定義uint8_t
來表達更精準的含意。
善用標頭檔拆分函式
在做 Arduino 專案的時候程式碼的架構相對小很多,把全部的功能塞在 loop
跟 setup
當中也不會讓程式碼太難讀,不過一旦電路變得更複雜一些,把全部實作塞在同一個檔案就有點雜亂了。
雖然 Arduino IDE 的副檔名叫做 .ino
,本質上背後還是 C++
,因此支援 C++
的語法,只是沒辦法使用標準函式庫。要撰寫模組,並且使用 Arduino 的函式的話,只要加入 Arduino.h
就可以了。
#include "Arduino.h"
#include <string.h>
另外如果是用 vscode
撰寫的話,會發現 Arduino 的內建函式庫沒辦法被 include
,直接引入的話雖然編譯會過,不過編輯器上會出現紅線,這時候可以加入 .vscode/c_cpp_properties.json
讓 vscode 可以找到正確的標頭檔。 如下所示:(macOS 版本為例,Linux 或 Windows 可能要另外找一下 Arduino 的安裝位置在哪裡)
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/**",
"/Applications/Arduino.app/Contents/Java/hardware/**",
"/Applications/Arduino.app/Contents/Java/hardware/arduino/**"
],
"defines": [],
"macFrameworkPath": [
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks"
],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
其他
noInterrupet()
跟cli()
是一樣的。在Arduino.h
當中是這樣定義的
#define interrupts() sei()
#define noInterrupts() cli()
- Arduino 直接用
std:string
會有問題,std 在 Arduino 上避免使用會比較好,所有那些美好的 C++ 功能在 Arduino 上統統用不了。可以參考論壇中的內文,社群上似乎有人嘗試著讓 std library 也可以在 Arduino 上運作。不知道進度如何。
這一趟實驗結果下來,我發現透過 Arduino 來學習各種比較底層的知識是個不錯的方法,既有內建的 Serial Port 可供 debug,USB 接起來 IDE 安裝一下馬上就可以上傳程式碼省掉各種麻煩,同時背後的實作也可以在 Github 上找到程式碼。同時也讓我想要找時間補足一下對於 C/C++ 的知識了(只有在大學課程時有碰過)
另外本篇文章由於對 Arduino 的不熟悉,雖然已經盡力找資料完善論述,但難免會有不完整或是出錯的地方,還請大家不吝指教。