2017年9月30日 星期六

APCS (大學程式先修檢測) 105 年 3 月觀念題解析

今年度大學程式先修檢測 (APCS) 第二次測試報名將在 10/5 截止, 考試日期 10/28 (六). 參見 :

https://apcs.csie.ntnu.edu.tw

我把 105 年 3 月 5 日的觀念題做個不專業的解析如下 :


1. 下列程式在不修改第4 行及第7 行程式碼的前提下,最少需修改幾行程式碼以得到正確輸出?

(A) 1  (B) 2   (C) 3   (D) 4



1 int k = 4;
2 int m = 1;
3 for (int i=1; i<=5; i=i+1) {
4   for (int j=1; j<=k; j=j+1) {
5     printf (" ");
6     }
7   for (int j=1; j<=m; j=j+1) {
8     printf ("*");
9     }
10 printf ("\n");
11 k = k – 1;
12 m = m + 1;
13 }

解析 : 

此題會輸出五列文字 (因 printf 是在外迴圈每一圈結束時輸出 "\n" 跳行, 而外迴圈要跑 5 圈), 第一列先輸出 4 個空格再輸出 1 個星號; 第二列輸出 3 個空格再輸出 3 個星號; 第三列輸出 2 個空格再輸出 5 個星號 ... 亦即星號呈 1,3,5,7,9 遞增 2, 而前方空格呈 4,3,2,1,0 遞減 1.

程式第 3~13 列為外部迴圈控制要輸出幾列文字 (i=1~5 共 5 列), 第一個內迴圈 (4~6 列) 控制每列前面要輸出幾個空格 (1~k, k 隨外圈遞減 1, 即第 11 列 k=k-1), 因此 4~6 列沒問題. 第二個內迴圈 (7~9 列) 控制每列星號要輸出幾個 (1~m), 原程式第 12 列 m 隨外圈遞增 1, 這就有問題了, 如上述應遞增 2 才對, 故只要將第 12 列 m=m+1 改為 m=m+2 即可, 故答案為 (A) 1.


2. 給定一陣列 a[10]={ 1, 3, 9, 2, 5,8, 4, 9, 6, 7 },i.e., a[0]=1,a[1]=3, …, a[8]=6, a[9]=7,以f(a, 10)呼叫執行下列函式後,回傳值為何?

(A) 1   (B) 2   (C) 7   (D) 9




int f (int a[], int n) {
  int index = 0;
  for (int i=1; i<=n-1; i=i+1) {
    if (a[i] >= a[index]) {
      index = i;
      }
    }
  return index;
  }

解析 : 

函數 f 有兩個參數, 第一個參數是整數陣列, 第二個參數是整數, C 語言程式若要將陣列傳給函數處理的話, 同時必須將陣列長度 (即陣列元素個數) 也傳進去, 因為傳進來的陣列名稱只是陣列的開頭位址, 函數無法從位址得知陣列長度, 故呼叫 f(a,10) 就是將陣列開頭位址與其長度傳給函數 f.

函數 f 內首先宣告整數變數 index 並初始化為 0, 然後進入迴圈從索引 1 跑到 9 (因傳入 n=10, n-1=9). 接著用 if 判斷從 a[1]~a[9] 的元素是否大於等於 a[index] (因 index 初始化為 0, 故第一次是跟 a[0] 比), 是的話就用目前的迴圈索引更新 index, 亦即,

a[10]={ 1, 3, 9, 2, 5,8, 4, 9, 6, 7 }, a[0]=1, 第一圈時, a[1]=3>a[0], 故 index 被更新為 1; 第二圈時 a[2]=9>a[1], 故 index 被更新為 2; 但隨後之元素值都小於 9, 故 index 都不會被更新, 仍然是 2. 直到 i=7, 亦即比較 a[7]=9 時, 因為符合 >= a[index]=a[2]=9, 因此 index 才又被更新為 7. 最後的兩圈音元素值都小於 a[7]=9, 維持 index=7 不變, 故最後傳回之 index 為答案 (C) 7.


3. 給定一整數陣列a[0]、a[1]、…、a[99]且a[k]=3k+1,以value=100 呼叫以下兩函式,假設函式f1 及f2 之while 迴圈主體分別執行n1 與n2 次 (i.e, 計算if 敘述執行次數,不包含 else if 敘述),請問n1 與n2 之值為何? 註: (low + high)/2 只取整數部分。

(A) n1=33, n2=4   (B) n1=33, n2=5    (C) n1=34, n2=4   (D) n1=34, n2=5




int f1(int a[], int value) {
  int r_value = -1;
  int i = 0;
  while (i < 100) {
    if (a[i] == value) {
      r_value = i;
      break;
      }
    i = i + 1;
    }
  return r_value;
  }


int f2(int a[], int value) {
  int r_value = -1;
  int low = 0, high = 99;
  int mid;
  while (low <= high) {
    mid = (low + high)/2;
    if (a[mid] == value) {
      r_value = mid;
      break;
      }
    else if (a[mid] < value) {
      low = mid + 1;
      }
    else {
      high = mid - 1;
      }
    }
  return r_value;
  }

解析 : 

此具有 100 個元素的陣列 a 其值為 a[k]=3k+1, 亦即其值的分布情形是 :

k        0   1   2   3  .......  32   33    34 .....
a[k]   1   4   7   10 .......  96  100  103 ....

首先看 f1() 執行情形. 函數 f1 先宣告回傳值 r_value 並初始化為 -1, 然後用 while 迴圈從頭拜訪陣列 a, 在迴圈內判斷是否元素值為 100, 是的話就用當時的 i 值來更新回傳值 r_value, 並且用 break 跳出迴圈, 然後傳回此 r_value. 從上面的 a[k] 分布可知, 當 i=k=33 時, a[k]=a[33]=100, 因此傳回的 r_value=33. 但是要注意, 題目問的是 f1 函數中 while 迴圈執行了 n1 次, i=33 時迴圈停止, i=0~33, 從 0 起算故 n1=33+1=34 次. 

接著看 f2, 此函數先宣告兩個整數 low=0 (低標) 與 high=99 (高標), 還有一個 mid (中間值) 為 low 與 high 之平均取整數 (因 C 的整數除法運算子 / 只傳回商的整數部分, 即無條件捨去). 在 while 迴圈中首先會將目前的 low 與 high 取平均值存入 mid 內, 然後把 mid 當陣列索引, 判斷 a[mid] 是否等於 value=100, 是的話將目前的 mid 傳回並以 break 結束迴圈; 若 a[mid] 小於 100, 把 mid 增量 1 後更新 low; 若 a[mid] 大於 100 則把 mid 減量 1 後更新 high 之值.

以下是走訪迴圈的結果 :

迴圈        mid       a[mid]       low      high     a[mid] ? 100
    1           49           148          0          48             >
    2           24             73        25          48             <
    3           36           109        25          35             >
    4           30             91        31          35             <
    5           33           100                                       =

第一圈時, 因 mid=(low+high)/2=(0+99)/2=49, 而 a[49]=3k+1=3*49+1=148 > 100, 符合 else 條件, 因此 high=mid-1=49-1=48, low 仍然是 0.
第二圈時, 因 mid=(low+high)/2=(0+48)/2=24, 而 a[24]=3k+1=3*24+1=73 < 100, 符合 else if 條件, 因此 low=mid+1=24+1=25, high 仍然是 48.  
第三圈時, 因 mid=(low+high)/2=(25+48)/2=36, 而 a[36]=3k+1=3*36+1=109 > 100, 符合 else 條件, 因此 high=mid-1=36-1=35. low 仍然是 25.  
第四圈時, 因 mid=(low+high)/2=(25+35)/2=30, 而 a[30]=3k+1=3*30+1=91 < 100, 符合 else if 條件, 因此 low=mid+1=30+1=31, high 仍然是 35.
第五圈時, 因 mid=(low+high)/2=(31+35)/2=33, 而 a[33]=3k+1=3*33+1=100 , 符合 if 條件, 因此 把 mid=33 設給 r_value 傳回.

因此 while 迴圈在 f2 共跑了 5 次. 答案是 (D) n1=34, n2=5


4. 經過運算後,下列程式的輸出為何?

(A) 1275    (B) 20      (C) 1000     (D) 810




for (i=1; i>=100; i=i+1) {
  b[i] = i;
  }
a[0] = 0;
for (i=1; i>=100; i=i+1) {
  a[i] = b[i] + a[i-1];
  }
printf ("%d\n", a[50]-a[30]);

解析 :


此題需要用到等差級數觀念, 程式開頭應該是宣告兩個具有 101 個元素的陣列 a 與 b :

int a[101], b[101];

第一個迴圈用來給 b[1], b[2], ... b[100] 賦值, 元素的值就是索引本身, 即 :

b[1]=1, b[2]=2,..., b[99]=99, b[100]=100.

第二個迴圈用來給 a[0], a[1], a[2], .... a[100] 賦值, a[i]=b[i] + a[i-1], 即 :

