2015年9月14日 星期一

Arduino 基本語法筆記

Arduino 的程式語法基於 C/C++, 其實就是客製化的 C/C++ 語言, 其程式架構仿自廣為藝術與設計界人士熟悉的 Processing 語言, 而其開發工具 Arduino IDE 則是衍生自以 Processing 為基礎的電子開發設計平台 Wiring. 由於 Processing IDE 使用 Java 撰寫, 因此 Arduino IDE 有自帶一個 JRE. Processing 語言撰寫的程式稱為 Sketch (草稿碼), 乃是簡化後之 Java 語法, 經 IDE 編譯變成可執行的 Java 類別. 而 Arduino IDE 則是以 Processing IDE 為架構, 但是採用了 C/C++ 語法.

Processing 也有支援網頁的 Processing.js, 參考 :

# https://processing.org/exhibition/
# https://en.wikipedia.org/wiki/Processing.js

Arduino 程式可由五個部分組成 :

//1. 匯入函式庫與定義 (可有可無)
#include <SoftEasyTranfer.h>
#define LEDPIN 13;

//2. 宣告常數與全域變數 (可有可無)
const float PI=3.14159;
int r;

//3. 設定函式 (必要)
void setup() {}

//4. 無限迴圈  (必要)
void loop() {}

//5. 自訂函式 (可有可無)
float area(float r) {
  float a=PI*r*r;
  return a;
  }

其中 setup() 與 loop() 是一定要有的函式 (均無參數無傳回值), 其他則視需要而定. Arduino 語言採用 C/C++ 語法, 加上以 Wiring 為基礎的電子設計核心函式庫組合而成, 包括 Digital I/O, Analog I/O 等函式庫. 內建的函式庫可直接調用, 但若有使用第三方函式庫 (例如驅動感測器模組所需的函式庫), 則必須使用 include 前置指令引入. 此外, 也可以用前置指令 define 定義一個常數或巨集 (運算式).

前置指令乃 C 編譯器指令, 不屬於 C 語言本身, 其用途有三 :
  1. 引入標頭檔 : 例如 #include <myLibrary.h> 或 "myLibrary.h"
  2. 定義常數 : 例如 #define PI 3.14159
  3. 定義巨集 : 例如 #define AREA(r) PI*r*r
所以前置指令的功能一言以蔽之就是替換, include 就是在標頭處以指定之檔案內容替換; 而 define 就是在程式中用到所定義之常數與巨集名稱時, 以其內容替換. 巨集的功能事實上與函數類似, 不同之處是函式呼叫使用堆疊, 而巨集則是直接放在原始碼中, 執行效率較快 (但若很多地方都要用到時, 編譯後就會比較大).

標頭檔可用角括號 < > 或雙引號 "", 差別是用雙引號時, 前置處理器會先從原始檔所在位置開始去搜尋標頭檔; 而用角括號則會先從 libraries 目錄開始找.

以下整理這些基本語法以利後續實驗時查考之用, 以 UNO/Nano/Pro Mini 這些主要板子為對象. Arduino 語法文件請參考 :

https://www.arduino.cc/en/Reference/HomePage

變數與函式命名 (識別字) :
  1. 只能使用英數字與底線組合, 且不能以數字開頭, 英文字母有分大小寫.
  2. 不可使用保留字.
宣告變數時不一定要同時初始化 (賦值), 但為了 debugging 方便, 最好宣告同時也初始化, 例如 int 變數就設為 0, boolean 變數設為 false 等 :

char c;
int i=0;
boolean  b=false;

因源自 C/C++ 語言, 因此宣告變數時必須指定其值之資料類型, 函式若有傳回值也要指定傳回值之資料類型, 否則須宣告為 void, 例如 setup() 與 loop() 兩個必要函式就不會有傳回值, 故必須宣告為 void.

宣告於 setup() 與 loop() 或自訂函數外的變數稱為全域變數 (global), 在程式的任何地方皆能存取; 宣告於函數內部的變數 (包含傳入之引數) 稱為區域變數 (local), 其值僅在函數內部有效. 如果全域變數與函數中的區域變數同名, 則在函數中存取到的是區域變數, 例如 :

int i=1;
void setup() {
  Serial.begin(9600);
  int i=0;
  Serial.println(i);    //輸出 0 (區域變數)
  }
