2018年1月4日 星期四

香港天文台天氣預告資料

對住香港天文台 (HKO) D 數據, 真係火都黎埋.
明明香港得一個天文台, 但佢地竟然有多個唔夾既數據資料公佈出黎.
數據唔夾都算喇, 更新時間唔穩定都算喇, 近日仲無端端成日出 not available.

由於整左個鐘睇天氣, 用開既天氣預測, 成日出 not available, 又要重新要研究比較唔同既 source.

以下資料, 如有地區可選, 均以 沙田 (地區碼 SHA) 為基礎.
本來拆 data 用英文好拆D, 但考慮到想加埋段中文既天氣預告, 可能會改用中文 source.
注意, 雖然佢 D link 係叫  XML,  不過, 天文台既 XML, 大部份都唔係我地心目中既 XML 黎既.

(1) 現時用緊既 source http://maps.weather.gov.hk/ocf/dat/SHA.xml  (Sample)
    相對既網頁為 http://maps.weather.gov.hk/ocf/text_e.html?mode=0&station=SHA#

    回傳既係一個好大既 JSON.
    當中包括多樣資料, 主要用到以下幾樣:
    - 8 天 最高及最低溫度
    - 8 天 天氣預測圖
    - 12 小時天氣預測 溫度
    - 12 小時天氣預測 濕度
    - 12 小時 天氣預測 3 小時圖
    註: 因為我個鐘係出 8 天預測 及 12 小時 預測, 所以唔係全部要哂既.
    
   但最近經常出現類似以下既老人痴呆症既病徵, 近既估唔到, 遠既就有.
(根據佢地講, 出 M 字 即係 not available)

(2) 初時即刻做左 urgent fix, 12小時預測無得攪, 每日天氣預測, 就轉去呢度
      不過, 好可惜, 佢果 7 天, 估佢唔到.  有可能係包埋今日, 亦可能係聽日開始, 更迎時間不定.
      但我要出今日加一星期既 8天預測, 都係少左一日, 唔好彩既 M 未果日無左, 一樣係乜.

      回傳既係含隔符號的合成資料.  以 "@" 分隔日, "#" 分隔每項資料.
      當中包括多樣資料, 可以用到以下幾樣:
      - 7 天 (可以係 day 0-6, 又可以係 day 1-7) 天氣圖
      - 7 天 最高最低溫度
      - 7 天 最高最低濕度
      - 7 天 風向
      - 7 天 天氣
      

(3) 考慮緊另一個 9 天預測 
      如果想睇 html 既顯示, 可以睇 呢個網頁, 佢就係用呢個 source 既.
      當中 1515031742121 係當時既 epoch, 去到 ms 既, 可以去 呢度 轉番時間睇.
      不過, 測試過, 好似唔比個時間就已經可以回番當時既資料, 暫時 ignore 佢.

      回傳既係一個好大既 JSON.
      當中包括多樣資料, 可以用到以下幾樣:
      - 即時天氣報告
      - 即時溫度 (HKO)
      - 即時濕度 (HKO)
      - 9 天  (可以係 day 0-8, 又可以係 day 1-9) 天氣圖
      - 9 天 最高最低溫度
      - 9 天 最高最低濕度
      - 9 天 風向
      - 9 天 天氣

真係比天文台吹X漲, 暫時只有 (1) 係肯定包左今日既天氣圖,  而 (2) 同 (3) 就亂X咁黎.
話就話 11:30 及 16:00 更新, 但有時 12:XX 仲係有今日, 轉頭 13:XX 左右就無左.
唔通呢D野係人手做, 唔係 set scheduler 做既? 有點莫名其妙.

2017年11月6日 星期一

super169/hkweather

之前既天文台資料 project, 整左個 docker image, 有興趣可以自己裝黎玩下.

主要目的係比部 MCU 顯示出黎, 做下簡單既天氣資料站.

家陣仲諗緊要乜野資料需要, 同埋每個資料既來源, 睇下點樣可以簡化佢.

初步諗只會有兩個頁面顯示天氣資料:

1) 主頁, 地區天氣資料 (可輸入地區碼)
2) 九天天氣預測 - 來源
  • 天氣圖示
  • 最高/低氣溫
  • 最高/低濕度

另外, 會插上 其他 sensor (例如溫度, 濕度, 光照, 人體, 距離, ....)

資源來源:

  1. http://rss.weather.gov.hk/rss/CurrentWeather.xml
  2. http://www.weather.gov.hk/wxinfo/ts/text_readings_e.htm
  3. http://rss.weather.gov.hk/rss/WeatherWarningSummaryv2.xml
  4. http://rss.weather.gov.hk/rss/SeveralDaysWeatherForecast.xml


各項資料分析 (盡量拆英文網站, 避免中文字撞碼):

即時本港天氣圖示 - 主(1)

src="http://rss.weather.gov.hk/img/pic60.png"

該區現時氣溫 - 主(2)  後備(1)

(1)  HK Observatory 25.1
(2)  Hong Kong Observatory 25 degrees 

該區現時濕度 - 主(2) 

HK Observatory                 25.1       66

紫外線指數/強度 - 主(1)

During the past hour the mean UV Index recorded at King's Park : 2
Intensity of UV radiation : low

今日最高/低氣溫 - 主(2) 

(1) HK Observatory                 25.1       66        25.2 / 21.8

天氣警告(只要颱風及暴雨) - 主(3)

hard code 認呢度既字眼: http://www.weather.gov.hk/m/warndef.htm

九天天氣預測 - 主(4) 圖示補充 準備轉用 

天氣圖示
成篇文咁, 拆到想死, 例如呢幾句野, 大家會點理解, 點決定用邊個圖:
  1. Mainly cloudy with one or two rain patches.
  2. Mainly cloudy. One or two rain patches at first. 
  3. Mainly cloudy with a few rain patches.

最高/低氣溫

Temp range: 23 - 27 C


最高/低濕度

R.H. range: 65 - 85 per Cent

香港天文台資訊

做乜鬼玩玩下 arduino 走去講 香港天文台資訊?

因為  pat pat 痕, 想整個 internet clock, 整好左之後, 見既然上左網, 不如攞埋天氣睇下.
搵左D sample, 可以用 http://wunderground.com, 不過, 唔知個 source 係邊.

既然講香港天氣, 更係搵下香港天文台喇, 年年花香港咁多錢, 點都有D野睇下掛.
而且, 唔知外國既, 有無香港D天氣警號 (例如 8 號風球).
點知, 上網搵下至知, 香港天文台既天氣 資料, 仲停留十幾年前既科技...只係得 RSS.

真係粗口都黎埋, 今時今日, 仲只係預人地用電腦睇, 仲有無落後D呀. 唉, 算喇, 香港....

再去搵下, 原來幾年前已經有人開左個 開放源碼香港天氣計劃 hk0weather.
不過, 佢係用 python,  小弟真係唔識.
而且, 個 OpenData 既 project, 好似重點唔係天氣.
加上自己用 mcu, 就算佢比 JSON 比我, 要拆都仲係煩.

求人不如求己, pat pat 痕多野, 決定自已做算了.


首天, 資料既來原好重要, 既然想睇香港天氣, 就算幾唔準都係, 都係睇番香港天文台先.
暫時睇, 主要會用呢幾個 source 既資料:



PDA Data source:




不過, 之前都講左, 香港天文台比番日既野, 十九幾年前既 RSS, 仲要 data 係夾左做 html 出.
所以, 唔好諗住有好靚既料, 例如: Temperature=29 咁比你, 只可以認位拆舍 html/xml.


本港地區天氣報告 (中文English) - 更新: 每小時的 02 分, 或有特別需要
有香港整體數據, 包括:

  • 氣溫
  • 相對濕度
  • 過 去 一 小 時 平 均 紫 外 線 指 數
  • 紫 外 線 強 度 
  • 各區氣溫
  • 颱 風 消息


香港分區天氣 (中文English) - 更新:

有各區氣象站錄得既數據, 包括:

  • 即時氣溫
  • 即時相對濕度
  • 今日最高/低溫度
  • 即時草溫
  • 今日最低草溫
  • 十分鐘平均風向,風速及最高陣風風速 (多個氣象站)
  • 平均海平面氣壓 (赤鱲角,長洲,天文台,流浮山,坪洲,沙田,石崗,上水,打鼓嶺,大埔,橫瀾島,濕地公園)
  • 十分鐘平均能見度 (中環,赤鱲角,西灣河,橫瀾島)
  • 太陽總輻射量/直接輻射量/漫射輻射量  (滘西洲,京士柏)

九 天 天 氣 預 報 (中文English) - 更新: 11:30, 16:30, 或有特別需要

未來九天天氣預測, 包括: 
  • 風向及風力
  • 天氣情況
  • 氣溫
  • 相對濕 度