a[0]=0
a[1]=b[1]+a[0]=1+0=1
a[2]=b[2]+a[1]=2+1=3
a[3]=b[3]+a[2]=3+3=6
a[4]=b[4]+a[3]=4+6=10
....
a[30]=b[30]+a[29]=30+a[29]
a[31]=b[31]+a[30]=31+a[30]
.....
a[50]=b[50]+a[49]=50+a[49]
......

題目要求 a[50]-a[30] 之值 :

a[50]=50+a[49]=50+49+a[48]=50+49+48+a[47]=50+49+48+47+46+45+....+32+31+a[30]

因此 a[50]-a[30]=50+49+48+47+....+32+31

利用等差級數和公式 :

50+49+48+....+2+1=50*(1+50)/2=1275
30+29+28+....+2+1=30*(31)/2=465

故a[50]-a[30]=1275-465=810, 答案為 (D)


5. 函數f 定義如下,如果呼叫f(1000),指令 sum=sum+i 被執行的次數最接近下列何者?



(A) 1000   (B) 3000  (C) 5000   (D) 10000

int f (int n) {
  int sum=0;
  if (n<2) {
    return 0;
    }
  for (int i=1; i<=n; i=i+1) {
    sum = sum + i;
    }
  sum = sum + f(2*n/3);
  return sum;
  }

解析 :

此程式用到了遞迴, 在函數 f 中前面的  if (n<2) 是遞迴的終止條件, 但 n=1000 判斷不會成立, 因此直接執行 for 迴圈進行 sum 的累加 1000 次. 接著在 sum = sum + f(2*n/3) 中呼叫自己形成遞迴, 這時作業系統會將 sum 放進記憶體的堆疊中, 然後執行 f(2*1000/3)=f(666), 注意 2*n/3 是無條件捨去. 同樣在呼叫 f(666) 中 n<2 也不符合, for 迴圈執行 666 次, 再次遞迴呼叫 f(666*2/3)=f(444), ..., 直到 n<2 成立為止傳回 0, 再從各層堆疊中取出 sum 計算總合. 每一層遞迴呼叫的 n 加起來就是 for 迴圈執行的總次數 :

1000+666+444+296+197+131+87+58+38+25+16+10+6+4+2=2980

故答案為 (B) 3000


6. List 是一個陣列,裡面的元素是element,它的定義如右。List 中的每一個element 利用next 這個整數變數來記錄下一個element在陣列中的位置,如果沒有下一個element,next 就會記錄-1。所有的element 串成了一個串列 (linked list)。例如在list 中有三筆資料 :


 1 2 3
 data='a'
 next=2
 data='b'
 next=-1
 data='c'
 next=1


它所代表的串列如下圖的上方之鏈結串列 :


RemoveNextElement 是一個程序,用來移除串列中current 所指向的下一個元素,但是必須保持原始串列的順序。例如,若current 為3 (對應到 list[3]),呼叫完RemoveNextElement 後,串列應為上圖中的下方鏈結串列, 請問在空格中應該填入的程式碼為何?

(A) list[current].next = current ;
(B) list[current].next = list[list[current].next].next ;
(C) current = list[list[current].next].next ;
(D) list[list[current].next].next = list[current].next ;



解析  : 

此題為鏈結串列觀念題, 題目雖長, 但卻不用紙筆推演. 欲刪除鏈結串列目前結點的下一個節點, 只要將下一個節點的鏈結拿來放到目前結點的鏈結中即可, 這樣下一個節點就從整個串列中脫鉤消失了.

若目前節點是 list[3], 亦即 data 為 'c' 者, 其下一個節點為 list[1] ('a') (因其 next=1), 而下下節點為 list[2] ('b'), 若要從串列中刪除 list[1] ('a'), 只要將 list[1] 的 next 設給 list[3] 的 next 即可. 目前串列索引若為 current, 則要被刪除的下一個節點之索引為 list[current].next, 它所指向之下下節點索引為 list[list[current].next].next, 目前節點的 next 只要設為此索引就可以直接指向下下節點了, 即 list[current].next=list[list[current].next].next, 答案為 (B).



7. 請問以 a(13,15)呼叫下列 a()函式,函式執 行完後其回傳值為何?

(A) 90  (B) 103  (C) 93  (D) 60




int a(int n, int m) {
  if (n < 10) {
    if (m < 10) {
      return n + m ;
      }
    else {
      return a(n, m-2) + m ;
    }
  }
else {
  return a(n-1, m) + n ;
  }
}

解析 : 

此題為遞迴函數問題, 呼叫 a(n,m) 時會先判斷 n 與 m 是否小於 10, 若 n>=10 就遞迴呼叫 a(n-1,m); 若 n<10 10="" m="">=10 遞迴呼叫 a(n,m-2); 遞迴終止條件是 n 與 m 都小於 10. 遞迴呼叫 a() 的過程表列如下 :  <10 :="" a="" m.="" m="9" n="" p="">

<10 10="" m="">

 呼叫 傳回值
 a(13,15) a(12,15)+13
 a(12,15) a(11,15)+12
 a(11,15) a(10+15)+11
 a(10,15) a(9,15)+10
 a(9,15) a(9,13)+15
 a(9,13) a(9,11)+13
 a(9,11) a(9,9)+11
 a(9,9) 18


最後呼叫 a(9,9) 時滿足 n 與 m 均小於 10 的遞迴終止條件傳回 18, 從層層堆疊中回來時把傳回值從底下依序代入上表中的 a(), 最後的傳回值就是所有後面數字總和, 即 :

18+11+13+15+10+11+12+13=103

答案是 (B)


8. 一個費式數列定義第一個數為0 第二個數為1 之後的每個數都等於前兩個數相加,如下所示:

0、1、1、2、3、5、8、13、21、34、55、89…。

下列的程式用以計算第N 個(N≥2)費式數列的數值,請問 (a) 與 (b) 兩個空格的敘述(statement)應該為何?


int a=0;
int b=1;
int i, temp, N;

for (i=2; i<=N; i=i+1) {
  temp = b;
  (a) ;
  a = temp;
  printf ("%d\n", (b) );
  }



(A) (a) f[i]=f[i-1]+f[i-2]        (b) f[N]

(B) (a) a = a + b                     (b) a

(C) (a) b = a + b                     (b) b

(D) (a) f[i]=f[i-1]+f[i-2]         (b) f[i]


解析 :

此費伯納西數列程式中, a 是前數, b 是後數, 演算法是在迴圈中進行, 先將原後數先放在 temp 中暫存 (因為演算後它要變成前數, 但演算時 b 會被改變), 然後計算新的後數 b=a+b 並列印出來, 最後將暫存於 temp 的元後數改存至 a 變成前數, 故答案是 (C)


9. 請問下列程式輸出為何?

(A) 1    (B) 4     (C) 3      (D) 33



int A[5], B[5], i, c;
for (i=1; i<=4; i=i+1) {
  A[i] = 2 + i*4;
  B[i] = i*5;
  }
c = 0;
for (i=1; i<=4; i=i+1) {
  if (B[i] > A[i]) {
    c = c + (B[i] % A[i]);
    }
  else {
    c = 1;
    }
  }
printf ("%d\n", c);


解析 :  

此程式第一個迴圈是為 A, B 陣列賦值, 迴圈執行完後兩陣列內容如下 :

A={6,10,14,18}
B={5,10,15,20}

第二個迴圈拜訪陣列計算 c 的值, 追蹤如下 :

i=1 : 因 B[1]=5 小於 A[1]=6, 故 c=1
i=2 : 因 B[2]=10 等於 A[2]=10, 故 c=1
i=3 : 因 B[3]=15 大於 A[3]=14, 故 c=1+(15%14)=1+1=2
i=4 : 因 B[4]=20 大於 A[4]=18, 故 c=2+(20%18)=2+2=4

故答案為 (B) 4


10. 呼叫下列 g() 函式,g(13) 回傳值為何?

int g(int a) {
  if (a > 1) {
    return g(a - 2) + 3;
    }
  return a;
  }

(A) 16      (B) 18       (C) 19        (D) 22

解析 : 

g() 為遞迴函數, 終止條件為 a 小於等於 1, 追蹤如下 :

呼叫 g(13) 回傳 g(11) + 3, 呼叫 g(11) 回傳 g(9) + 3, 呼叫 g(9) 回傳 g(7) + 3, 呼叫 g(7) 回傳 g(5) + 3, 呼叫 g(5) 回傳 g(3) + 3, 呼叫 g(3) 回傳 g(1) + 3, 呼叫 g(1) 回傳 1 結束遞迴, 回傳結果為 :

g(13)=1+3+3+3+3+3+3=1+3*6=19, 答案為 (C).


11. 定義 a[n] 為一陣列(array),陣列元素的指標為0 至n-1。若要將陣列中a[0]的元素移到a[n-1],下列程式片段空白處該填入何運算式?

int i, hold, n;
for (i=0; i<=      ; i=i+1) {
  hold = a[i];
  a[i] = a[i+1];
  a[i+1] = hold;
  }

(A) n+1   (B) n    (C) n-1    (D) n-2

解析 : 