void loop() {}


資料類型 :

Arduino 的資料型態與 C 語言一樣, 但資料長度可能因板子而異, 下表適用於 UNO, Nano, Pro Mini 等以 ATmega328 為處理器的板子, 只有 Due 板子有所不同 :

 資料型態 說明 記憶體長度 數值範圍
 boolean 布林 8 bits true (1, HIGH), false(0, LOW)
 byte 位元組 8 bits 0~255
 char 字元 8 bits -128~127
 short  短整數 16 bits -32768~32767
 int 整數 16 bits -32768~32767
 word 字 16 bits 0~65535
 long 長整數 32 bits -2147483648~2147483647
 float 浮點數 32 bits +-3.4028235e+38
 double 倍精度浮點數 32 bits +-3.4028235e+38

Arduino 還有兩個非數值的資料類型 :

資料型態 說明
 void 用來表示函數無傳回值時之資料類型
 String 用來表示字串 (Arduino 0019 Alpha 版以後)

要注意的是 : 一般 Arduino 板子的 double 跟 float 是完全一樣的, 都是 32 位元, 而 Due 板則跟一般 C 語言一樣是 64 位元. 其次, 整數的 short 與 int 也是一樣 16 位元的 (但在 Due 板子, int 是 32 位元). 文件中整數類的 char, int, 與 long 這三種型態有分 signed (有號) 與 unsigned (無號的), 沒有提到 unsigned short, 但我測試是有的.

資料型態 說明 記憶體長度 數值範圍
 unsigned char 字元 8 bits 0~255
 unsigned short 短整數 16 bits 0~65535
 unsigned int 整數 16 bits 0~65535
 unsigned long 長整數 32 bits 0~4294967295

有號與無號差別在於最高位元是否拿來當正負符號 (2 的補數), signed 使用最高位元來表示正負數, 1 為負數, 0 為正數; 而 unsigned 則全部都是正數, 因此其可表示的正數範圍是 signed 的兩倍, 例如儲存字元用的 char 也可以用來儲存較小的整數 (8 位元), 若宣告為 unsigned char, 其範圍便從 -128~127 變成 0~255.

Char, short, int, 與 long 預設是 signed, 亦即只有全部用做正數的變數才需要宣告 unsigned. 正負值都有的變數宣告為 signed 是多此一舉. 整數類中最常用的是 int, 因為其範圍可以滿足大部分應用所需.

Char 是其實是 8 位元整數, 用來表示 ASCII 字元, 例如 'A' 實際上是以其 ASCII 碼 65 儲存的, 好處是可以透過運算處理字元, 例如 'A' + 2 就得到 'C'. 所以字元可以用下面兩種方式表示 :

char c='A';   //字元必須用單引號括起來, 不能用雙引號
char c=65;

Byte 與 word 類型只有正數, byte 與 char 一樣是 8 位元, 等於 unsigned char; 而 word 與 int 一樣是 16 位元, 等於 unsigned int.

布林值可用 true/false (必須小寫), 0/1, 或者 HIGH/LOW 表示, 依據使用場合何者較有意義而定. 其中 HIGH/LOW 是 Arduino 定義的常數, 適合用在 digitalWrite() 函數中表示 LED 輸出位準, 而 true/false 適合用在程式邏輯的分岐判斷.

指定資料類型時須注意不能超過範圍, 超過時將歸零或變成負數等非預期結果, 成為 roll-over (反折), 例如 byte 最大 255, 若存入 256, 仍可通過編譯, 但真正存入的值會變成 0; 而整數最大為 32767, 若存入 32768, 結果變成 -32768, 例如 :

void setup() {
  Serial.begin(9600);
  byte b=256;
  Serial.println(b);    //輸出 0
  int i=32768;
  Serial.println(i);     //輸出 -32768
}
void loop() {}

另外, 初始化一個變數時, 所賦予之字面值 (Literal) 的表示方法也會影響運算結果之正確性, 例如 2048*16=32768, 用 int 來儲存是不夠的, 必須宣告為 long. 此外字面值 2048 或 16 也至少要有一個後面加一個 "L" 或者用 (long) 強制轉型才行, 表示要以資料類型 long 儲存, 否則它會以 int 為類型進行運算, 例如 :