只係知道個 url, 但佢會點出, 好多野唔係時時有既, 只可以估估下, 到時執生.


玩玩下都幾火, RSS 真係唔多合用黎拆 data, 好多野都唔知.
比如 Weather 既 condition 會有幾多種唔知, 就唔知要做幾多個 icon 比佢.
天文台就當然唔會有 spec 提供, 估都有排估, 試下拆佢成版 HTML 話者仲易.
暫時用 regular express 拆左兩個 page, 整埋個 service  可以用下:


https://hkweather-tester01-rbhcwiqj1ojo.runkit.sh/c?location=hko

即時天氣, 有氣溫, 濕度, 同埋 UV 指數.  如果有 location 比埋你果區既氣溫.
Location code 係我自己定既, 其實好懶咁將天文台果 26 區既 initial 起既.
比如: TM=屯門, ST=沙田, TMT=大美篤, ....
唔比或者唔存在, 就會出番天文台 HKO.
幾個 data 分別係:

  • pic# : 用黎顯示家陣既天氣既公仔
  • 氣溫
  • 相對濕度
  • 過去一小時紫外線指數
  • 紫外線強度
  • 地區英文區
  • 地區氣溫


遲D可能會去埋 香港分區天氣 攞多D地區資料, 不過, 果版野...真係拆到火都黎埋.


https://hkweather-tester01-rbhcwiqj1ojo.runkit.sh/f

九天天氣預報.



2017年8月30日 星期三

Arduino 之間的 I2C 通訊 (9) 實例(一) 簡單傳感數據收集

完成以上 8 章之後, 要進行 arduino 之間的 i2c 通訊, 應該沒有難度吧.

但有個問題, 為什麼要以 i2c 通訊呢?

因為有很多原因, 單一片 arduino 不足以滿足要求, 比如 I/O 不足, 又或有些程式太煩覆, 希望可以拆開, 以多片 arduino 合成一個大的系統.

比如你想建立一個超多傳感的測量系統, arduino 的 I/O 絕對不能滿足.  而且, 要安裝不同的庫, arduino 的記憶體也是一個限制.

如果可以把工作分開, 由多個 arduino 板子組成一個更大的系統, 當中由一個 master 板子, 執行主程式, 其他 slave 去配合讀取不同的資料.

當然, 現在的 arduino, 一般只有一組 i2c, 所以整個系統就只可以有一組 i2c bus.
而在  i2c bus 上只可以有一個 master, 所以 i2c 有關的設備, 都只可以靠 master 去處理.
另外, 相同 i2c 地址的設備, 不會因為加入多塊 arduino 板子, 而可以同時使用.
這個缺憾, 需要有另一個系統去配合.
比如在  i2c 之外, 再加上SPI 的通訊, 就可以把 i2c 都分開了.
這個複雜的系統, 之後再去探討吧.  現在以一個 i2c bus 為基礎去開始.

首先, 嘗試做一個簡單系統.  假設你的板子 I/0 都用盡了, 你還想加入一個 DHT11 的溫濕度測量.  你可以簡單加入一片 mini 或 nano 的板子, 以 i2c  跟主板連上, 去擴充你的 I/O.

最簡單是修改 "由 master 向 slave 要求資料回傳" 的例子.
當 master 向 slave 發出請求時, 配合 "(8) 浮點的傳送" 的例子, 就可以把 溫濕度 數據以直接回傳.

由於某些 設備的庫, 不一定可以在 interrupt 內執行的, 可以嘗試在 slave 的 loop 內不斷更新, 當收到 master 的請求時, 就把最後的資料發過去.

詳情大家可以在相關程式中看到, 執行後, 在 master 隨意發送一些資料, slave 就會回傳.



而 slave 的 serial monitor 亦會同時顯示出已發送的資料供對比:



用類似的方法, 就可以把 非 i2c 的傳感, 放到另一片 arduino 中去讀取數據, 甚至先進行簡單分析.  例如 濾波.
一片 arduino slave 板子, 可以同時接上多個 非 i2c device 呢.  其主力工作, 就是為主程式準備所需的資料.

當 主程式需要時, 就可以向 slave 直接讀取了.  主程式亦不需要加入有關的庫, 會變得簡單一點.

當然, 這不是沒有付出的, 系統上, 就要增加了 arduion 板子.  是否值得, 還是看需要吧.


相關程式下載:




Arduino 之間的 I2C 通訊 (8) 浮點的傳送

之前有點忙, 在準備 "Arduino 之間的 I2C 通訊 (7) 單片機有效傳送數據的選擇" 中, 突然停了一下, 沒想到一停就停了一年多.

當中留下了一個問題給看官思考的, 就是有關 浮點的傳送.

網上經常會看到, 有人問用 串口通訊時, 如何把 浮點 的數據送出去?
一般的答案, 都會是轉成字串發送吧, 例如 "123.45".
如果需要的浮點, 有固定的大小, 精確度有限, 這個有可能失真的方法還不錯吧.
為什麼說"失真'呢?  因為原本的數值, 小點後可能是連續很長的, 要轉成字串, 就要把長度限制了, 自然會有失真.

但大家有否考慮過, 可以簡單又完全不失真的傳送呢?
注意, 這裡的"不失真", 只是跟原來儲存的數值比較, 並非所有數值都可以.
如果原來儲存的數值已經是有"失真"的話, 傳送時也只會原原本本的發過去.
這是什麼意思呢?
簡單說, 要記錄 pi, 用 float 本身就有精確度的限制, 發送出去的 float, 絕不可能突然變成 double 的精度吧.  可以做到的, 就是你給我一個 float, 我就把這個 float 原整的發出去.  詳細情況, 可以在最後的例子中看到.

如果要轉成字串, 要用多少個字符才足夠呢? 100? 1000? 就是一萬也不一定是完全一樣.

但如果有考慮過電腦內的記憶體的運作,  無論發送什麼數值的 float, 也只需 4 個 byte 就可以了.
為什麼? 發送 123.45 有 5 個數字, 還要記下小數位, 也只需 4 bytes?

沒錯, 就是 4 bytes, 這沒有什特別, 因為原本用來儲存 float 的記憶體, 就只需要 4 bytes.
只要我們把這 4 個 bytes 發出去, 就可以原原本本的把那個數值傳過去了.

但怎樣可以把這 4 個 bytes 發出去, 在 接收端得到一個 float 呢?

看看 wire 或 serial 有關的 function, 都沒有以 float 作為參數或結果的, 那可以怎樣做到.

學習 c 的人, 應該不會有問題吧.  c 的 pointer 可是超好用的東西.

大家可以看看 wire 的庫, write 的方法之中, 其中一個就是:
virtual size_t write(const uint8_t *, size_t);
只要把 const uint8_t * 指到要發出的 float 去, size_t 設定為 4, 不就可以把 float 的 4 個 byte 發出去了嗎?

比如 你的資料儲存在   float data = 123.45678, 當中 data 是一個 float.
只要用  wire.write((uint8_t *) &data, 4)  就可以把它的值, 原原本本的發出去.

注意:  由於 data 是 float, &data 是 (float *), c 是不會主動把 (float *) 轉成 (uint8_t *) 的, 所以需要自己指定轉換.


之後, 在接收端收到 4 個 byte, 用相反的方法, 把 4 個 byte 用 pointer 放進 float 對應的記憶體, 就可以還原了.

文字很難清楚說明, 還是直接看程式碼吧.


slave 回傳 float

以下是一個收到任何請求, 都回傳一個 float 數值的例子,

#include 
 
#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 115200 
 
#define I2C_BUFFER_SIZE 32  
uint8_t i2cBuffer[I2C_BUFFER_SIZE];
uint8_t i2cBufferCnt = 0;

float data = 123.45678;
 
void setup() {
  Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
  Wire.onRequest(requestEvent); // register event
  
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Slave.08 started\n");
}
 
void loop() {
}
 
void requestEvent()
{
  Wire.write((uint8_t *) &data,4);
}



master 接收 float

下面是對應的 master 程式例子:

#include 
 
#define SLAVE_ADDRESS 0x12
#define SERIAL_BAUD 115200 
#define DATA_SIZE 4
 
void setup()
{
  Wire.begin();
  
  Serial.begin(SERIAL_BAUD);
  Serial.println("I2C Master.08 started");
  Serial.println();
}

float data = 0;
  
void loop()
{
  if (Serial.available()) {
 
    Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
    Wire.beginTransmission(SLAVE_ADDRESS);
    data = 0;
    uint8_t *ptr = (uint8_t *) &data;
    if (Wire.available()) {
      Serial.print("Data returned: ");
      while (Wire.available()) {
        uint8_t b = Wire.read();
        *ptr++ = b;
        Serial.print(b, HEX);
        Serial.print(" ");
      }
      Serial.println();
      Serial.print("Data value: ");
      Serial.println(data, 6);
    }
    Wire.endTransmission();
    while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
  }
 
}