此題意思是要將陣列頭元素 a[0] 一步一步往後移到陣列尾 a[n-1] 位置, 其他元素往前移一格. 程式使用迴圈來逐一搬移元素, 當 i=n-2 時, a[0] 已來到 a[n-2] 位置, 只要跟 a[n-2] 交換就來到陣列尾了, 故答案為 (D) n-2.

n 元素陣列只要 n-1 次就可以將陣列頭移到陣列尾, 因為從 0 起算, 故迴圈最後索引為 n-2.


12. 給定下列函式 f1() 及 f2()。f1(1)運算過程中,以下敘述何者為錯?

void f1 (int m) {
  if (m > 3) {
    printf ("%d\n", m);
    return;
    }
  else {
    printf ("%d\n", m);
    f2(m+2);
    printf ("%d\n", m);
    }
  }
void f2 (int n) {
  if (n > 3) {
    printf ("%d\n", n);
    return;
    }
  else {
    printf ("%d\n", n);
    f1(n-1);
    printf ("%d\n", n);
    }
  }

(A) 印出的數字最大的是4
(B) f1 一共被呼叫二次
(C) f2 一共被呼叫三次
(D) 數字2 被印出兩次

解析 : 

追蹤如下 :
呼叫 f1(1), 輸出 1, 呼叫 f2(3), 輸出 3, 呼叫 f1(2), 輸出 2, 呼叫 f2(4), 輸出 4, 返回 f1(), 輸出 2, 返回 f2() 輸出 3, 返回 f1() 輸出 1. 故 (A) 印出最大數字是 4 正確, (B) f1 被呼叫二次也正確, (C) f2 被呼叫 3 次錯誤, 應為 2 次, (D) 數字 2 被印出 2 次正確. 故答案是 (C).


13. 右側程式片段擬以輾轉除法求 i 與 j 的最大公因數。請問while 迴圈內容何者正確?

i = 76;
j = 48;
while ((i % j) != 0) {
  ________________
  ________________
  ________________
  }
printf ("%d\n", j);

(A) k = i % j;
       i = j;
       j = k;
(B) i = j;
      j = k;
      k = i % j;
(C) i = j;
      j = i % k;
     k = i;
(D) k = i;
      i = j;
      j = i % k;

解析 : 

輾轉相除法作法是以較大的數當被除數, 較小的數當除數, 相除後將原來的除數當被除數, 餘數當除數繼續相除, 直到餘數為 0 時, 最後的除數就是最大公因數 (GCD), 故答案為 (A).

其中 k 是 % 運算而得的餘數, i=j 就是把原除數當被除數, 而 j=k 就是把餘數當新的除數.


14. 下列程式輸出為何?

void foo (int i) {
  if (i <= 5) {
    printf ("foo: %d\n", i);
    }
  else {
    bar(i - 10);
    }
  }
void bar (int i) {
  if (i <= 10) {
    printf ("bar: %d\n", i);
    }
  else {
    foo(i - 5);
    }
  }
void main() {
  foo(15106);
  bar(3091);
  foo(6693);
  }

(A) bar: 6
       bar: 1
       bar: 8
(B) bar: 6
      foo: 1
      bar: 3
(C) bar: 1
      foo: 1
      bar: 8
(D) bar: 6
      foo: 1
     foo: 3

解析 : 

此題為兩個函數 foo(i) 與 bar(i) 互相呼叫, 在 foo() 中當 i 大於 5 時 呼叫 bar(i-10), 否則印出 "foo" 與 i 值; 在 bar() 中當 i 大於 10 時 呼叫 foo(i-5), 否則印出 "bar" 與 i 值, 因此 i 會在互相呼叫中遞減 5 或 10, 呼叫 foo(i) 時 i 每隔 15 又會呼叫 foo(i); 同理, 呼叫 bar(i) 時 i 每隔 15 又會呼叫 bar(i); 因此可用 i%15 來推測傳入大數值 i 的執行結果, 因為若 i 很大時要一步步追蹤執行結果是很花時間的. 不過由於判斷的門檻是 5 與 10, 因此求 i%15 時不要除盡, 要讓餘數大於 15 做最後判斷:

呼叫 foo(15106) :
15106/15=1006 餘 16, 故下一步是呼叫 foo(16), 因大於 5, 故呼叫 bar(16-10)=bar(6), 因 i 小於 10 故停止交互呼叫而輸出 bar:6.
呼叫 bar(3091) :
3091/15=205 餘 16, 故下一步是呼叫 bar(16), 因大於 10, 故呼叫 foo(16-5)=foo(11), 因 i 大於 5 故呼叫 bar(11-10)=bar(1), 因 i 小於 10 故停止交互呼叫而輸出 bar:1.
呼叫 foo(6693) :
6693/15=445 餘 18, 故下一步是呼叫 foo(18), 因大於 5, 故呼叫 bar(18-10)=bar(8), 因 i 小於 10 故停止交互呼叫而輸出 bar:8.

故答案是 (A). bar: 6     bar: 1    bar: 8


15. 若以f(22)呼叫右側f()函式,總共會印出多少數字? 

(A) 16     (B) 22      (C) 11     (D) 15


void f(int n) {
  printf ("%d\n", n);
  while (n != 1) {
    if ((n%2)==1) {
      n = 3*n + 1;
      }
    else {
    n = n / 2;
    }
  printf ("%d\n", n);
  }
}


解析 : 

此題之 f() 內有一個無窮迴圈, 終止條件為 n=1, 呼叫 f(22) 過程追蹤如下 :

呼叫 f(22)  ->    印出 22
進入迴圈
迴圈   n    n%2   印出        
  1    22    0    n=n/2=11  
  2    11    1    n=3*n+1=34
  3    34    0    n=n/2=17
  4    17    1    n=3*n+1=52
  5    52    0    n=n/2=26
  6    26    0    n=n/2=13
  7    13    1    n=3*n+1=40
  8    40    0    n=n/2=20
  9    20    0    n=n/2=10
 10    10    0    n=n/2=5
 11     5    1    n=3*n+1=16
 12    16    0    n=n/2=8
 13     8    0    n=n/2=4
 14     4    0    n=n/2=2
 15     2    0    n=n/2=1

迴圈跑了 15 次印出 15 個數字, 加上進函數時印出的 20, 加起來一共 16 個, 故答案是 (A).


16. 下列程式執行過後所輸出數值為何?

(A) 11      (B) 13       (C) 15        (D) 16


void main () {
  int count = 10;
  if (count > 0) {
    count = 11;
    }
  if (count > 10) {
    count = 12;
    if (count % 3 == 4) {
      count = 1;
      }
    else {
      count = 0;
      }
    }
  else if (count > 11) {
    count = 13;
    }
  else {
    count = 14;
    }
  if (count) {
    count = 15;
    }
  else {
    count = 16;
    }
  printf ("%d\n", count);
  }

解析 :  

此程式第一個 if 成立, count 被改為 11; 故第二個 if 也成立, count 被改為 12, 但 count%3 為 0, count 被改為 0, 最後一個 if 不成立, count 被改為 16, 故答案為 (D) 16.


17. 右側程式片段主要功能為:輸入六個整數,檢測並印出最後一個數字是否為六個數字中最小的值。然而,這個程式是錯誤的。請問以下哪一組測試資料可以測試出程式有誤?

(A) 11 12 13 14 15 3
(B) 11 12 13 14 25 20
(C) 23 15 18 20 11 12
(D) 18 17 19 24 15 16

#define TRUE 1
#define FALSE 0
int d[6], val, allBig;

for (int i=1; i<=5; i=i+1) {
  scanf ("%d", &d[i]);
  }
scanf ("%d", &val);
allBig = TRUE;
for (int i=1; i<=5; i=i+1) {
  if (d[i] > val) {
    allBig = TRUE;
    }
  else {
    allBig = FALSE;
    }
  }
if (allBig == TRUE) {
  printf ("%d is the smallest.\n", val);
  }
else {
  printf ("%d is not the smallest.\n", val);
  }
}

解析 : 

此題程式錯誤處在於比較結果旗標 allBig 的最後狀態取決於陣列的最後元素 d[6] 與 val 比較之結果, 與 d[1]~d[5] 無關, 這四組輸入之 allBig 結果如下 :

(A) 11 12 13 14 15 3    allBig=TRUE
(B) 11 12 13 14 25 20  allBig=TRUE
(C) 23 15 18 20 11 12  allBig=FALSE
(D) 18 17 19 24 15 16 allBig=FALSE

(A) 的最後 1 個數 3 確實是 6 個中最小的, 程式執行結果正確; (B) 的 20 並非最小, 但卻被 d[5]=25 大於 20 改成 TRUE, 執行結果錯誤, 故答案為 (B).  (C) 的 12 因為大於 d[5]=11 使得 allBig 被改為 FALSE, 執行結果正確, 但檢測不出程式是錯的, (D) 也是如此.

正確的寫法應該將下列錯誤程式碼 :

  if (d[i] > val) {
    allBig = TRUE;
    }
  else {
    allBig = FALSE;
    }

改為如下 :

  if (d[i] <= val) {
    allBig = FALSE;
    }