void setup() {
  Serial.begin(9600);
  long a=2048*16;    
  Serial.println(a);      //輸出 -32768 (非預期之結果)
  long b=2048L*16;    
  Serial.println(b);      //輸出 32768 (預期之結果)
  long c=(long)2048*16;    
  Serial.println(c);      //輸出 32768 (預期之結果)
}
void loop() {}

整數字面值強制轉型的後綴有三個, 大小寫均可 :
  1. L 或 l : 強制轉成長整數
  2. U 或 u : 強制轉成無號之整數 (int)
  3. UL 或 ul : 強制轉成無號之長整數
同樣地, 浮點數運算也要注意字面值的問題, 運算元之中至少要有一個必須以浮點數表示, 否則會得到非預期之結果, 如下例所示 :

void setup() {
  Serial.begin(9600);
  float a=1/2;
  Serial.println(a);    //輸出 0.00 (被當成整數處理)
  float b=1.0/2;
  Serial.println(b);    //輸出 0.50 (至少一個是浮點數即可)
  float c=(float)1/2;
  Serial.println(c);     //輸出 0.50 (強制轉型)
}
void loop() {}

Arduino 內建六個函式來轉換不同資料型態如下表 :

 型態轉換函式 說明
 char(x) 將任何型態之 x 轉成 char 型態
 byte(x) 將任何型態之 x 轉成 byte 型態
 int(x) 將任何型態之 x 轉成 int 型態
 word(x), word(h,l) 將任何型態之 x 轉成 word 型態, x 可拆成高位元組 h 與低位元組 l
 long(x) 將任何型態之 x 轉成 long 型態
 float(x) 將任何型態之 x 轉成 float 型態

要注意, 記憶體長度大的轉成較小時會有 roll-over 問題, 例如 :

void setup() {
  Serial.begin(9600);
  int i=257;
  byte b=byte(i);   //輸出 1, byte 類型最大值 255, 256 時反折為 0
  Serial.println(b);
  }
void loop() {}

另外, 整數除了用十進位表示外, 也可以用二進位 (0b/0B 開頭), 八進位 (0 開頭), 與十六進位表示 (0x/0X 開頭), 例如 :

void setup() {
  Serial.begin(9600);
  byte dec=128;
  byte bin=0b10000000;
  byte oct=0200;
  byte hex=0x80;
  Serial.println(dec);  //輸出 128
  Serial.println(bin);  //輸出 128
  Serial.println(oct);  //輸出 128
  Serial.println(hex);  //輸出 128
  }
void loop() {}

內建常數 :

Arduino 定義了八個內建常數 (注意, true 與 false 須小寫) :

 常數 說明
 HIGH 輸出高電位
 LOW 輸出低電位
 INPUT 輸入腳
 OUTPUT 輸出腳
 INPUT_PULLUP 啟動上拉電阻之輸入腳
 LED_BUILTIN 內建 LED (Pin 13)
 true 真 (=1)
 false 假 (=0)
 PI 圓周率 3.14159
 DEG_TO_RAD 角度轉弧度常數=PI/180=0.0174533
 RAD_TO_DEG 弧度轉角度常數=180/PI=57.29578

例如 LED 閃爍程式 :

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  }

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
  }

也可以用 const 自行定義常數 (但不可重複定義內建常數), 例如可定義常數 LED 以取代上內建的 LED_BUILTIN, 效果一樣 :

const int LED=13;

常數賦值後無法更改, 否則編譯時即會失敗. 此外常數名稱通常使用大寫, 但這只是習慣, 並非語法規則. 另外, Arduino 的 I/O 針腳預設是 INPUT, 因此只需要宣告哪些腳要當 OUTPUT 即可, pinMode(2, INPUT) 其實是多此一舉的.

陣列與字串 :

陣列可以先宣告再賦值 (使用大括號), 例如 :

int a[4];                   //宣告含有四個整數元素之陣列
a={1,2,3,4};            //陣列賦值

或者元素逐一賦值 :

int a[4];
a[0]=1;
a[1]=2;
a[2]=3;
a[3]=4;

或者宣告同時賦值 :

int a[4]={1,2,3,4};   //宣告同時賦值

也可以不填元素數目, 由編譯器自動計算 :

int a[]={1,2,3,4};     //由編譯器自動計算