以上範例, 只在於說明如何發送參數給 slave 使用, 當中並沒有加入錯誤的檢測.

執行之後, 你會得到以下結果:

Data returned: DF E9 F6 42
Data value: 123.456779


DF E9 F6 42 就是記憶體中, 用來儲存 123.45678 的 4 個 bytes 了.

但...為什麼 Data value 是 123.456779, 而不是 123.456780 呢?  不是說不會"失真"嗎?

這裡那 0.000001 的差別, 並非傳送時的失真, 而是 float 本身的失真的.
在 arduino 的 float, 是不能準確記下 123.456780 的, 其精確度所限, 會記錄成 123.456779.
正如上面說過, 傳送過程是會原原本本的發過去, 結果就是 123.456779 了.

不信的話, 你可以試試在 slave 的程式中, 把 data 以 6 位小數印出來看看吧.




相關程式下載:


2017年8月22日 星期二

顯示器(一) Nokia 5110 (84x48 LCD)

Nokia 5110 既 LCD 都有好多唔同款既, 而 pin 位都有少少分別.
小弟買左既就係呢款, 接線亦會以呢款為跟據:




基本資料:



相關資料下載:



    接線方法:

    除特定連接 (例 D3-D7) 外, 一般例子都以 SPI 接法, 轉化如下:

    5110MappedUNO
    RSTRSTD11
    CECSD12
    DCDCD10
    DinMOSID9
    ClkSCKD8
    VccVcc3.3V/5V
    BLBL3,3V
    GndGndGND

    (以上連接, 同一般 SPI 庫用 D10-D13 有D唔同, 但搵唔番出處了, 有待再驗證.)


    好可惜...我塊 5110 唔知乜事死左, 要買過塊再試.

    2017年3月17日 星期五

    Arduino 溫度濕度 DHT11

    測量溫度, 濕度 基本上係大路野, 而比較便宜既傳感, 應該可以話係 DHT11 了.
    一般平既可以買到有燈同無燈既版本, 多盞燈大約會貴 1 蚊左右, 但唔會話準左既.


    有樣特別, 唔知係咪我買到既有問題, 有燈同無燈, 安裝係相反既.
    至於邊隻準D, 真係唔知喇.  但係 DHT11 既模塊, 讀數都幾參差.
    我買左三隻, 其中一隻有燈, 用  Adafruit 既 庫, 三隻既讀數都有差別.
    溫度係 22-25 度, 濕度係 44-48%, 比較特別係 溫度高左既濕度就比較低.
    由於我都唔知正確既溫度同濕度係幾多, 無辦法比較.
    遲下買隻號稱準好多既 DHT22 番黎, 再比較一下.


    基本資料:


    電壓:3.3-5V
    溫度測量範圍:20%-95%(0度-50度范围)误差:+-5%
    濕度測量範圍:0度-50度 温度测量误差:+-2度
    输出形式:数字输出

    相關資料下載:
    DHT11  (AOSONG 版本, D-Robotics 版本, 中文版本)

    DHT11 既庫太多了, 暫時用個大路既, Adafruit 出品.



    接線方法:

    得三隻腳, {+,Out,-} 或 {VCC, DATA, GND} , 由於係 數字輸出, 接 D? 腳, 測試用 D2.

    UNODHT11
    D2Out/Data
    3.3V / 5V+/VCC
    GND-/GND


    測試程式:

    在 Adafruit 庫中, 已有 DHTtester 的程式.

    2017年3月13日 星期一

    Arduino 時間系列 (二) DS1307

    除左 DS3231 外, 比較常用既時鐘模塊應該算係  DS1307


    基本資料:

    電壓:3.3V


    相關資料下載:
    DS1307 既庫又係超多, 以下只係我用開既, 唔代表係最好既


    DS1307 Datasheet

    接線方法:

    I2C 的標準接法.

    UNOHS-SR04

    SQ

    DS
    A5SCL
    A4SDA
    3.3VVCC
    GNDGND

    BAT


    測試程式:

    在相關庫的 examples 中.
    
    

    Arduino 時間系列 (一) DS3231

    今次要講既係時鐘模塊  DS3231


    基本資料:

    電壓:3.3V


    相關資料下載:
    DS3231 既庫真係超多, 以下只係我用開既, 唔代表係最好既
    DS3231 (來源: https://github.com/NorthernWidget/DS3231 )
    RTClib  (來源: https://github.com/adafruit/RTClib )
    DS3231 Datasheet

    接線方法:

    I2C 的標準接法.

    UNOHS-SR04

    32K

    SQW
    A5SCL
    A4SDA
    3.3V / 5VVCC
    GNDGND


    測試程式:

    在相關庫的 examples 中.

    
    

    2017年3月5日 星期日

    旋轉編碼器 KY-040

    呢隻野, 真係比佢玩死.  之前做 CNC 手控時, 買左黎玩.

    但係寫完程式測試, 一直都唔可以成功量度到轉動既訊號, 經常無故自動亂跳.

    最近手痕再攞出黎玩, 見 Youtube 有幾清楚既教學 (https://www.youtube.com/watch?v=J9cDEef0IbQ), 就跟住做一次.
    點知, 結果都係一樣.  其實個程式同我之前既做法差唔多.

    心諗無理由我買幾人個都係壞既, 而且後尾發現, 原來只要我隻手放迎近佢就會亂跳, 好奇怪.  開始懷疑係線路上有問題.

    用另一部機試, 完全正常...omg, 比佢玩左我好耐, 原來係部機既 USB 有問題.
    只係知有問題, 唔知乜野問題.
    再試, 改下個程式用 TFT 輸出, 外部比電, 完全正常.
    之前買左個 USB 数字隔离器 諗住比部 CNC 用, 但最後都唔關事.  手痕就攞出黎試下, 果然係掂.  似係 arduion 受電腦經 USB 干擾....

    "肺話"講完, 入正題, 講番點用, 唔係第日又唔記得.

    測試程式可以用番上面條 Youtube 片果個, 又或者用我改左經 UsartGPU TFT 輸出既.

    接線方法:

    UNOKY-040
    D3CLK
    D4DT
    D8SW
    5V+
    GNDGND

    注意, 呢度既 CLK 同 DT, 其實個名有點亂, 應該即係兩個感應器既輸出, pinA 同 pinB.
    佢既原理在 Youtube 片講得好清楚了, 主要係靠兩個差 90度既感應, 從而量度出旋轉既步數, 及推算方向 (順時針/逆時針).  所以, 就算 CLK 同 DT 交換插都唔會有問題, 只係可能順/逆既方向調轉左.

    程式中以 CLK 去觸發 Interrupt, 所以如果 D3 有其他野用左, 只可以轉用其他有 interrupt 既 pin.
    有關 Interrupt pin 既選擇, 可以去 呢度 睇.

    測試程式:
    KY040.ino   (原網址, 私人收藏)

    2017年2月27日 星期一

    ILI9341 3.2 TFT

    Using Rinky-Dink's UTFT Library:  UTFT myGLCD(ILI9341_16, 38, 39, 40, 41);


    From TFT to Due:

    TFT-Pin Lable Due
    1 CS 40
    2 RS 38
    3 WR 39
    4 RD 3.3V
    5 RST 41
    6 DB0 37
    7 DB1 36
    8 DB2 35
    9 DB3 34
    10 DB4 33
    11 DB5 32
    12 DB6 31
    13 DB7 30
    14 DB8 22
    15 DB9 23
    16 DB10 24
    17 DB11 25
    18 DB12 26
    19 DB13 27
    20 DB14 28
    21 DB15 29
    22 SDCS --
    23 BL 3.3V
    24 VDD 3.3V
    25 VDD 3.3V
    26 GND GND
    27 GND GND
    28 NC --
    29 MISO 3
    30 MOSI 4
    31 PEN 2
    32 F_CS --
    33 T_CS 5
    34 CLK 6


    From Due to TFT:

    Due Lable
    2 PEN
    3 MISO
    4 MOSI
    5 T_CS
    6 CLK
    22 DB8
    23 DB9
    24 DB10
    25 DB11
    26 DB12
    27 DB13
    28 DB14
    29 DB15
    30 DB7
    31 DB6
    32 DB5
    33 DB4
    34 DB3
    35 DB2
    36 DB1
    37 DB0
    38 RS
    39 WR
    40 CS
    41 RST
    -- SDCS
    -- NC
    -- F_CS
    3.3V RD
    3.3V BL
    3.3V VDD
    3.3V VDD
    GND GND
    GND GND


    References: 

    2016年5月6日 星期五

    Arduino 之間的 I2C 通訊 (7) 單片機有效傳送數據的選擇

    這個章節的重點, 不在於 I2C 通訊, 但却是通訊中常見的問題.

    要完成一個通訊, 最基本會有兩個主角, 發送者 及 接收者.
    兩者之間, 只要有著共同的通訊協定, 就可以簡單達成通訊了.

    以下的例子中, 在表達傳送數據的時候, 為了分辨一個 byte, 以及文字的數據, 將會用 [xx] 表示一個數值為 xx 的十六進數據 (有需要時, 我會加上 {...} 去顯示二進值), 例如 [31] 即數值為 0x31 的一個 byte.  而要表達一些文字時, 就會用 "..." 去表達, 例如 "A" 就是一個 A 字, 其 ACSII code 為 65, 實際送出 0x41.  所以 [41] 跟 "A" 將會是相同的東西.



    通訊協定的選擇

    (1) 單一字節
    對於發送單一字節的訊息, 大家可能會覺得沒有什麼協定吧, 就是一個 write, 另一個  read 就完成.
    其實, 當中對通訊的資料, 也有一個協定了, 就是發出的資料, 長度是一個 byte.
    資料的長度, 也是協定的一種.

    例: 如要送出 x=30, 就送出以下一個  byte

    [1E]

    (2) 簡單合成
    比如我要發送 一個物體的 坐標 (x,y,z 均為 0-1023 的整數),  每個坐標可以用 2 byte 去發出.
    最簡單的方法, 就是定了資料的次序, 每個數據以 H L 的次序送出 2 byte, 連續發出 6 個 byte, 那這個協定就可以看成是:

    [X][X][Y][Y][Z][Z]

    例:  當 x=30, y=123, y= 567 時, 就會送出

    [00][1E][00][7B][02][37]

    (3) 轉化合成
    另一個方式, 考慮到 0-1023 的數值, 只用了 10bits, 3 個數字加起來, 也只有 30 bit.  如果認為通訊時間比較重要, 不介意多做前後期工作, 甚至可以把數據先合成 4個 bytes 再送出去.
    最後餘下的 2個 byte, 還可以用作 checksum.
    這個協定, 就變成是:

    {xxxxxxxx}{xxyyyyyy}{yyyyzzzz}{zzzzzzcc}

    例:  當 x=30, y=123, y= 567 時, 就會送出
      30 = 0x001E = 0000011110b
    123 = 0x007B = 0001111011b
    567 = 0x0237 = 1000110111b
    cc = 00 (暫定不作任何計算)
    {00000111}{10000111}{10111000}{11011100}

    即 發送以下 4 個 bytes 就可以了.
    [07][87][B8][DC]

    (4) 在數據中插入開始/結束碼

    有時, 數據的變化比較大, 比如要發送一段文字, 總不可能每次都固定為最大長度去發送吧.  當中加入結束碼, 可以更有效處理數據.   開始/結束碼的選擇, 也有一定的技巧, 一定要避開數據中會出現的所以可能性, 如果沒法避免, 就要加入特別處理方法.


    例:  當 x=30, y=123, z= 567, 結束碼為 [FF] 時, 就會送出

    [1E][FF][7B][FF][02][37][FF]

    萬一數據中有可能出現 [FF], 就要對數據加以處理, 例如數據中每一個 [FF] 就變成 [FF][FF].
    接收的時候, 就做一次還原, 每當碰上連續兩個 [FF] 就變成 [FF], 最後單獨的就是結束碼了.

    例如 x=255, y=123, z=567

    就會發送 [FF][FF][FF][7B][FF][02][37][FF]

    最前的兩個 [FF] 合成數據 [FF}, 之後單獨的就是 結束碼, 如此下去就可以了.

    (5) 固定長度文字

    以上都是站在電腦的角度去溝通, 但有時為了讓用家方便看到資料, 會把數據轉成人類可以看到的形式...就是完轉化成文字送出去.  上面的例子中, 數據是0-1023, 最簡單的方法, 每個數據用 4個 字符, 前面補 零, 合共 12 個字符.

    [X][X][X][X][Y][Y][Y][Y][Z][Z][Z][Z]

    例:  當 x=30, y=123, z= 567 時, 就會送出
    "003001230567"

    即送出以下 12 個 byte:

    [30][30][33][30][30][31][32][33][30][35][36][37]

    (個人認為, 對 MCU 而言, 實在不值得)

    (6) 在文字中加入開始/結束碼

    跟 4 有點相似, 上例中 x=30 時, 前面浪費了 2 個 byte, 有些時間, 如果數據的變化比較大, 用固定長度會出現更大的浪費.  比如數值由 0-10000000, 因為要滿足所有數值, 就要預留 8 個 byte, 如果正常數據只是 100 以內, 那大部份數據都要浪費 6 個 byte 了.  所以, 有些協定, 就會加入 開始/結束 碼, 把幾個數提分開.  注意, 為免誤認 開始/結束碼, 一般會使用數據中不會出現的字符.  例如上面要傳送數字, 就絕不會用數字作開始/結束 碼了.  但當要發送二進數據時, 就要像 (4) 的對數據進行處理.

    如果只用結束碼 [#] 的話, 上面的協定就變成:

    [X]*[#][Y]*[#][Z]*[#]

    例:  當 x=30, y=123, z= 567 時, 就會送出
    "30#123#567#"

    如果要發送二進的數據, 因為開始/結束 碼有機會在數據中出現, 就要進行特別處理.
    例如, 當數據中出現 "#" 時, 把每一個 "#" 就變成 "##".  接收程式只會在收到 基數的連續 "#" 才會把最後一個看成是結束碼, 

    例如: "12#3", 就會送出 "12##3#", 接收時, 因為第一次的 "#" 是連續兩個 "#", 就會變回數據中的一個 "#", 而最後一個是單獨的, 就是結束碼了.  

    (7) 加入更多資訊

    慢慢發展下去, 有些人會覺得, 這種固定次序的方式, 對程式更新有限制.
    比如 將來 加減資料, 新舊程式難以共存.  對於一些公用的服務, 會有很大影響.
    經過不斷改良, 就出現了像 XML 的格式, 每一個資料都加上一個名稱.  讀取資料就更準確, 不易出錯了.  將來改變參數也不會有大問題.

    比如上面的例子上, 要發送一個位置 x=30, y=123, z= 567 , 可以送出以下字串:

    <location><x>30</x><y>123</y><z>567</z></location>

    非常清楚的表達了要發送的資料, 以及每個資料的意義, 很好吧?
    但請細心想想, 對通訊而言, (2) 的簡單合成固定長度位置發出去, 好在那裡?

    以 MCU 為中心去作出選擇

    機械不是人類, 只要有清楚的協定, 就會明白數據的意義.

    對桌面機或大型電腦而言, 丁點禿的數據處理, 沒什麼大不了.
    但在 MCU 上應用, 只會是加重負擔, 可以說是畫蛇添足的門面功夫.  既浪費資源, 亦沒有好處.

    以上的方法中, (3), (5), (6) 及 (7) 對 MCU 而言, 都只會做成不必要的負擔.  個人不贊成用在 MCU 的通訊上.

    (3) 的方法可以省點通訊時間, 但合成數據除非本身有簡單硬件去做, 又或數據的來源就是合成的, 不需額外處理, 否則, 對一般通訊而言, 未必有大幫助.  但對程式的要求會相對提高了, 所以不贊成初學者使用.  而且, 執行上也要浪費更多資源去處理數據的合成及還原.

    (5),(6),(7) 可能是比較多人會喜歡用的, 特別是 (6), 例如用 "30,123,567" 去發送資料, "看"起來不錯吧.
    但這個"看"法, 只在於用人眼去"看", 對MCU而言, 一點也不好"看" 呢.

    而 MCU 一般用途比較專一, 將來出現大變化的機會不會太大, (7) 的方法實在是太浪費了,  MCU 的程式, 應該要更重視資源的運用.  那些多餘的 tag header, 實在要不得.

    一般而言, 個人會選擇 (2) 或 (4), 又或是混合使用.  如果數據是固定的, (2) 可以說是最快捷簡單的方式.  有時可以加入開始/ 結束碼, 去防止傳送時出現問題, 而誤認數據, 就更加安全了.

    日常生活中, 網絡發送的數據包, 它的 header 也是用固定長度的數據送出的.

    以下是 IP Header 的資料格式的例子:

    如果你把數據拿來看, 只會是一堆難以直接理解的二進數值.
    但對電腦系統而言, 要看這樣的數據, 比看文字容易得多呢.  接收端可以直接讀取 第 3,4 個byte 去獲取資料的總長度, 第 10 個 byte 去辨別類型, .....

    如果轉成了文字發送, 你自己可能會容易看到資料的意思, 例如 "TotalLength:128; Protocol:TCP", 但系統要了解當中的意思, 可要花費不少資源呢.
    通訊用的資料是給系統看的, 應該先考慮那個方法對系統更好, 可不要 喧賓奪主.

    注意:
    我並不是說 XML 不適合用作通訊, 在大型的系統上, XML 也有很大的好處的.
    只是因應 MCU 的限制, 以 XML 格式進行通訊, 絕非一個好的選擇.


    發送資料的方式

    但發送數據, 並非只有一個個 byte 的, 不同的數據, 又如何轉成 byte[] 去發送呢?
    網上經常會看到, 有人問用 串口通訊時, 如何把 浮點 的數據送出去?
    很多人都會選用 文字的方式, 例如 "123.45" 送出去.
    如果精確度是固定的話, 這個方法還勉強可以, 如果精確度不定, 那要轉成多少個位數才合理呢?

    如果大家可以把自己是人類的前提取下, 把自己當成是一台電腦, 問題就會簡單得多了.

    為什麼?  先問問自己, 你要發送的資料, 發送之前是放在那裡的?  是用什麼形式去放?

    或許你會答我, 是放在 某個 變數中吧.  例如 float a = 123.45;  就是放在 a 那裡.
    那 a 是什麼?  那個 123.45 是怎樣放?  放在那裡?

    a 可以說是放在電腦內的記憶體的某些資料吧, 至於怎樣放置 小數, 這是大學課程中有教的吧.


    未完.....待續....

    2016年5月5日 星期四

    Arduino 之間的 I2C 通訊 (6) 由 master 提供參數, 再由 slave 作出相應的回復

    看了(5) 之後, 這個章節其實也沒大意義, 只是把 master 發出的東西加點變化吧.
    重點將會是下一章節中所提及, 如何有效地把數據送出 (不論是 master 或 slave 都一樣).

    為什麼說 (6) 是沒意義呢?  請大家再看看 (5) 吧:




    第 (5) 是 slave 可以提供不同的資料供 master 去選擇.
    而 (6) 是 由 master 把需要的參數提供給 slave 應用.

    如果認真去想, (5) 的範例中, 不是已經由 master 提供了一個參數嗎?
    對, 就是用來選擇有關資訊的 dataMode 吧.
    只不過, (5) 接收到那個 參數後, 只是用來分辨要回傳的資料, 而不是用來計算.
    但實際的作用是沒分別的, 就是由 master 提供了一個數據給 slave 吧.

    那為什麼又要多開一個沒意義的章節呢?

    首先, 很多朋友會覺得, 參數的作用會有所不同.
    其次, 在 (5) 的 選擇服務功能上, 基本上只是一個 byte 的數據, 簡單的送出去就行了.
    但 參數可以超過一個 byte, 有不同的類形(int, float, char[],...), 亦可以同時有多個參數.

    所以, 發送參數, 就有需要考慮到發送的方式.
    流程上都是一樣, 把要發送的東西, 用 Wire.write 送出去, 而接收的, 也就是不斷用 Wire.read 去接收.

    重點在於, 如何把不同類型的數據, 都用 Wire.write 發出去.

    同樣的做法, 也可以應用到串口通訊的.

    如果在桌面電腦上, 要作出通訊, 基本上用什麼形式也沒大問題, 因為基本上不需要考慮速度以及記憶體的問題.
    就以現在比較常見的 XML 通訊為例, 小小的幾個 byte, 做一個很大的 XML 發出去, 也是很平常的事.
    但在 MCU 上, 要準備及處理一個 XML, 需要浪費很大的資源, 實在是不值得的.

    下一個章節, (7) 單片機有效傳送數據的選擇, 將會針對 單片機的資源限制, 尋求既有效率又簡單的通訊方法.


    以下先用一個簡單的例子, 說明如何可以在 master 向 slave 提供參數.

    假設有一台 slave 服務機, 可以提供 AND , OR  及 XOR 的計算.
    由 master 送出 3 個 byte, 中間一個作為運算的選擇, 'A' = AND, 'O' = OR, 'X' = XOR.
    如運算不在 'A', 'O' 及 'X' 之內, 就會把結果定為 0xFF.
    而 slave 會回傳接收到的資料, 再加上運算結果.

    比如發出 0x31 0x41 0x35 就是要計算 0x31 AND 0x35 的結果 (0x31 & 0x35 = 0x31).
    Slave 就會回傳 0x31 0x41 0x35 0x31

    Master 的程式:


    #include <Wire.h>
     
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    #define DATA_SIZE 4
    
    void setup()
    {
      Wire.begin();
      
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Master.06 started");
      Serial.println();
    }
    
    byte data[4];
      
    void loop()
    {
      if (Serial.available()) {
         
        Wire.beginTransmission(SLAVE_ADDRESS);
        for (int i = 0; i < 3; i++) {
          data[i] = Serial.read();
          delay(1);
        }
        Wire.write(data, 3);
        Wire.endTransmission();
        while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
        
        Serial.print("Data sent: ");
        for (int i = 0; i < 3; i++) {
          Serial.print(data[i], HEX);
          Serial.print(" ");
        }
        Serial.println();
    
        Wire.beginTransmission(SLAVE_ADDRESS);
        Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
        if (Wire.available()) {
          for (int i = 0; i < 4; i++) data[i] = Wire.read();
          while (Wire.available()) Serial.print((char) Wire.read());
          Serial.println();
        }
        Wire.endTransmission();
    
        Serial.print("Data returned: ");
        for (int i = 0; i < 4; i++) {
          Serial.print(data[i], HEX);
          Serial.print(" ");
        }
        Serial.println();
    
      }
    }
    

    Slave 的程式:

    #include <Wire.h>
    
     
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
     
    byte data[4];
    boolean dataReceived = false;
    boolean dataReady = false;
    boolean dataReturned = false;
    
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
      Wire.onReceive(receiveEvent); // register event
      Wire.onRequest(requestEvent); // register event
      
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Slave.06 started\n");
    }
     
    void loop() {
      if (dataReturned) {
        dataReturned = false;
        Serial.print("Data received: ");
        for (int i = 0; i < 3; i++) {
          Serial.print(data[i], HEX);
          Serial.print(" ");
        }
        Serial.print(" => ");
        Serial.println(data[3], HEX);
      }
    }
     
    void receiveEvent(int count) {
      if (Wire.available()) {
        for (int i = 0; i < 3; i++) data[i] = Wire.read();
        while (Wire.available()) Wire.read();
        dataReceived = true;
        dataReturned = false;
      }
    }
     
    void requestEvent()
    {
      char op = (char) data[1];
      Serial.print("op = ");
      Serial.println(op);
      switch (op) {
        case 'A':
          data[3] = data[0] & data[2];
          break;
        case 'O':
          data[3] = data[0] | data[2];
          break;
        case 'X':
          data[3] = data[0] ^ data[2];
          break;
        default:
          data[3] = 0xFF;
          break;
      }
      Wire.write(data, 4);
      dataReceived = false;
      dataReturned = true;
    }
    

    以上範例, 只在於說明如何發送參數給 slave 使用, 當中並沒有加入錯誤的檢測.
    而且只用了簡單參數, 下一個章節將會探討給何發送不同的數據.

    相關程式下載:

    2015年8月1日 星期六

    Arduino 之間的 I2C 通訊 (5) master 向 slave 要求不同資料

    相關指令:

    指令發出耆作用
    Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
    Wire.beginTransmission(<address>);master開始對 <address> 的連線
    Wire.endTransmission();master 關閉之前的連線
    Wire.requestFrom(<address>,<len>);master在線上向拍定地址發出回傳 <len> bytes 資料的請求
    Wire.available();master檢查連線上是否有可接收的資料
    Wire.read();master / slave讀取連線上的一個 byte 的資料
    Wire.write(<array>,<size>);slave / slave在連線上送出 <size>個 byte 的資料
    Wire.onReceive(<function>)slave 設定用來接收資料的函數
    Wire.onRequest(<function>)slave 設定函數用來回應線上的請求

    這個可以說是之前的一個初步小總結了.

    從 (4) 已經說明了 slave 如果把資料回傳, 但大家可能會發覺, 某些 i2c device, 是可以選擇不同的資料的, 在處理請求的函數中, 如何得知 master 想要什麼?

    對, 在 request 之中, 是沒有任何有關請求的資料提供的, 就只是一個空白的請求.

    但就像 MPU6050 之類的模塊, 不是可以選擇不同的數據嗎?
    如果大家有用過 I2CDev 的庫, 或許會發覺 readByte 的函數, 不是有一個 regAddr, 向 slave 要求不同的數據嗎?

        static int8_t readByte(uint8_t devAddr, uint8_t regAddr, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);

    大家有看過之前 (1) - (4) 的話, 難道想不出來嗎?  多思考一下, 把所知道的結合起來.

    對了, 說穿了就只是把兩個工序結合起來.

    • master 先向 slave 發送所需資料的指令
    • slave 收到後, 先記下來, 不作處理
    • master 再向 slave 發出傳送資料的請求
    • slave 收到請求後, 先看看 master 剛發過來有關所需資料的指令, 再把相關資料回傳

    是否很簡單呢?

    master 向 slave 選擇所需資料

    明白了原理, 步驟也很簡單, 就是把 (2) + (4) 結合:

    1. 執行 beginTransmission 並指定接收指令的地址
    2. 以 Wire.write 把所需資料的指令發送出去
    3. 執行 endTransmission 完成指令發送
    4. 執行 beginTransmission 並指定連線地址
    5. 以 Wire.requestFrom 發出請求
    6. 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出
    7. 執行 endTransmission 作結束

    當中 3. 4. 是否有必要, 我也不肯定.  但為了把 發送 / 接收 可獨立一點, 先把發送完結或者會好一點.  而且, 3. 4. 之後, 有需要或許要加入一點 delay, 好讓 slave 處理資料.  當然, 看實際情況決定, 並不是必須的.

    以下是簡單的例子, master 發出請求時, 會先指定所需資料.


    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    #define DATA_SIZE 8
     
    void setup()
    {
      Wire.begin();
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Master.05 started");
      Serial.println();
    }
     
     
    void loop()
    {
      if (Serial.available()) {
        
        Wire.beginTransmission(SLAVE_ADDRESS);
        Wire.write(Serial.read());
        delay(1);
        Wire.endTransmission();
    
        Wire.beginTransmission(SLAVE_ADDRESS);
        Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
        if (Wire.available()) {
          Serial.print("Data returned: ");
          while (Wire.available()) Serial.print((char) Wire.read());
          Serial.println();
        }
        Wire.endTransmission();
        while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
    
      }
    }
    


    salve 向 master 回傳所需資料

    跟 master 一樣, 所需步驟, 同樣是把 (3) + (4) 結合起來就可以了.


    • 執行 Wire.onReceive 去設定接收資料的函數
    • 執行 Wire.onRequest 去設定處理請求的函數
    • 在 onRecevie 函數中, 以 Wire.available 檢查是否有資料需要處理.  
    • 要有複雜的程序, 先記下來, 留待主程式去做
    • 在主程序中檢查是否有 接收資料待處理
    • 在 onRequest 函數中, 跟據之前 onReceive 收到的指令, 以 Wire.write 把 所需資料回傳


    但這裡有一點要留意, 之前沒提及的.  就是當 master 發出 requestFrom 時, slave 是會觸發一次 onReceive 而當中是沒有資料的.  所以在 onReceive 當中, 必須要先檢查 Wire.available.

    以下是一個簡單例子, master 可以選擇 2 種資料, 而回傳結果如下:
    0 - 回傳 "Super169"
    1 - 回傳 "Apple II"
    其他 - 回傳 "WhoAmI"


    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    
    uint8_t dataMode = '0';
    boolean modeChanged = false;
    
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
      Wire.onReceive(receiveEvent); // register event
      Wire.onRequest(requestEvent); // register event
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Slave.05 started\n");
    }
    
    void loop() {
      if (modeChanged) {
        Serial.print("Change data mode to ");
        Serial.println((char) dataMode);
        modeChanged = false;
      }
    }
    
    void receiveEvent(int count) {
      if (Wire.available()) {
        dataMode = Wire.read();
        modeChanged = true;
        while (Wire.available()) Wire.read();
      }
    }
    
    void requestEvent()
    {
      switch (dataMode) {
        case '0':
          Wire.write("Super169",8);
          break;
        case '1':
          Wire.write("Apple II",8);
          break;
        default:
          Wire.write("Who am I",8);
      }
    }
    


    額外測試項目

    大家記得之前 (2) 時提及, 不要把 Serial.print 放在 receiveEvent 當中嗎?
    在之前的例子, 由於只要 receiveEvent 在工作, 不會有任何影響.
    但在今次的例子中, 大家看到為了把 data mode 的改變顯示出來, Serial.print 的部份放到主程式內.
    有興趣的朋友可以試試把它修改一下, 放回 receiveEvent 內, 看看會有什麼結果.


    相關程式下載:


    Arduino 之間的 I2C 通訊 (4) 由 master 向 slave 要求資料回傳

    相關指令:

    指令發出耆作用
    Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
    Wire.beginTransmission(<address>);master開始對 <address> 的連線
    Wire.endTransmission();master 關閉之前的連線
    Wire.requestFrom(<address>,<len>);master在線上向拍定地址發出回傳 <len> bytes 資料的請求
    Wire.available();master檢查連線上是否有可接收的資料
    Wire.read();master讀取連線上的一個 byte 的資料
    Wire.onRequest(<function>)slave 設定函數用來回應線上的請求
    Wire.write(<array>,<size>);slave在連線上送出 <size>個 byte 的資料

    之前已經試過由 master 向 slave 發送資料, 但對於一些像 傳感器的設備, 反而是需要由 slave 把資料回傳的.  有些傳感器可能需要一段時間去讀取資料 (例如 PM2.5 收集 30 秒數據), 如果在主控中執行, 可能會影響其他工序.  如果加一片 arduino 板子, 以 slave 形式去讀取資料, 當 master 發出請求時, 就可以直接把最後的結果回傳, master 就不用花太多時間了.  再者, 把程序分開後, 每個程序可獨立除錯, 而主程式亦變得簡單了.  就如 PM2.5 的例子, 把當中讀取及計算的部份後 master 中抽走, 就不用考慮因為要讀取連續數據而影響其他程序的問題了.

    由於 slave 是不能主動發動連線, 所以只可以等待 master 的請求, 然後把資料送出去.

    slave 回應請求

    像之前接收資料一樣, slave 會設定一個 函數去回應 master 的請求的.


    • 執行 Wire.onRequest 去設定處理請求的函數
    • 在該函數中, 以 Wire.write 把 資料回傳

    以下是一個簡單例子, 當接到請求後, 就回傳一句 "Super169"

    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    
    #define I2C_BUFFER_SIZE 32  
    uint8_t i2cBuffer[I2C_BUFFER_SIZE];
    uint8_t i2cBufferCnt = 0;
    
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
      Wire.onRequest(requestEvent); // register event
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Slave.04 started\n");
    }
    
    void loop() {
    }
    
    void requestEvent()
    {
      Wire.write("Super169",8);
    }
    


    master 發出請求 並 接收資料


    在 master 中發出請求及接收資料, 也只是幾個簡單步驟就可以了:

    1. 執行 beginTransmission 並指定地址
    2. 以 Wire.requestFrom 向指定地址發出請求, 並指定回傳資料的數目
    3. 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出
    4. 執行 endTransmission 作結束

    這裡大家可能會對 requestForm 有點疑問:

    1. 為什麼 requestForm 要提供地址?
    在 beginTransmission 中, 已指定了連線的地址, 其他地址是不能使用的, 為何還要指定地址?
    這個本人亦有疑問, 但在網上找不到確實的答案.  以下是本人的推測, 有錯的希望大家指出.
    首先, requestForm 是不需要放在 beginTransmission 之後的, 是可以獨立執行的, 所以必須要有地址.
    但為什麼又要放入 beginTransmission 之後呢?  可能是為了確保 slave 在回傳資料時, 可以合法使用連線.  
    2. 為什麼 requestForm 要提供回傳資料的大小
    在之前的通訊中, 大家也看到, slave 發送完畢, 是不需要執行任何指令的.  對於 Wire 而言, 是沒有明確的訊息得知 slave 已發送完畢.  所以只好在發出請求時, 設定回傳資料的數目.  在正常情況下, 一般有特別的通訊協定, 發出請求的一方, 都會知道回傳資料的數目, 所以不會有問題的.  如果真的是沒有限定的話, 例如通訊協定中, 以回傳的第一個 byte 回報資料長度.  這樣只好用盡 Wire 的 buffer 把資料都接回來, 之後再進行分析處理.

    以下就是一個前 slave 發出請求回傳資料的例子:

    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    #define DATA_SIZE 8
    
    void setup()
    {
      Wire.begin();
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Master.04 started");
      Serial.println();
    }
     
     
    void loop()
    {
      if (Serial.available()) {
    
        Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
        Wire.beginTransmission(SLAVE_ADDRESS);
        if (Wire.available()) {
          Serial.print("Data returned: ");
          while (Wire.available()) Serial.print((char) Wire.read());
          Serial.println();
        }
        Wire.endTransmission();
        while(Serial.available()) Serial.read();  // Clear the serial buffer, in this example, just request data from slave
      }
    
    }
    



    執行後, 在 master 一方的 serial monitor 不論輸入什麼, 都會向 slave 發出回傳資料的請求.  而 slave 回傳的字句, 會在 serial monitor 中顯示出來.


    相關程式下載:




    Arduino 之間的 I2C 通訊 (3) 由 master 向 slave 發送資料/發出指令 [slave 延遲處理]

    相關指令:

    指令發出耆作用
    Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
    Wire.beginTransmission(<address>);master開始對 <address> 的連線
    Wire.endTransmission();master 關閉之前的連線
    Wire.write(<data>);master 在連線上送出 一個 byte 的資料
    Wire.onReceive(<function>)slave 設定用來接收資料的函數
    Wire.available();slave檢查連線上是否有可接收的資料
    Wire.read();slave讀取連線上的一個 byte 的資料


    slave 延遲處理

    之前的一篇, 不是已經可以由 master 向 slave 發送資料, 而 slave 亦成功收到了, 為什麼又攪個 延遲處理出來?

    之前一篇, 處理 master 送來的資料, 都是在接收的函數之內.  但大家不要忘記, arduino 的主線, 是在 loop 之內執行的.  接收函數太大, 或會影響主程式進行, 而且, 有些時候, 主程式或許需要用到接收回來的資料.

    延後處理就是把資料放進緩存一樣, 讓主程式去處理.
    方法很簡單, 可以直接把資料放有有關變數, 又或用最通用的方式, 先放入一個 buffer 中.

    以下例子, 就是用最通用的方式處理,

    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    
    #define I2C_BUFFER_SIZE 32  
    uint8_t i2cBuffer[I2C_BUFFER_SIZE];
    uint8_t i2cBufferCnt = 0;
    boolean dataPending = false;
    
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
      Wire.onReceive(receiveEvent); // register event
    
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Slave.03 started\n");
    }
    
    void loop() {
      if (dataPending) {
        Serial.println("Receive Data:");
        for (int idx = 0; idx < i2cBufferCnt; idx++) Serial.print((char) i2cBuffer[idx]);
        Serial.println("\n");   
        dataPending = false;
      }
    }
    
    void receiveEvent(int count) {
      i2cBufferCnt = 0;
      while(Wire.available()) {
        i2cBuffer[i2cBufferCnt++] = Wire.read();
      }
      dataPending = true;
    }
    


    執行後, 跟之前的是沒分別的.
    當然, 你也可以嘗試把 i2c 的 buffer 加大或收細, 看看有什麼影響.


    相關程式下載 (master_03 跟 master_02 是一樣的)

    Arduino 之間的 I2C 通訊 (2) 由 master 向 slave 發送資料/發出指令 [slave 直接處理]

    相關指令:

    指令發出耆作用
    Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
    Wire.beginTransmission(<address>);master開始對 <address> 的連線
    Wire.endTransmission();master 關閉之前的連線
    Wire.write(<data>);master 在連線上送出 一個 byte 的資料
    Wire.onReceive(<function>)slave 設定用來接收資料的函數
    Wire.available();slave檢查連線上是否有可接收的資料
    Wire.read();slave讀取連線上的一個 byte 的資料


    通訊也有不同程度的, 先做一個最簡單的單向通訊.
    由於只有 master 可以主動發出通訊要求, 最簡單的就是由 master 向 slave 發送資料了.
    這個例子是針對一些操控裝置, 例如你做了一個 i2C 的舵機, 由 arduino 發出指令, 要它轉到指定的角度, 而舵機是不會回傳任何資料的

    程式同樣分開 mater 及 slave 的部份, master 發送, slave 接收.

    master 發送資料

    在 master 發送資料, 只有幾個簡單步驟就可以了:

    1. 執行 beginTransmission 並指定接收資料地址
    2. 以 Wire.write 把資料送出去
    3. 執行 endTransmission 作結束

    以下是一個簡單的例子, master 把 串口收到的資料, 發送給 slave.

    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    
     
    void setup()
    {
      Wire.begin();
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Master.02 started");
      Serial.println();
    }
     
     
    void loop()
    {
      if (Serial.available()) {
        Wire.beginTransmission(SLAVE_ADDRESS);
        while(Serial.available()) {
          Wire.write(Serial.read());
          delay(1);
        }
        Wire.endTransmission();
      }
    }
    


    slave 接收資料

    而 slave 接收, 是有點像 interrupt driven 的形式去做,  簡單的做法如下.


    1. 執行 Wire.onReceive 去設定接收資料的函數
    2. 在接收資料的函數中, 不斷以 Wire.available 檢查是否有資源, 並以 Wire.read 把一個 byute 資料提出, 直到所有資料提出後, Wire.available 為 false

    注意, 因為 Wire 庫的 buffer 只有 32 個 byte, 每次發送的資料應限制在 32 個 byte 之內.
    由於 master 進行 write 是沒有限制的, 如果發送超過 32 bytes, 之後的資料就會流失.

    以下例子, 是在 接收的函數中, 直接處理接收回來的資料.



    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    #define SERIAL_BAUD 57600 
    
     
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 1
      Wire.onReceive(receiveEvent); // register event
    
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Slave.02 started\n");
    }
    
    void loop() {
    }
    
    void receiveEvent(int count) {
      Serial.println("Receive Data:");
      while(Wire.available()) {
        Serial.print((char) Wire.read());
      }
      Serial.println("\n");
    }
    


    注意:  
    這個例子中在 receiveEvent 中用到 Serial.print, 只是為了簡單展示結果, 雖然這裡沒有問題, 但其實是絕不應該的.
    在正常的程式中, 應該盡量避免在 receiveEvent 中放入複雜的程序, 在下一章中會提及正確的做法.


    執行程式後, 在 master 板子連線的 serial monitor 輸入資料, 就會發送到 slave 板子, 再在相關的 serial monitor 顯示出來.


    相關程式下載:

    2015年7月31日 星期五

    Arduino 之間的 I2C 通訊 (序) 簡單介紹

    一直有興趣用不同的方式把細小的 arduino 系統組成一個大系統, 剛剛看了 幻生幻灭 大大 的 世界最小的Arduino——ATTiny13上手全攻略1A简介, 心思思想買回來試試.
    初步的目標, 是用 ATTiny13 把 一般舵機改成 總線舵機.  雖然已有 PCA9685 之顃 經 i2c 通訊的控制板, 但每個舵機還是直接連到控制板上, 接線相對比較長而且混亂.  如果可以做出總線舵機的效果, 每個舵機之間以三根線連上, 可以一個一個連開去, 又可以中途分開幾個, 又或者直接連到主板.  變化比較大, 而且方便很多.  所以, 還是值得嘗試的.

    在未有 ATTiny13 之前, 先研究一下 arduino 之間的 I2C 通訊吧.

    在網上不難找到教學, 但很多都只是以一個 byte 通訊, 不足以滿足我的要求, 所以嘗試自己做一些簡單的應用例子, 將來或許用得著, 希望對大家有幫助.

    由於網上有不少資源, 太深入的未必人人有興趣, 而且我自己也不慬, 所以, 嘗試用簡單的例子把一些重點拿出來討論.  如果說錯了什麼, 還望大家幫忙指證.

    I2C 通訊的特性

    首先 要了解 I2C 跟其他通訊(例如串口) 的分別

    • I2C 是 master & slave 的設計
    • 整個線路上只有一個 master, 其他的都是 slave
    • 只有 master 可以 向 slave 進行通訊
    • slave 與 slave 之間是不可以通訊的
    • 只有 master 可以主動向 slave 發送資料或提出請求
    • slave 只可因應 master 的請求而回傳資料, 不可以主動發送資料給 master


    I2C 通訊有什麼好處?


    • 可以同時以 1 master 連接多個 slave 設備 
    • 速度快
    • 不需另外購買通訊模塊, 可以說是完全免費的
    • 只需三根線連通就可以, 不用複雜的連線


    I2C 通訊有什麼缺點?


    • 由於 I2C 是 master 主導, 所有 slave 板子都不能主動提出通訊要求, 亦不能跟其他 slave 通訊.  程式設定上, 就要有一個主控制板, 其他都只是分工.  
    • slave 板子不能用來連接其他 i2c 設備 (除非大神們另外寫一個 Wire 庫, 用其他接口吧)

    因此, 分工的時間, slave 只可以分擔非 i2c 的設備, 對於 i2c 的設備, 還是要靠 master 自己負責.
    所以 master 除了要管理 slave 的分工外, 還要處理 i2c 設備.



    準備功夫:

    要做的 I2C 通訊, 只需要 Wire 庫就可以了 (當然, 對於大神來說, Wire 庫也可以不需要, 自己完全做出來也可以.), 所以基本上不需要再找什麼庫.

    當然, 如果配合 I2Cdev 之顃的庫, 一定程度上可以簡單一點.  但為了方便大家了解, 還是用最基本的 Wire 指令.   將來大家想用什麼庫也沒限制.

    我嘗試做一個 I2C 通訊系統的例子, 由簡單的一步步建立, 每一個段落的例子都可以獨立進行測試, 希望大家可以一步步了解.

    由於是通訊系統,  最少要準備 兩塊 arduino 板子, 不需要是相同的, 任何組合也可以 (UNO, Nano, Mini, Mega 也沒關係).
    三根杜邦線, 把 GND, A4, A5 都連上.  連接方法就是 相同的連起來.  GND-GND, A4-A4, A5-A5, 有多少塊都是接在一起就可以了.

    為了方便之後的討論, 請選定一個作為 master, 其他都是 slave.  以後會用 master 板子, 及 slave 板子作稱呼.


    暫定會有以下的題目, 將會一步一步發出, 完成後, 希望大家都可以做出一個簡單的 i2c 通訊系統.

    (1) I2C 地址設定 及 I2C 地址掃瞄
    (2) 由 master 向 slave 發送資料/發出指令 [slave 直接處理]
    (3) 由 master 向 slave 發送資料/發出指令 [slave 延遲處理]
    (4) 由 master 向 slave 要求資料回傳
    (5) master 向 slave 要求不同資料
    (6) 由 master 提供參數, 再由 slave 作出相應的回復
    (7) 單片機有效傳送數據的選擇
    (8) 浮點的傳送
    (9) I2C 通訊實例(一) 簡單傳感數據收集 (把非 i2c 傳感變成 i2c)
    (10) I2C 通訊實例(二) PM2.5 數據收集 (把較長時間的收集由 slave 完成)
    (11) I2C 通訊實例(三) 總線舵機 (由 master 向 slave 發送指令)


    以上只耍 (1) -  (4) 就可以了作出基本通訊, 而 (5), (6) 是加入一些簡單通訊協定, 讓 slave 有更多功能, 之後是一些實例, 用來演示 i2c 通訊的應用.

    相關程式只在演示通訊的功能, 對 Wire 的錯誤完是沒有處理的, 有興趣可自己加入錯誤處理的程序.

    Arduino 之間的 I2C 通訊 (1) I2C 地址設定 及 I2C 地址掃瞄

    相關指令:

    指令發出耆作用
    Wire.begin([<address>]);master / slave啟動 Wire (由於 i2c 是用 Wire 的, 這就等同啟動 i2c 了)
    Wire.beginTransmission(<address>);master開始對 <address> 的連線
    Wire.endTransmission();master 關閉之前的連線



    i2c 地址設定

    i2c address 就像是你家中的地址, 每個 slave 都有自己的地址, 由於線路上只有一個 master, 加上 slave 只可以向 master 通訊, 所以 master 是不需地址的.

    設定 i2c 地址, 基本上是沒有限制的, 只要同一線路上, 沒有重複就可以了.
    要設定 slave 板子的地址, 只需要執行 Wire.begin(<地址>); 而 master 因為不需要地址, 只要 Wire.begin() 就可以了,.

    以下是一個 slave 設定的例子, 把 slave 板子的地址設定為 0x12, 上載到 slave 板子上去執行就可以了.

    #include <Wire.h>
    
    #define SLAVE_ADDRESS 0x12
    
    void setup() {
      Wire.begin(SLAVE_ADDRESS);    // join I2C bus as a slave with address 0x12
    }
    
    void loop() {
    }
    


    i2c 地址掃瞄

    slave 建立後, 就要看看 master 如果找到它了.
    i2c scanner 可以說是 master 板子的最基本例子, 可以用作測試線路上連接了的設備的存在 (只測試存在性, 並非測試其功能).
    i2c_scanner 其實也很簡單, 由 master 向所有地址發出 beginTransmission 再 endTransmission, 嘗試建立連線.  在 beignTransmission 中輸入 slave 的地址, 就可以測試該地址的裝置了.

    如果 error = 0 (沒 error), 即代表這個地址有設備登記
    如果 error = 4 (這是 Wire 庫的設定, 不要問我為什麼是 4), 即代表這個地址可能有設備, 但有錯誤.
    否則, 就代表該地址沒有設備使用了.

    以下是一個簡單的 i2c scanner 程式:

    #include <Wire.h>
    
    #define SERIAL_BAUD 9600 
    
    void setup()
    {
      Wire.begin();
     
      Serial.begin(SERIAL_BAUD);
      Serial.println("I2C Scanner started");
      Serial.println();
    }
     
     
    void loop()
    {
      uint8_t error, i2cAddress, devCount, unCount;
     
      Serial.println("Scanning...");
     
      devCount = 0;
      unCount = 0;
      for(i2cAddress = 1; i2cAddress < 127; i2cAddress++ )
      {
        Wire.beginTransmission(i2cAddress);
        error = Wire.endTransmission();
     
        if (error == 0)
        {
          Serial.print("I2C device found at 0x");
          if (i2cAddress<16) Serial.print("0");
          Serial.println(i2cAddress,HEX);
          devCount++;
        }
        else if (error==4)
        {
          Serial.print("Unknow error at 0x");
          if (i2cAddress<16) Serial.print("0");
          Serial.println(i2cAddress,HEX);
          unCount++;
        }    
      }
    
      if (devCount + unCount == 0)
        Serial.println("No I2C devices found\n");
      else {
        Serial.println();
        Serial.print(devCount);
        Serial.print(" device(s) found");
        if (unCount > 0) {
          Serial.print(", and unknown error in ");
          Serial.print(unCount);
          Serial.print(" address");
        }
        Serial.println();
      }
      delay(5000);
    }

    在這個例子中, 應該可以得到以下的結果:
    I2C Scanner started

    Scanning...
    I2C device found at 0x12
    1 device(s) found

     I2C Scanner 是非常有用的, 當你買了一個新裝置, 如果送來的程式不成功, 先看看相關地址是否可找到裝置, 可簡單測試裝置是否有問題.


    相關程式下載:


    2015年7月18日 星期六

    SD Card 模塊

    SD Card 模塊, 都無乜特別, 咪就係用黎讀寫 SD Card 咁大把.
    有時用黎做下 logging 都幾好既, 又或者可以用黎放歌仔去播.


    睇睇樣先:



    基本資料:


    電壓:5V/3.3V
    接口:SPI


    相關資料下載:




    接線方法:


    UNOSD Module
    D4 / D10CS
    D11MOSI
    D12MISO
    SCKD13
    5V/3.3V5V/3.3V
    GNDGND

    注意:
    一般 CS 係接 D10 既, 不過官網上既例子係用左 D4.  所以要睇清楚去接.
    呢塊野可以用 5V 或 3.3V 既, 如果 UNO 就無所謂, 無樣都有, 否則就跟番你塊板有乜接乜.



    測試程式:

    基本上 Arduino IDE 本身已有唔少 (File->Examples->SD), 我都唔想多講.
    不過就左方便對比, 做左幾個測試結果出黎.

    首先搵左張 2GB 既 SD card (注意, 佢唔支援 SDHC 的, 一定要最弱既 SD).
    Format 用 FAT16, 再整左一個 ReadMe.Txt, 一個 Arduino 既 Folder, 入面有個 Dummy.Txt.
    全部 file 都係吉既, 只係為左測試用.

    注意, 因為部份 example 會寫 file, 呢個結果係由 吉card 順住做的.
    而當中有幾個會不斷 write 野, 中途停佢好似會出事.  個 Example 應該加個位比人 quit.

    執行 CardInfo 既結果:



    執行 Datalogger 既結果:
    (注意, 因為一 upload 完佢就行, 係咁 send 野, 有可能你一開始仲有幾個數字的)


    執行 Datalogger 既結果:
    (注意, 同樣一 upload 完佢就行, 係咁 send 野, 有可能你一開始仲有幾個數字的)


    執行 Files 既結果:


    執行 listfiles 既結果:
    玩完上面幾個 examples, 因為經常會 write 緊果下停, 有機會出現呢個 error, 今次剛好在測試 listfiles 時出現.  


    只好再 format 佢一次, create 番之前講既 file 再行.

    最後係執行埋 ReadWrite...好不幸, 出 error:

    抽出黎再試左幾次都係一樣.  開番張 card 黎睇, 無哂我之前 create 既野, 得番一個 TEST.TXT, 入面吉既.   咁又試下再 format 張 card, 吉既比佢行.  結果都係一樣, 放棄了.

    最後提提大家, 今次 Arduino IDE 入面既 Example, 真係好差.  SD card 既 read / write, 除非你做 自動系統, 否則 read / write SD card 最好有少少 UI, 避免 read / write 過程中 SD card 被拔走.

    之前自己做既 logging, 一個自動化系統, 每次 write 緊都會亮燈, write 完即 close file 等一等就熄燈.  而且每次再 write 都會有一定時間分隔.  除非死機, 否則自己見到亮燈就唔會停佢.