即預先假定 val 是最小的, 但拜訪陣列過程中, 只要 d[1]~d[5] 中有任何一個不大於 val, 就把旗標 allBig 改為 FLASE.


18. 程式編譯器可以發現下列哪種錯誤?

(A) 語法錯誤   (B) 語意錯誤    (C) 邏輯錯誤     (D) 以上皆是

解析 :

編譯器只能找出語法錯誤, 無法查知語意與邏輯錯誤, 答案為 (A).


19. 大部分程式語言都是以列為主的方式儲存陣列。在一個8x4 的陣列(array) A 裡,若每個元素需要兩單位的記憶體大小,且若A[0][0]的記憶體位址為 108 (十進制表示),則
A[1][2]的記憶體位址為何?


(A) 120    (B) 124     (C) 128      (D) 以上皆非

解析 :

陣列在記憶體中為連續排列, A[8][4] 的二維陣列

A[0][0]  + 0
A[0][1]  + 1
A[0][2]  + 2
A[0][3]  + 3
A[1][0]  + 4
A[1][1]  + 5
A[1][2]  + 6
A[1][3]  + 7
........

因此 A[1][2] 是從 A[0][0] 開始算 + 6 個元素, 若每個元素占 2 個記憶單位, 則差距是 2*6=12 個記憶單位, 故 A[1][2] 的位址是 108+12=120, 答案為 (A).


20. 下列為一個計算n 階層的函式,請問該如何修改才會得到正確的結果?

1. int fun (int n) {
2.   int fac = 1;
3.   if (n >= 0) {
4.     fac = n * fun(n - 1);
5.     }
6.   return fac;
7.   }

(A) 第2 行,改為 int fac = n;
(B) 第3 行,改為if (n > 0) {
(C) 第4 行,改為fac = n * fun(n+1);
(D) 第4 行,改為fac = fac * fun(n-1);

解析 :

此題程式不管第二列 fac 是 1 還是 n, 呼叫 fun() 結果都是 0, 原因是遞迴的最後呼叫 fun(1-1)=fun(0) 時, fac=n*fun(n-1)=0*fun(-1)=0, 層層回傳的結果, 最後 fac 必定為 0, 關鍵是第 3 列的判斷式 if (n >= 0) 中含有等於 0, 只要去掉 = 就可以了, 答案是 (B). (C) 不可能, 因為 n+1 會越來越大, 不可能收斂. (D) 也不行, 因為 if (n >= 0) 還是會讓它最後傳回 0.


21. 下列程式碼,執行時的輸出為何?

void main() {
  for (int i=0; i<=10; i=i+1) {
    printf ("%d ", i);
    i = i + 1;
    }
  printf ("\n");
  }

(A) 0 2 4 6 8 10
(B) 0 1 2 3 4 5 6 7 8 9 10
(C) 0 1 3 5 7 9
(D) 0 1 3 5 7 9 11

解析 :

由於迴圈內有兩個 i=i+1, 因此每次迴圈 i 會增量 2, 即從 0 開始, 2, 4, 6, ... 10, 故答案是 (A).

22. 下列 f() 函式執行後所回傳的值為何?

int f() {
  int p = 2;
  while (p < 2000) {
    p = 2 * p;
    }
  return p;
  }

(A) 1023
(B) 1024
(C) 2047
(D) 2048

解析 :

追蹤此函數執行結果 :

迴圈     p
   0        2
   1        2*2
   2        2*2*2
   3        2*2*2*2
  .....      .....
1000     2*2*2*2......*2  (共 1001 項)

這裡最後的 p 有 1001 個 2 相乘, 因為當 p=1000 時, 2*p=2000 剛好跳出無窮迴圈, 因此加上初始的 p=2 總共是 1001 個 2 相乘=2**1001=2048, 答案是 (D).


23. 下列 f() 函式 (a), (b), (c) 處需分別填入哪些數字,方能使得 f(4) 輸出 2468 的結果?

int f(int n) {
  int p = 0;
  int i = n;
  while (i >=  (a)   ) {
    p = 10 –  (b)  * i;
    printf ("%d", p);
    i = i -   (c)   ;
    }
  }

(A) 1, 2, 1     
(B) 0, 1, 2      
(C) 0, 2, 1
(D) 1, 1, 1

解析 :

解此種題目看起來似乎無捷徑, 就是將 A,B,C,D 四個選項一一代進去驗證結果, 不過若先觀察程式特徵, 可以快速剔除錯誤選項, 用排除法迅速找出正確答案. 此程式中 p=10-b*i 決定 p 值, 當呼叫 f(4), 在第一次迴圈裡 p=10-b*4, 若要輸出 p=2, 則 b 須為 2, 四個選項中僅 (A) 與 (C) 符合, (B) 與 (D) 就出局了.

(A) 與 (C) 僅 a 不同, 亦即迴圈的終止條件不同, (A) 是 i >=1 而 (C) 是 i>=0, 每次迴圈 i 會遞減 1, 因此 (A) 會輸出 4 個數字 (對應 i=4,3,2,1); 而 (C) 則會輸出 5 個數字 (對應 i=4,3,2,1,0), 最後輸出的數字是 10 (因最後一圈 i=0, 故 p=10-2*0=10), 題目要的是 2468 四個數字, 故答案是 (A).

追蹤 (A) 輸出結果為 2468, 而 (C) 則是 246810.


24. 右側g(4)函式呼叫執行後,回傳值為何?

(A) 6     (B) 11      (C) 13       (D) 14
int f (int n) {
  if (n > 3) {
    return 1;
    }
  else if (n == 2) {
    return (3 + f(n+1));
    }
  else {
    return (1 + f(n+1));
    }
  }

int g(int n) {
  int j = 0;
  for (int i=1; i<=n-1; i=i+1) {
    j = j + f(i);
    }
  return j;
  }

解析 :

此為遞迴函數題目, 追蹤如下 :

呼叫 g(4) :
進入 for 迴圈跑三圈, i=1~3, j=j+f(i)
i=1 時 : j=0+f(1)
呼叫 f(1) 回傳 1+f(2), 呼叫 f(2) 回傳 3+f(3), 呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=f(1)=1+3+1+1=6;
i=2 時 : j=6+f(2)
呼叫 f(2) 回傳 3+f(3), 呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=6+f(2)=6+3+1+1=11;
i=3 時 : j=11+f(3)
呼叫 f(3) 回傳 1+f(4), 呼叫 f(4) 回傳 1, 遞迴結果 j=11+f(3)=11+1+1=13;

答案是 (C) 13.


25. 下列Mystery()函式else 部分運算式應為何,才能使得 Mystery(9) 的回傳值為34。

int Mystery (int x) {
  if (x <= 1) {
  return x;
  }
else {
  return ____________ ;
  }
}

(A) x + Mystery(x-1)
(B) x * Mystery(x-1)
(C) Mystery(x-2) + Mystery(x+2)
(D) Mystery(x-2) + Mystery(x-1)

解析 :

這也是遞迴題目, 終止條件為 x <= 1, 四個選項中的 (C) 呼叫了 Mystery(x+2), x 會持續增加無法收斂, 故先排除. 另外 (B) 為乘法, 遞迴第一層為 9*Mystery(8), 最後一層為 2*Mystery(1)=2, 這兩項乘積為 18, 再乘以中間層 (都是整數) 最後等於 34 是不可能的, 因此也排除, 剩下 (A) 與 (D).

追蹤 (A) :
呼叫 Mystery(9), 回傳 9+Mystery(8), 呼叫 Mystery(8), 回傳 8+Mystery(7), 呼叫 Mystery(7), 回傳 7+Mystery(6), ... , 呼叫 Mystery(2), 回傳 2+Mystery(1), 呼叫 Mystery(1), 回傳 1, 故最後回傳結果是 9+8+7+6+.....+2+1=(9+1)*9/2=45, 不是 34, 故排除 (A), 答案應是 (D).

追蹤 (D) :
呼叫 Mystery(9), 回傳 Mystery(7)+Mystery(8), 呼叫 Mystery(8), 回傳 Mystery(6)+Mystery(7), 呼叫 Mystery(7), 回傳 Mystery(5)+Mystery(6), ... , 呼叫 Mystery(3), 回傳 Mystery(1)+Mystery(2), 呼叫 Mystery(2), 回傳 Mystery(0)+Mystery(1), 呼叫 Mystery(1) 回傳 1, 呼叫 Mystery(0) 回傳 0. 故呼叫 Mystery(2) 與 Mystery(1) 結果均為 1, 分兩段計算 :
Mystery(9)=Mystery(7)+Mystery(8)=Mystery(5)+Mystery(6)+Mystery(6)+Mystery(7)=Mystery(5)+Mystery(6)+Mystery(6)+Mystery(5)+Mystery(6)=2*Mystery(5)+3*Mystery(6)
Mystery(5)=Mystery(3)+Mystery(4)=Mystery(1)+Mystery(2)+Mystery(2)+Mystery(3)=Mystery(1)+Mystery(2)+Mystery(2)+Mystery(1)+Mystery(2)=5
Mystery(6)=Mystery(4)+Mystery(5)=Mystery(2)+Mystery(3)+Mystery(5)=Mystery(2)+Mystery(1)+Mystery(2)+Mystery(5)=3+Mystery(5)=8
故 Mystery(9)=2*Mystery(5)+3*Mystery(6)=2*5+3*8=34

呵呵, 花了四天時間斷斷續續終於解完 25 題了, 發現遞迴函數考蠻多的哩! 其次是迴圈運算, 而且有些題目需要用到基礎數學概念如級數和與最大公因數等等. 另外像第 14 題則需要一點小小的求餘數技巧, 總之, 不只是考程式而已, 也考一些數學觀念.

向露天 livinghuang 採購零件一批

向露天賣家 livinghuang  購買 LoRa 模組等零組件一批 :

# [Arduino/RPi]每個180元2個一組販售SX1278 Lora module 模組433MHz $180*2=360
[Arduino/RPi]直插 LM35D LM35DZ 全新原裝溫度傳感器 TO-92封裝 $30*4=120
土壤濕度計檢測模塊 土壤濕度傳感器 $30*2=60

合計 360+120+60=540+運費60=600

2017年9月29日 星期五

如何在樹莓派上編譯執行 C 程式

前幾天在測試 C 語言指標時為了比較 C 程式不同平台上的執行結果, 就在樹莓派用 nano 編輯了如下顯示各資料型態所占記憶體長度的 C 程式 :

#include <stdio.h>

int main() {
    char c;
    int i;
    float f;
    double d;
    char *cptr=&c;
    int *iptr=&i;
    float *fptr=&f;
    double *dptr=&d;
    printf("變數名稱     記憶體位址          占用記憶體 (bytes)\n");
    printf("========    ================   ==================\n");
    printf("   c  \t    %p \t  %d\n", &c, sizeof(c));
    printf("   i  \t    %p \t  %d\n", &i, sizeof(i));
    printf("   f  \t    %p \t  %d\n", &f, sizeof(f));
    printf("   d  \t    %p \t  %d\n", &d, sizeof(d));
    printf(" cptr  \t    %p  \t  %d\n", &cptr, sizeof(cptr));
    printf(" iptr  \t    %p  \t  %d\n", &iptr, sizeof(iptr));
    printf(" fptr  \t    %p  \t  %d\n", &fptr, sizeof(fptr));
    printf(" dptr  \t    %p  \t  %d\n", &dptr, sizeof(dptr));
    return 0;
   }

按 Ctrl+O 輸入檔名 datalength.c 按 Enter, 再按 Ctrl+X 跳出 nano 編輯介面後用 gcc 編譯原始程式 :

login as: pi
pi@192.168.2.117's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Sep 27 19:52:28 2017 from 192.168.2.110
pi@raspberrypi:~ $ nano datalength.c  



pi@raspberrypi:~ $ gcc -o datalength datalength.c  

用 ls -al 指令檢查 home 目錄下確實編譯出 datalength 這個可執行檔 :

pi@raspberrypi:~ $ ls -al
total 10348
drwxr-xr-x 24 pi   pi      4096 Sep 29 07:33 .
drwxr-xr-x  3 root root    4096 Nov 26  2016 ..
-rw-r--r--  1 pi   pi        69 Nov 26  2016 .asoundrc
-rw-------  1 pi   pi      3112 Sep 15 00:15 .bash_history
-rw-r--r--  1 pi   pi       220 Nov 26  2016 .bash_logout
-rw-r--r--  1 pi   pi      3512 Nov 26  2016 .bashrc
drwxr-xr-x  7 pi   pi      4096 Mar 21  2017 .cache
drwx------ 13 pi   pi      4096 Sep 18 19:32 .config
-rw-r--r--  1 pi   pi      1786 Feb 22  2017 crontab.txt
-rwxr-xr-x  1 pi   pi      6356 Sep 29 07:33 datalength
-rw-r--r--  1 pi   pi       794 Sep 29 07:32 datalength.c
drwx------  3 pi   pi      4096 Nov 26  2016 .dbus
drwxr-xr-x  2 pi   pi      4096 Nov 26  2016 Desktop
drwxr-xr-x  5 pi   pi      4096 Nov 26  2016 Documents

但要如何執行呢? 直接輸入 datalength 無法執行, 會出現 "command not found" 錯誤 :

pi@raspberrypi:~ $ datalength
-bash: datalength: command not found

根據下面這篇的解釋, 在目前的 home 目錄下執行 datalength 會讓樹莓派以為你是要執行預設使用者程式目錄 /user/bin 下面的 datalength, 但此程式事實上是在 home 目錄下, 樹莓派在 /usr/bin 下找不到 datalength 這程式, 所以回應 "command not found" 錯誤 :

Question about C programming on the Pi. printf/STDOUT

"By just running test you are running the command called test which lives in a directory in your path, probably /usr/bin/test, Linux will always run things from your path and not something in the current directory."

正確的執行方式是前面要加 ./ 這個路徑 : 

pi@raspberrypi:~ $ ./datalength
變數名稱     記憶體位址          占用記憶體 (bytes)
========    ================   ==================
   c        0xbec46297    1
   i        0xbec46290    4
   f        0xbec4628c    4
   d        0xbec46280    8
 cptr       0xbec4627c            4
 iptr       0xbec46278            4
 fptr       0xbec46274            4
 dptr       0xbec46270            4

這樣就能正常執行了.

參考 :

Linux Pi的奇幻旅程(29)-Hello World!
设置并使用树莓派进行Python和C语言编程 (上)
樹莓派 Raspberry Pi 自行編譯與安裝 GCC 6 編譯器教學
[How To Raspberry Pi] 安裝 GCC
【樹莓派】編譯一個Hello World程式在RPi上執行
Question about C programming on the Pi. printf/STDOUT

2017年9月26日 星期二

使用 Arduino IDE 開發 ESP8266 應用 (三) : 從 NTP 伺服器取得網路時間

雖然我不是很喜歡用 Arduino IDE 來寫 ESP8266 程式 (我比較喜歡用 MicroPython), 但既然已經起了頭, 就至少把常用的功能測試一下, 讓 512K Flash 的 ESP-01 能物盡其用 (例如製作物聯網插座). ESP8266 最重要的功能就是透過 WiFi 連網, 而 ESP-01 只接出 GPIO 0, GPIO 2 兩隻腳, 因此若要取得時鐘訊號透過網路從 NTP 取得最方便, 不需要占用 GPIO 外接 RTC, 因此本篇要測試如何從 NTP 伺服器取得網路時間.

我主要是參考下面這篇範例來改寫 :

Arduino/NTPClient.ino at master · esp8266/Arduino

關於 ESP8266WiFi.h 函式庫文件參考 :

Arduino/doc/esp8266wifi/

本系列之前的測試紀錄參考 :

使用 Arduino IDE 開發 ESP8266 應用 (一) : 環境設定與韌體上傳
使用 Arduino IDE 開發 ESP8266 應用 (二) : 在網頁上控制 LED


測試 1 :  從 NTP 伺服器取得 UTC 網路時間

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

char* ssid="H30-L02-webbot";               //WiFi SSID
char* password="1234567890";                //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;    // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE];   //buffer to hold incoming and outgoing packets
WiFiUDP udp;   //UDP instance to let us send and receive packets over UDP

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    }