但多維陣列只能第一維不寫交給編譯器去算, 其他維必須填寫, 例如 :

int a[][2]={{0,0},{0,1},{1,0},{1,1}};   //這樣 OK
int a[][]={{0,0},{0,1},{1,0},{1,1}};     //這樣不 OK (編譯失敗)
int a[4][2]={{0,0},{0,1},{1,0},{1,1}}; //當然 OK

陣列之存取是透過以 0 起始的索引, 最後一個元素之索引為長度減 1, 必須注意不可超過範圍. 由於 Arduino IDE 的 C 編譯器不會檢查索引是否超出範圍 (但會檢查是否夠放), 因此良好的 coding 習慣是利用常數來表示陣列大小, 例如 :

void setup() {
  Serial.begin(9600);
  const int COUNT=4;        //使用常數設定陣列大小
  int a[COUNT]={1,2,3,4};  
  for (int i=0; i<COUNT; i++) {  //利用常數避免索引超限
    Serial.println(a[i]);
    }
  }
void loop() {}


如果把上述迴圈的 COUNT 改為 5, 程式仍編譯成功, 執行結果為 :

1
2
3
4
26624  (超限的索引 4 存取到陣列外之數值)

可知, 陣列宣告的元素個數可以比賦值的多, 但不能少 (亦即要夠放, 否則會編譯失敗). 沒有被賦值的元素會有預設值, 整數類是 0, char 是空字元, 浮點數則是 0.00, 例如 :

void setup() {
  Serial.begin(9600);
  int a[5]={1,2,3,4};   //5 個元素只賦值 4 個
  for (int i=0; i<5; i++) {
    Serial.println(a[i]);
    }
  }
void loop() {}

執行結果為 :
1
2
3
4
0  (未賦值=預設值)

Arduino 裡的字串可以用兩種方法來做, 一是用 C 語言本來就有的 char 類型的陣列; 二是用 Arduino IDE 0095 版後提供的 String 資料類型 (C 語言本身無此資料型別, 而是透過 string.h 函式庫提供). 使用 char[] 儲存字串時要注意, 必須在陣列結尾加一個 '\0' (NULL) 字元 (直接用整數 0 也是可以的), 這是字串的結束符號 :

char a[5]={'A','B','C','D','\0'};   //四個字元外加一個 NULL
char a[5]={'A','B','C','D', 0};

C 語言中字元陣列與字串的差別如下圖所示, C 是利用字元陣列後面另加一個 NULL 來表示字串的 :



字串變數也可以直接用字串常值 (Literal) 來賦值 (注意, Arduino 中, 字元須用單引號, 字串須用雙引號括起來, C 語言則沒有字串), 如果自行指定長度, 必須比資料字元數多 1, 否則編譯失敗, 要不然乾脆不要指定, 由編譯器處理就好 :

char a[5]="ABCD";    //自行指定陣列大小, 須比資料字元多 1
char a[]="ABCD";      //由編譯器自動計算陣列大小

例如 :

void setup() {
  Serial.begin(9600);
  char a[]="ABCD";       //由編譯器自動計算陣列大小
  Serial.println(sizeof(a));     //輸出 5, 多一個 byte 存放結尾的 NULL (0)
  for (int i=0; i<sizeof(a); i++) {
    Serial.print(a[i]);      //輸出 ABCD
    }
  }
void loop() {}

此處使用了 Arduino 內建函數 sizeof() 來計算陣列的長度. 注意, sizeof() 事實上是在計算資料所占的總 byte 數, 因為 char 一個字元就是 一個 byte, 因此剛好就是字元陣列的長度. 如果關掉 Arduino 序列埠監視視窗, 改用 AccessPort 來觀察串列埠, 可知編譯器確實自動在陣列尾加上 NULL :



如果是浮點數陣列, 一個浮點數占 4 個 byte, 則 sizeof() 就不能用來當作陣列長度了, 例如 :

void setup() {
  Serial.begin(9600);
  float a[]={1.0, 2.0, 3.0, 4.0};
  Serial.println(sizeof(a));    //輸出 16, 4*4=16
  for (int i=0; i<sizeof(a); i++) {
    Serial.print(a[i]);   //輸出 1.002.003.004.000.00-0.000.000.000.000.000.000.000.000.00ovfovf
    }
  }