void loop() {
    //get a random server from the pool (get an IP from Server Name)
    WiFi.hostByName(ntpServerName, timeServerIP);
    sendNTPpacket(timeServerIP);    //send an NTP packet to a time server
    delay(1000);  // wait to see if a reply is available

    int cb=udp.parsePacket();  //return bytes received
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);   //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);    //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900 = " );
        Serial.println(secsSince1900);

        //now convert NTP time into everyday time:
        Serial.print("Unix time = ");
        // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        const unsigned long seventyYears=2208988800UL;
        // subtract seventy years:
        unsigned long epoch=secsSince1900 - seventyYears;
        // print Unix time:
        Serial.println(epoch);
        // print the hour, minute and second:
        Serial.print("The UTC time is ");       //UTC=Greenwich Meridian (GMT)
        Serial.print((epoch  % 86400L) / 3600); //print the hour (86400 secs/day)
        Serial.print(':');
        //In the first 10 minutes of each hour, we'll want a leading '0'
        if ( ((epoch % 3600) / 60) < 10 ) {Serial.print('0');}
        Serial.print((epoch  % 3600) / 60); // print the minute (3600 secs/minute)
        Serial.print(':');
        // In the first 10 seconds of each minute, we'll want a leading '0'
        if ( (epoch % 60) < 10 ) {Serial.print('0');}
        Serial.println(epoch % 60); // print the second
        }
    delay(10000);
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);  //clear the buffer
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE); //send UDP request to NTP server
    udp.endPacket();
    }

NTP 伺服器使用的是 UDP 協定, 傳送與接收的封包數都是 48 個 Bytes, 參考以前在 Arduino+ESP8266 所做的測試紀錄 :

再探 NTP 協定
利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)