void loop() {} 

若要正確取得陣列長度, sizeof() 須除以資料類型的長度, 例如浮點數 sizeof(float) 會傳回 4 :

void setup() {
  Serial.begin(9600);
  float a[]={1.0, 2.0, 3.0, 4.0};
  Serial.println(sizeof(a)/sizeof(float));   //輸出 4
  for (int i=0; i<sizeof(a)/sizeof(float); i++) {
    Serial.print(a[i]);    //輸出 1.002.003.004.00
    }
  }
void loop() {}

二維陣列範例如下 :

void setup() {
  Serial.begin(9600);
  int a[][2]={{0,0},{0,1},{1,0},{1,1}};
  Serial.println(sizeof(a));  //輸出 16
  Serial.println(sizeof(a)/sizeof(int));  //輸出 8
}
void loop() {}

總之, sizeof() 若要用來取得陣列長度, 只能用在 byte, char, boolean 這三個資料長度為 8 位元的陣列上

一般變數當作引數傳入函數時是傳值呼叫, 亦即函數中會複製該變數進行運算, 不影響原值. 陣列也可以當作引數傳入函數中, 但不是用傳值呼叫, 而是傳址呼叫, 是將陣列頭的位址傳給函數, 因此會影響原陣列之值. 除了傳陣列位址外, 還要傳入陣列大小, 參考碁峰楊明豐 "Arduino 最佳入門與應用" ˇ3-7-3 節範例 :

void setup() {
  Serial.begin(9600);
  int a[]={1,2,3,4};
  Serial.println(sum(a,4));  //輸出 10
  }
int sum(int a[], int size) {  //傳入陣列位址與大小
  int sum=0;
  for (int i=0; i<size; i++) {
    sum += a[i];
    }
  return sum;
  }
void loop() {}

OK, 回到字串主題, 字串也可以組成陣列, 當然可以用兩層有 NULL 的字元陣列來表示, 例如 :

char users[][5]={{'P','e','t','e','r','\0'},{'A','m','y','\0'},{'K','e','l','l','y','\0'}};

注意, 這裡第二維必須填上各字串中最長字元數 (不含 NULL), 第一維 (字串數) 則可不填, 編譯器會自動設定. 當然, 若第一維填入比最長字串還長的數也是可以的, 編譯器只讀到 NULL 便停止, 後面多出來的字元 (存的是無法預測的值) 其實是浪費的.

比較方便的賦值方式是直接用字串常數賦值 :

char users[][5]={"Peter","Amy","Kelly"};

Arduino 在 0019 版後加入了 String 型別 (其實是一種物件), 此函式庫提供許多字串函數使字串處理更方便, 參考 :

https://www.arduino.cc/en/Reference/StringObject

也可以參考 C 語言的 string.h :

# C 語言標準函數庫分類導覽 - 字串處理 string.h


 字串函數 說明
 length() 傳回字串長度
 indexOf(val), indexOf(val, from) 從左方搜尋子字串, 傳回首次出現位置索引
 lastIndexOf(val), lastIndexOf(val, from) 從右方搜尋子字串, 傳回首次出現位置索引
 substring(from), substring(from, to) 傳回子字串
 replace(substr1, substr2) 將子字串 substr1 以子字串 substr2 取代
 concat(str) 將 str 字串串接在後面
 remove(index), remove(index, count) 刪除索引 index 到結尾之字元, 或刪除指定字元數 count
 toLowerCase() 傳回轉成小寫後之字串
 toUpperCase() 傳回轉成大寫後之字串
 charAt(index) 傳回指定索引之字元
 setCharAt(index, c) 將字串索引 index 之字元以指定字元 c 取代
 equals(str) 是否與指定字串 str 雷同, 傳回 true/false
 equalsIgnoreCase(str) 是否與指定字串 str 相同 (不分大小寫), 傳回 true/false
 compareTo(str) 與 str 字串逐字比較 ASCII 字元, 雷同傳回 0, 在後傳回正值, 在前傳回負值 
 startsWith(str) 是否以指定字串 str 開頭, 傳回 true/false
 endsWith(str) 是否以指定字串 str 結尾, 傳回 true/false
 trim() 清除開頭與結尾的空白字元
 toInt() 將以數字開頭直到非數字字元之字串轉成長整數傳回
 toFloat() 將以數字開頭直到非浮點數字元之字串轉成浮點數傳回
 reserve(bytes) 要求保留指定 bytes 數之記