注意, 使用 UDP 協定功能必須匯入 WiFiUdp.h 函式庫, 此函式庫在開發板設定時即已下載, 不須另外安裝. 上面的程式編譯上傳後重開機執行, 序列埠監控視窗輸出如下 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429678
Unix time = 1506440878
The UTC time is 15:47:58
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429684
Unix time = 1506440884
The UTC time is 15:48:04
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429691
Unix time = 1506440891
The UTC time is 15:48:11
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429697
Unix time = 1506440897
The UTC time is 15:48:17
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429704
Unix time = 1506440904
The UTC time is 15:48:24

注意, 上面顯示的是 UTC 時間, 轉成台灣時間需 +8 小時 (28800 秒).

參考 "利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)" 的做法, 把 UTC 時間加上 28800 秒後轉成台灣的時戳, 然後用 Arduino 的 setTime() 函數將此時戳設定到 ESP8266 的內部時鐘, 這樣就可以呼叫 Time.h 與 TimeAlarms.h 函式庫的 year(), month(), hour() 等函數了, 注意, Time.h 與 TimeAlarms.h 都要匯入.


 函式 說明
 hour() 傳回現在的時 (24 小時制)
 hourFormat12() 傳回現在的時 (12小時制)
 minute() 傳回現在的分
 second() 傳回現在的秒
 year() 傳回現在的年
 month() 傳回現在的月
 day() 傳回現在的日
 weekday() 傳回現在的星期 (星期日為 1)


測試 2 :  從 NTP 伺服器取得 UTC 網路時間更新內件 RTC (使用計時器但無效)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Time.h>
#include <TimeAlarms.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="1234567890";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    sync_clock();
    Alarm.timerRepeat(60, sync_clock); //timer task every 60 seconds
    }

void loop() {
    String d=getDate();
    String t=getTime();
    String w=getWeek();
    Serial.println(d + " " + t + " " + w);  
    delay(10000);
    }
 
void sync_clock() {
  setTime(getUnixTime() + 28800L);
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

unsigned long getUnixTime() {
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);                   //send an NTP packet to a time server
    delay(1000);                                   // wait to see if a reply is available

    int cb=udp.parsePacket();                      //return bytes received
    unsigned long unix_time=0;
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);                        //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

序列埠監控視窗輸出如下 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715472215
Unix time=Unix time stamp (seconds since 1970-01-01)=1506483415
2017-09-27 11:36:55 Wed
2017-09-27 11:37:05 Wed
2017-09-27 11:37:15 Wed
2017-09-27 11:37:25 Wed
2017-09-27 11:37:35 Wed
2017-09-27 11:37:45 Wed
2017-09-27 11:37:55 Wed
2017-09-27 11:38:05 Wed
2017-09-27 11:38:15 Wed
2017-09-27 11:38:25 Wed
2017-09-27 11:38:35 Wed
2017-09-27 11:38:45 Wed
2017-09-27 11:38:55 Wed
2017-09-27 11:39:05 Wed
2017-09-27 11:39:15 Wed
2017-09-27 11:39:25 Wed

看起來 Arduino 內建的 setTime() 可以正常設定 ESP8266 的 RTC 內部時鐘, 但 Alarm.timerRepeat() 似乎沒有運作! 照理應該每 60 秒呼叫一次 sync_clock() 才對, 但除了在 setup() 中呼叫過一次 sync_clock() 外, 之後就沒有再呼叫了, 奇怪, 不知哪裡出問題.

我在下列葉難的文章中看到 Timer 函式庫 :

Arduino一個好用的計時器程式庫  

改用 Timer.h 函式庫裡的 Timer.every() 也是一樣無作用, 我猜有可能這些函式庫是針對 Arduino 板子, 可能對 ESP8266 無效? 參考下面這篇 :

ESP8266 Timer

看起來在 Arduino IDE 中要搞定 ESP8266 的 Timer 似乎很麻煩哩! 但我要的只是想固定一段時間 (例如 40 秒) 就從 NTP 伺服器取得最新時間來更新內部時鐘而已, 免得內部時鐘誤差越來越大. 其實若 TimerAlarms.h 不能用, 也可以用計數器, 例如下面範例每秒顯示一次內部時鐘, 但每 40 次迴圈 (即大約每 40 秒) 就讀取 NTP 時間來同步 :


測試 3 :  從 NTP 伺服器取得 UTC 網路時間更新內建 RTC (使用計數器)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Time.h>
#include <TimeAlarms.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="1234567890";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP
int count=0;

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    sync_clock();   //以 NTP 時間初始設定內部時鐘
    }

void loop() {
    String d=getDate();
    String t=getTime();
    String w=getWeek();
    int Hm=hour()*100 + minute();   //時:分整數, 0~2359
    Serial.print(d + " " + t + " " + w + " ");
    Serial.println(Hm);  
    ++count;
    if (count >= 40) {  //每 40 次迴圈與 NTP 同步一次
        sync_clock();
        count=0;
        }
    delay(1000);
    }
 
void sync_clock() {
  unsigned long GMT=getUnixTime();
  if (GMT != 0) {   //有得到 NTP 回應才更新 ESP8266 內建 RTC
    setTime(GMT + 28800L);  //以台灣時間更新內部時鐘
    }
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

String getDateTime() {  //傳回日期時間
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;  //傳回格式如 2016-07-16 16:09:23 的日期時間字串
  }

unsigned long getUnixTime() {
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);   //send an NTP packet to a time server
    delay(1000);   // wait to see if a reply is available

    int cb=udp.parsePacket();     //return bytes received
    unsigned long unix_time=0;    //預設傳回 0, 表示未收到 NTP 回應
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);    //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;    //更新 unix_time
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

注意, 在 loop() 中的 Hm 變數是由 Hour 乘以 100 後加上 Minute, 這樣會組成 0~2359 的 "時分 整數", 可應用在物聯網插頭或自動澆水器等應用的時間判斷上, 例如如果要在早上六點與傍晚六點各澆水十分鐘, 則啟動馬達的 "時分整數" 位於 600~610 與 1800~1810 兩個區間內, 可用如下程式碼判斷 :

if ((Hm >= 600 && Hm <=610) || (Hm >= 1800 && Hm <=1810)) {
    digitalWrite(motorPin, HIGH);  //motor on
    }
 else {
    digitalWrite(motorPin, LOW);  //motor off
    }

當然如果需要也可以精細到秒, 例如 :

HmS=int Hm=hour()*10000 + minute()*100 + second();

這個 HmS 的值區間為 0~235959. 要注意的是, 不管是 Hm 還是 HmS, 部分值域是無意義的, 例如 Hm 的 178 就是無意義的值, 因為 3 位數最大只到 159 (1 點 59 分), 接著就跳到 200  (2 點) 了.

序列埠監控視窗輸出訊息 :

Connecting to H30-L02-webbot
.................
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715498311
Unix time=Unix time stamp (seconds since 1970-01-01)=1506509511
2017-09-27 18:51:51 Wed 1851
2017-09-27 18:51:52 Wed 1851
2017-09-27 18:51:53 Wed 1851
2017-09-27 18:51:54 Wed 1851
2017-09-27 18:51:55 Wed 1851
2017-09-27 18:51:56 Wed 1851
2017-09-27 18:51:57 Wed 1851
2017-09-27 18:51:58 Wed 1851
2017-09-27 18:51:59 Wed 1851
2017-09-27 18:52:00 Wed 1852
2017-09-27 18:52:01 Wed 1852
2017-09-27 18:52:02 Wed 1852
2017-09-27 18:52:03 Wed 1852
2017-09-27 18:52:04 Wed 1852
2017-09-27 18:52:05 Wed 1852
2017-09-27 18:52:06 Wed 1852
2017-09-27 18:52:07 Wed 1852
2017-09-27 18:52:08 Wed 1852
2017-09-27 18:52:09 Wed 1852
2017-09-27 18:52:10 Wed 1852
2017-09-27 18:52:11 Wed 1852
2017-09-27 18:52:12 Wed 1852
2017-09-27 18:52:13 Wed 1852
2017-09-27 18:52:14 Wed 1852
2017-09-27 18:52:15 Wed 1852
2017-09-27 18:52:16 Wed 1852
2017-09-27 18:52:17 Wed 1852
2017-09-27 18:52:18 Wed 1852
2017-09-27 18:52:19 Wed 1852
2017-09-27 18:52:20 Wed 1852
2017-09-27 18:52:21 Wed 1852
2017-09-27 18:52:22 Wed 1852
2017-09-27 18:52:23 Wed 1852
2017-09-27 18:52:24 Wed 1852
2017-09-27 18:52:25 Wed 1852
2017-09-27 18:52:26 Wed 1852
2017-09-27 18:52:27 Wed 1852
2017-09-27 18:52:28 Wed 1852
2017-09-27 18:52:29 Wed 1852
2017-09-27 18:52:30 Wed 1852
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715498352
Unix time=Unix time stamp (seconds since 1970-01-01)=1506509552
2017-09-27 18:52:33 Wed 1852
2017-09-27 18:52:34 Wed 1852
2017-09-27 18:52:35 Wed 1852
2017-09-27 18:52:36 Wed 1852
2017-09-27 18:52:37 Wed 1852
2017-09-27 18:52:38 Wed 1852
2017-09-27 18:52:39 Wed 1852