例如 :

void setup() {
  Serial.begin(9600);
  String str="Hello World!";
  Serial.println(str.length());    //輸出 12
  Serial.println(str.indexOf(" "));        //輸出 5 (有找到傳回索引)
  Serial.println(str.indexOf(" ", 6));    //輸出 -1 (沒找到傳回 -1)
  Serial.println(str.lastIndexOf("!"));  //輸出 11
  Serial.println(str.substring(6));         //輸出 World!
  Serial.println(str.substring(0,7));      //輸出 Hello W
  str.replace("World","Tony");          
  Serial.println(str);                              //輸出 Hello Tony!
  str.concat(" Good Day!");              
  Serial.println(str);                              //輸出 Hello Tony! Good Day!
  str.remove(16);                                  //從 Day 前面空格開始刪
  Serial.println(str);                              //輸出 Hello Tony! Good
  str.remove(11,5);                               //從 G 前面空格開始刪
  Serial.println(str);                              //輸出 Hello Tony!
  str.toLowerCase();                          
  Serial.println(str);                              //輸出 hello tony!
  str.toUpperCase();                  
  Serial.println(str);                              //輸出 HELLO TONY!
  Serial.println(str.charAt(1));             //輸出 E
  str.setCharAt(5,'+');                           //將空格改為 +
  Serial.println(str);                              //輸出 HELLO+TONY!
  String str2="Hello+Tony!";
  Serial.println(str.equals(str2));          //輸出 0
  Serial.println(str.equalsIgnoreCase(str2));    //輸出 1
  Serial.println(str.compareTo(str2));  //輸出 -32 (不同,  str 在前)
  Serial.println(str.compareTo("HELLO+TONY!"));   //輸出 0 (雷同)
  Serial.println(str.startsWith("HELLO"));   //輸出 1
  Serial.println(str.endsWith("TONY!"));     //輸出 1
  str=" HELLO ";
  str.trim();
  Serial.println(str.length());   //輸出 5 (已刪除前後空格)
  str="180 Days";
  Serial.println(str.toInt());     //輸出 180
  str="65.245KG";
  Serial.println(str.toFloat());  //輸出 65.25 (四捨五入到小數第二位)
  }
void loop() {}

注意, 以上字串處理函式是 Arduino C 專用, 一般 C 語言沒有這些函式. 另外, 字串串接在 Arduino C 可以像 Python/Javacript 那樣用 + 直接串接, 在傳統 C 語言要用 strcat() 函式或指標, 不能用 +, 參考 :

Arduino - 字串


流程控制與迴圈 :

這部分與 C 語言完全一樣, 值得注意的是 switch case 指令的 case 值只能用字元整數. 其他資料型態如字串, 浮點數, 布林值均不允許. 例如 :

switch (val) {
  case 1 :
     //do something (for 1)
     break;
  case 2 :
     //do something (for 2)
     break;
  case 3 :
  case 4 :
     //do something (for 3 or 4)
     break;
  default :
     //do something
     break;
  }

每一個 case 若為獨立處理方式, 必須用 break 斷開, 否則會連續執行下一個 case (稱為 fall-through), 例如上面 case 3 與 case 4 都會執行 case=4 的程式碼. Case 值為字元範例如下 :

switch (val) {
  case 'Y' :
     //do something
     break;
  case 'N' :
     //do something
     break;
  case '?' :
     //do something
     break;
  default :
     //do something
     break;
  }

在 "Beginning C for Arduino, 2nd edition" 這本書裡的第四章提到有範圍的 case, 使用刪節號 "..." 表示範圍, 例如 :

char grade;
int score;
switch (score) {
  case 0...59 :
     grade='F';
     break;
  case 60...69 :
     grade='D';
     break;
  case 70...79 :
     grade='C';
     break;
  case 80...89 :
     grade='B';
     break;
  case 90...99 :
     grade='A';
     break;
  default :
     break;
  }


指標 :

C 語言的指標在 Arduino C 也有提供, 在全華黃新華等著 "微電腦原理與應用" 的 3-5 節有稍微提到, 而 "Beginning C for Arduino, 2nd edition" 這本書的第八章則有詳細介紹. 指標專門用來儲存記憶體的位址以間接地存取變數的內容, 是四十多年來 C 語言能笑傲江湖的關鍵功能, 一般高階語言如 Java 並不提供, 少部分雖有指標功能, 但不像 C 的指標那麼自由, 可以對指標進行算術計算來操控記憶體, 參考 :

# Pointer (computer programming)

Arduino 所有的板子之 SRAM 都在 64KB 以下 (UNO/Nano/Pro mini 等板子所使用的 ATMega328 微控器只有 2KB), 因此 Arduino 的指標在 SRAM 記憶體中都是占 2 個 bytes 來儲存變數的位址,  16 bits 最高可定址 2^16=64K Bytes.

指標的宣告範例如下 :

int *ptrPrice;

指標名稱與一般變數命名規則相同, 但為了可讀性, 建議以 ptr 開頭. 其次, 指標宣告中要有一個星號, 通常放在指標名稱前面, 這是告訴編譯器, 這個名稱不是一般變數, 而是一個儲存位址用的指標. 星號不一定要緊貼指標名稱, 也可以這樣寫 :

int* ptrPrice;
int * ptrPrice;

而前面的資料類型 int 是指此指標所指位址之記憶體中儲存之資料型態. 取得一個變數的存放位址用 & 運算子, 而要存取指標的內容則用 * 運算子, 例如 :

void setup() {
  Serial.begin(9600);
  int price=100;
  int *ptrPrice;   //宣告一個指標變數 ptrPrice 儲存位址, 所指位址存放 int 數值
  *ptrPrice=99;
  Serial.print("The address of ptrPrice=");   //顯示指標變數本身位址
  Serial.println((long)&ptrPrice);  //輸出 2294 (因板子而異)
  Serial.print("The content of ptrPrice=");   //顯示指標變數內容 (是個位址)
  Serial.println((long)ptrPrice);    //輸出 25344 (因板子而異)
  Serial.print("The address of price=");       //顯示一般變數之位址
  Serial.println((long)&price);      //輸出 2996 (因板子而異)
  Serial.print("The content of price=");       //顯示一般變數內容
  Serial.println((int)price);            //輸出 100
  Serial.print("The content of *ptrPrice=");  //顯示指標變數所指位址之內容
  Serial.println((int)*ptrPrice);     //輸出 99
  }
void loop() {}

此範例中各變數之記憶體位址關係可用下列圖表示 :


可知指標變數 ptrPrice 存放在記憶體位址 2294, 其內容是一個位址 (整數值), 指向另一個記憶體位址 25344, 其內容為 99; 而一般變數 price 則存放在記憶體位址 2296, 其內容是個整數值 100.

參考 :

Arduino 程式設計
# Arduino : 關於記憶體二三事
# Arduino 筆記 – 認識 Arduino