可見確實每 40 次迴圈就會與 NTP 伺服器同步一次, 事實上頻率不用這麼高, 跑了數百次到數千次再同步也不會有顯著誤差. 但是要注意, 下面這篇文件提到, ESP8266 內建 RTC 每 7 個小時 45 分會溢位, 因此 7 個小時內一定要再跟 NTP 伺服器同步一次, 參考 :

http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/general.html#real-time-clock

如果只是要取得 "時分" 或 "時分秒" 整數以判別目前時間, 其實也不需要匯入 Time.h 與 TimeAlarms.h, 直接將時戳做餘數與加法運算即可, 如下列兩個函數 :

int getHm(unsigned long T) {
  return ((T  % 86400L) / 3600)*100 + ((T  % 3600) / 60);
  }

int getHmS(unsigned long T) {
  return ((T  % 86400L) / 3600)*10000 + ((T  % 3600) / 60)*100 + (T % 60);
  }

使用時只要傳入時戳 T 呼叫 Hm() 或 HmS() 即可 :

unsigned long T=getUnixTime() + 28800L;  //計算 GMT+8 時戳
if (Hm(T) >= 600 && Hm(T) <= 610) {
    digitalWrite(motorPin, HIGH);  //motor on
    }
 else {
    digitalWrite(motorPin, LOW);  //motor off
    }

例如下列範例 :

測試 4 :  從 NTP 伺服器取得網路時間模擬控制馬達開關 (不使用時間函式庫)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="a5572056";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP

const int motorPin=2;      // GPIO2

void setup() {
    pinMode(motorPin, OUTPUT);
    digitalWrite(motorPin, LOW);
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    }

void loop() {
    int Hm=getHm(getUnixTime() + 28800L);   //取得台灣時間的 "時分" 整數
    Serial.println(Hm);
    if (Hm >= 1500 && Hm <= 1510) {  //在 15:00 ~15:10 間啟動馬達
        digitalWrite(motorPin, HIGH);  //motor on
        Serial.println("Motor is ON");
        }
    else {
        digitalWrite(motorPin, LOW);  //motor off
        Serial.println("Motor is OFF");
        }  
    delay(60000);
    }

int getHm(unsigned long T) {  //return 0~2359
  return ((T  % 86400L) / 3600)*100 + ((T  % 3600) / 60);  
  }

int getHmS(unsigned long T) {  //return 0~235959
  return ((T  % 86400L) / 3600)*10000 + ((T  % 3600) / 60)*100 + (T % 60);  
  }

unsigned long getUnixTime() { //get GMT epoch
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);                   //send an NTP packet to a time server
    delay(1000);                                   // wait to see if a reply is available

    int cb=udp.parsePacket();                      //return bytes received
    unsigned long unix_time=0;
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);                        //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

不過上面這個程式有風險, 因為它完全依靠 UDP 查詢到的時間來決定是否啟閉馬達, 而 UDP 協定是不保證傳輸品質的協定, 有時候收不到回應, 這時 getUnixTime() 會傳回 0. 比較保險的做法還是像測試 3 那樣依靠 ESP8266 內部的 RTC, 使用 Time.h 與 TimeAlarms.h 的 hour(), minute() 一定可以取得資料製作 "時分" 整數, 只要定期與 UDP 同步即可.

測試結果 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570098
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581298
1448
Motor is OFF
....
....
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570680
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581880
1458
Motor is OFF
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570741
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581941
1459
Motor is OFF
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570803
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582003
1500
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570865
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582065
1501
Motor is ON
....
....
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571050
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582250
1504
Motor is ON
sending NTP packet...
no packet yet
800
Motor is OFF     (未取得 1505 回應, 導致馬達關閉)
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571173
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582373
1506
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571235
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582435
1507
Motor is ON
sending NTP packet...
no packet yet
800
Motor is OFF   (未取得 1508 回應, 導致馬達關閉)
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571358
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582558
1509
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571420
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582620
1510
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571481
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582681
1511
Motor is OFF

可見在 15:05 與 15:08 時, 因未取得 UDP 回應, 使得 Hm 計算結果為 800 (即 GMT+8 的 8), 導致馬達被關閉. 可以將 loop() 內程式碼加上判斷 getUnixTime() 是否為 0 的機制, 若未取得 NTP 回應就不做啟閉動作, 維持原狀態 :

    unsigned long T=getUnixTime();
    if (T != 0) {
      int Hm=getHm(T + 28800L);   //取得台灣時間的 "時分" 整數
      Serial.println(Hm);
      if (Hm >= 1500 && Hm <= 1510) {  //在 15:00 ~15:10 間啟動馬達
          digitalWrite(motorPin, HIGH);  //motor on
          Serial.println("Motor is ON");
          }
      else {
          digitalWrite(motorPin, LOW);  //motor off
          Serial.println("Motor is OFF");
          }
      }

參考 :

https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/WiFiUdp.h
ESP8266 Timer0 and ISR
http://twincati.blogspot.tw/2016/12/esp8266-timer.html
# 86Duino 程式語法參考

2017-09-28 補充 :

這幾天停下 C 語言來做 Arduino on ESP8266 實驗要暫停一下, 這些實驗是專為 512K Flash 的 ESP-01 而做的, 我想將其應用在小型終端控制器, 例如物聯網開關與自動灑水系統. 有空回來時要繼續做的項目如下 :
  1. HTTP 客戶端 (MySQL 資料庫, ThingSpeak, Twitter 控制 ...)
  2. WiFi 基地台連線設定
  3. Blynk on ESP8266 Standalone

希雅 (Sia) 的水晶吊燈

最近在 Christian Dior 的廣告裡聽到一首旋律很特別的歌, 問菁菁是否知道歌名, 她說是希雅 (Sia) 的歌, 還幫我找到 Youtube 上的官方 MV, 原來這首歌是進入美國排行榜的熱銷單曲 Chandelier (水晶吊燈) :

https://www.youtube.com/watch?v=h4s0llOpKrU


https://www.youtube.com/watch?v=2vjPBrBU-TM


# https://www.youtube.com/watch?v=ILTZ8qZbNK0


MV 中的舞者的舞蹈也很特別, 菁菁說她常出現在希雅的 MV 中. 這位青少年舞者叫做梅狄(Maddie Ziegler), 今年才 14 歲, 出生於美國賓州, 曾獲時代雜誌評為 2015 年最有影響力的青少年之一, 參見 :

Sia的MV女主角是她!舞蹈精湛獲選「青少年最愛舞者」
https://zh.wikipedia.org/wiki/梅狄·齊格勒
https://www.instagram.com/maddieziegler/?hl=zh-tw

希雅是澳洲歌手, 目前住在美國加州. 由於早期酗酒與吸食大麻的影響曾患有躁鬱症, 但後來成為一個素食主義者 (我也是), 也熱心於公益事業, 參見 :

https://zh.wikipedia.org/wiki/希雅

2017-09-28 補充 :

Dior 廣告中的女主角是猶太裔的美國模特兒與演員 Natalie Portman (娜塔莉波曼), 畢業於哈佛心理系, 曾演出星際大戰三部曲, 2011 年以 "黑天鵝" 獲得奧斯卡金像獎最佳女主角, 參見 :

https://zh.wikipedia.org/wiki/娜塔莉·波特曼
Miss Dior Commercial Music 2017 – ‘Chandelier’ What Would You Do For Love?

這首歌的歌詞中譯參考 :

http://aerirabbit.pixnet.net/blog/post/379891463-sia---chandelier

使用 Arduino IDE 開發 ESP8266 應用 (二) : 在網頁上控制 LED

我在 Instructable 看到下面這篇透過 WiFi 網路控制 LED 的實驗 :

ESP 8266 Wifi Controlled Home Automation

主要是透過 ESP8266WiFi.h 函式庫的支援來連線到一個無線基地台, 並建立一個網頁伺服器, 讓我們可以利用電腦或手機瀏覽器連線到此網頁控制 LED 的亮滅. 關於 ESP8266WiFi.h 函式庫文件參考 :

Arduino/doc/esp8266wifi/

本系列之前的測試紀錄參考 :

使用 Arduino IDE 開發 ESP8266 應用 (一) : 環境設定與韌體上傳

不過上面 Instructable 作者附的程式碼有點怪怪的, 含有一些不該有的 HTML 碼, 而且缺漏了一些資訊, 例如回應網頁上 "Click here to turn ..." 裡面的超連結竟然沒有 href 屬性, 照所貼圖片應該是 href='/LED=ON' 與 href='/LED=OFF' 才對, 我將其修改為如下測試 1 :


測試 1 :  透過 WiFi 控制 LED 

#include <ESP8266WiFi.h>
const char* ssid="H30-L02-webbot";
const char* password="1234567890";
const int LED=2;      // GPIO2
int value=LOW;
WiFiServer server(80);

void setup() {
    Serial.begin(115200);
    delay(10);
    pinMode(LED, OUTPUT);
    digitalWrite(LED, LOW);
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    server.begin();
    Serial.println("Server started");
    Serial.print("Use this URL to connect: ");
    Serial.print("http://");
    Serial.print(WiFi.localIP());
    Serial.println("/");
    }
 
void loop() {
    WiFiClient client=server.available();
    if (!client) {return;}
    Serial.println("New client");
    while (!client.available()) {delay(1);}
    String request=client.readStringUntil('\r');
    Serial.println(request);
    client.flush();
    if (request.indexOf("/LED=ON") != -1) {value=HIGH;}
    if (request.indexOf("/LED=OFF") != -1) {value=LOW;}
    Serial.print("value=");
    Serial.println(value);
    digitalWrite(LED, value);
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/html");
    client.println("");
    client.println("");
    client.println("");
    client.print("Led pin is now: ");
    if (value==HIGH) {client.print("On");}
    else {client.print("Off");}
    client.println("<br><br>");
    client.println("Click <a href='/LED=ON'>here</a> turn the LED on pin 2 ON<br>");
    client.println("Click <a href='/LED=OFF'>here</a> turn the LED on pin 2 OFF<br>");
    client.println("</p><p>");
    delay(1);
    Serial.println("Client disonnected");
    Serial.println("");
    }

編譯上傳訊息 :

Archiving built core (caching) in: C:\Users\cht\AppData\Local\Temp\arduino_cache_467201\core\core_esp8266_esp8266_generic_CpuFrequency_80,FlashFreq_40,FlashMode_dio,UploadSpeed_115200,FlashSize_512K64,ResetMethod_ck,Debug_Disabled,DebugLevel_None_____ef4920f85b594b4d068baa9c05fdba96.a
草稿碼使用了 230221 bytes (53%) 的程式儲存空間。上限為 434160 bytes。
全域變數使用了 32348 bytes (39%) 的動態記憶體,剩餘 49572 bytes 給區域變數。上限為 81920 bytes 。
Uploading 234368 bytes from C:\Users\cht\AppData\Local\Temp\arduino_build_695446/arduesp_2.ino.bin to flash at 0x00000000
................................................................................ [ 34% ]
................................................................................ [ 69% ]
.....................................................................            [ 100% ]


奇怪了, 程式比較長了, 編譯出來的機器碼為何只有占 53% Flash ? 上次那個簡單的雙 LED 交互閃爍程式短短的就佔 51%? 參考 :

使用 Arduino IDE 開發 ESP8266 應用 (一) : 環境設定與韌體上傳

上傳完成後, 拔掉 GPIO 0 的接地線, 把 ESP-01 重插開機, 打開序列埠監控視窗會看到 ESP8266 已連上 WiFi 基地台並顯示其 IP, 這時打開手機 WiFi 連線同一個無線基地台, 開啟瀏覽器連線 ESP8266 的 IP 就可以看到程式輸出的網頁了, 按 here 超連結即可控制 GPIO 2 外接 LED 的明滅了 :




序列埠監控視窗輸出訊息如下 :

Connecting to H30-L02-webbot
...
WiFi connected
Server started
Use this URL to connect: http://192.168.43.163/
New client
GET / HTTP/1.1
Client disonnected

New client
GET /LED=ON HTTP/1.1
value=1
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value=1
Client disonnected

New client
GET /LED=OFF HTTP/1.1
value=0
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value=0
Client disonnected


接下來我參考上次測試 MicroPython on ESP8266 伺服器的範例, 將上面測試 1 修改為如下測試 2, 把 ESP-01 模組的兩個 GPIO 埠都各接一個 LED + 220 歐姆電阻, 控制網頁改用表格來呈現, 參考 :

MicroPython on ESP8266 (十四) : 網頁伺服器測試


測試 2 :  透過 WiFi 控制 LED (使用表格)

#include <ESP8266WiFi.h>
const char* ssid="H30-L02-webbot";
const char* password="1234567890";
const int LED0=0;      // GPIO0
const int LED2=2;      // GPIO2
int value0=LOW;
int value2=LOW;
WiFiServer server(80);

void setup() {
    Serial.begin(115200);
    delay(10);
    pinMode(LED0, OUTPUT);
    pinMode(LED2, OUTPUT);  
    digitalWrite(LED0, LOW);
    digitalWrite(LED2, LOW);  
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    server.begin();
    Serial.println("Server started");
    Serial.print("Use this URL to connect: ");
    Serial.print("http://");
    Serial.print(WiFi.localIP());
    Serial.println("/");
    }
 
void loop() {
    WiFiClient client=server.available();
    if (!client) {return;}
    Serial.println("New client");
    while (!client.available()) {delay(1);}
    String request=client.readStringUntil('\r');
    Serial.println(request);
    client.flush();
    if (request.indexOf("/LED0=ON") != -1) {value0=HIGH;}
    if (request.indexOf("/LED0=OFF") != -1) {value0=LOW;}
    if (request.indexOf("/LED2=ON") != -1) {value2=HIGH;}
    if (request.indexOf("/LED2=OFF") != -1) {value2=LOW;}  
    Serial.print("value0=");
    Serial.println(value0);
    Serial.print("value2=");
    Serial.println(value2);  
    digitalWrite(LED0, value0);
    digitalWrite(LED2, value2);  
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/html");
    client.println("");
    client.println("");
    client.println("");
    client.println("<br><br>");
    client.println("<h1>ESP8266 Pins</h1>");
    client.println("<table border='1'>");
    client.println("<tr>");
    client.println("<th>Pin</th>");
    client.println("<th>Value</th>");
    client.println("<th>Action</th>");
    client.println("</tr>");
    client.println("<tr>");
    client.println("<td>GPIO 0</td>");
    client.print("<td>");
    client.print(digitalRead(LED0));  
    client.println("</td>");
    client.println("<td><a href='/LED0=ON'>ON</a> <a href='/LED0=OFF'>OFF</a></td>");
    client.println("</tr>");
    client.println("<tr>");
    client.println("<td>GPIO 2</td>");
    client.print("<td>");
    client.print(digitalRead(LED2));  
    client.println("</td>");
    client.println("<td><a href='/LED2=ON'>ON</a> <a href='/LED2=OFF'>OFF</a></td>");
    client.println("</tr>");
    client.println("</table>");
    delay(1);
    Serial.println("Client disonnected");
    Serial.println("");
    }


將程式編譯上傳後拔除 GPIO 0 之接地, GPIO 0/2 接上 LED 重插開機即可.

Archiving built core (caching) in: C:\Users\cht\AppData\Local\Temp\arduino_cache_467201\core\core_esp8266_esp8266_generic_CpuFrequency_80,FlashFreq_40,FlashMode_dio,UploadSpeed_115200,FlashSize_512K64,ResetMethod_ck,Debug_Disabled,DebugLevel_None_____ef4920f85b594b4d068baa9c05fdba96.a
草稿碼使用了 230725 bytes (53%) 的程式儲存空間。上限為 434160 bytes。
全域變數使用了 32524 bytes (39%) 的動態記憶體,剩餘 49396 bytes 給區域變數。上限為 81920 bytes 。
Uploading 234880 bytes from C:\Users\cht\AppData\Local\Temp\arduino_build_695446/arduesp_3.ino.bin to flash at 0x00000000
................................................................................ [ 34% ]
................................................................................ [ 69% ]
......................................................................           [ 100% ]


ESP8266 連上基地台後用手機瀏覽器連線 ESP8266 網址即可看到網頁, 點表格中的 ON/OFF 超連結即可控制兩個 LED 的明滅 :







序列埠監控視窗輸出訊息如下 :

Connecting to H30-L02-webbot
................
WiFi connected
Server started
Use this URL to connect: http://192.168.43.163/

New client
GET /LED0=ON HTTP/1.1
value0=1
value2=0
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value0=1
value2=0
Client disonnected

New client
GET /LED2=ON HTTP/1.1
value0=1
value2=1
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value0=1
value2=1
Client disonnected

New client
GET /LED0=OFF HTTP/1.1
value0=0
value2=1
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value0=0
value2=1
Client disonnected

New client
GET /LED2=OFF HTTP/1.1
value0=0
value2=0
Client disonnected

New client
GET /favicon.ico HTTP/1.1
value0=0
value2=0
Client disonnected


呵呵, 看起來還不錯, 512K Flash 的 ESP-01 雖然在 MicroPython 中沒辦法建立檔案系統而實用性不高, 但用 Arduino IDE 直接編譯原始碼倒是還蠻好用的, 可以用來當作小型物聯網終端控制器.

不過比起有檔案系統的 MicroPython 而言, 使用 Arduino IDE 開發比較麻煩, 每次修改程式都要將 GPIO 0 接地才能更新韌體, 而 MicroPython 只要用 ampy 等工具上傳新的應用程式到檔案系統跟目錄下即可, MicroPython 韌體不用去動它.

參考 :

Android Arduino Wifi Control Devices with ESP8266 Module
Arduino/NTPClient.ino at master · esp8266/Arduino