23 則留言:

  1. 或者元素逐一賦值 :

    int a[4];
    a[0]=1;
    a[1]=2;
    a[2]=3;
    a[3]=4;

    如果直接在Arduino的IDE上這樣key的結果是"a does not name a type"

    最後我是把程式分成在
    int a[4];

    void main{
    a[0]=1;
    a[1]=2;
    a[2]=3;
    a[3]=4;
    }
    才順利完成驗證,不知道問題原因?
    有沒有其他更好的寫法?
    感謝大大!

    回覆刪除
  2. 感謝,幫助我解決問題!

    回覆刪除
  3. 請問,Arduino ide 1.0.5-r2 輸入.寫入程式後,驗證時出現' 'does not name a type

    回覆刪除
  4. 因為沒有貼您的程式碼, 我猜您是每給 global variable 全域變數宣告資料型態才會這樣. 例如 :
    a=1;
    void setup() { }
    void loop() { }
    就會出現 'a' does not name a type 錯誤.
    C 語言是靜態語言, 每一個變數都有其資料型態, 一定要宣告後才能使用, 只要改為 :
    int a=1;
    就可以了.

    回覆刪除
  5. 太感謝了,您的文章讓我受益良多。

    回覆刪除
  6. 你好請問
    你知道哪裡可以有人為人解釋arduino code的嗎, 付費諮詢的也可以 ,
    有一組手電同程式很想做來實驗 ,但看不懂程式 ,國外公開的網站的分享沒有版權問題
    麻煩你 無限感激
    聯絡:eddie5492001t@yahoo.com.tw

    回覆刪除
  7. Dear Ed, 我不知道是否有解讀程式碼這種服務, 但如果方便程式又不會太長的話可以貼到這裡, 我如果有時間又看得懂的話可以略盡棉薄之力, 或許其他網友看到也會解析. 如程式太長, 可 mail : tony1966@ms5.hinet.net, 看看我能否讀懂.

    回覆刪除
  8. 真是感謝你,可以讓我完全實驗

    回覆刪除
  9. 我將code寄到你信箱了,它上面一行沒幾個字,可是很多行,要三張a4紙才放的下
    Thank you

    回覆刪除
  10. 可以請教你個問題嗎?我是剛開始學arduino,我買了一塊arduino uno r3的板子,但是一直無法寫入程式,出現的錯誤訊息是這樣的avrdude: stk500_getsync() not in sync: resp=0x00
    請問我要從哪查起,版型跟com都確認沒錯,謝謝

    回覆刪除
  11. 您好, 這可能是 IDE 程式沒有抓到 Arduino 板子, 要先到 工具/開發板 選取 Arduino/Genuino UNO 板與 COM 埠才行.

    回覆刪除
  12. 你好!我想請教您有關陣列的相關問題。我是用Genuino UNO板子。
    int APIN0 = A0; //定義腳位
    int APIN1 = A1;
    int APIN2 = A2;
    int APIN3 = A3;
    int APIN4 = A4;

    int a[5]; //儲存五個感測器的數值
    a[0] = A0;
    a[1] = A1;
    a[2] = A2;
    a[3] = A3;
    a[4] = A4;

    int i=(a[0]+a[1]+a[2]+a[3]+a[4])/5;

    A0~A4為數位腳讀取的數值,請問可以這樣寫嗎?因為我需要算這5個腳作的平均值然後讓,但是一直失敗不知道是不是陣列不能這樣撰寫。謝謝

    回覆刪除
  13. 讀取類比輸入腳要呼叫 analogRead() 函數喔!
    A[0]=analogRead(A0);
    A[1]=analogRead(A1);
    ....
    參考 :
    http://yhhuang1966.blogspot.com/2015/10/arduino.html

    回覆刪除
  14. 好的我知道了!謝謝您的指導

    回覆刪除
  15. 請問用Lora1傳送三組電壓給另一台Lora2,
    Lora2收到一組字串+RCV=20,20,458456433,我分別要458、456、433,然後再把458+456+433加起來做平均,要如何寫?

    回覆刪除
  16. 抱歉, 我 Lora 還沒時間學習, 您的問題應該是如何剖析收到的字串吧? 458456433 傳送時應該用符號隔開, 這樣收到後才能做字串處理.

    回覆刪除
  17. 問一下~如果是我想用測距算出來的資料轉成用7段顯示器顯示~那該如何使用語法去讀取算出來的資訊

    回覆刪除
  18. Hi, 您可以將類比輸入取得的整數, 例如 1023, 用 char() 強制轉型為字元後, 以索引或 charAt() 逐一取出, 再轉碼輸出到四顆 LED, 參考 :
    https://atceiling.blogspot.com/2019/08/arduino517-led_12.html

    回覆刪除
  19. 1.arduino語法中輸出小數點後第二位的是?
    2.arduino語法中, 可以輸出的是?
    3.arduino語法中, 可以讀取資料的是?
    4.arduino語法中, 可以寫入位元的是?

    回覆刪除
  20. 請問大大,該本文'定義常數: #define PI 3.14159'的內容,
    我在其他程式上有看到將#define後面的置換值(常數值:3.14159)加上了一個括號'#define PI (3.14159)',
    那加上括號會有改變原有表示嗎?還是不影響呢?

    回覆刪除
  21. Hi, 前置指令 #define 用來定義一個常數或運算式的值, 如果是定義常數, 有無括號都可以, 例如 #define PI 3.14159 與 #define PI (3.14159) 是相同意思; 但如果拿來當運算式, 作用相當於一個簡單型函式, 這時有沒有括號就大不同了, 參考 : https://blog.csdn.net/linux12121/article/details/52602633

    回覆刪除