2021年6月12日 星期六

Arduino 讀取/寫入 SD 卡資料

隨著接觸越來越多的感測器,在 Arduino 紀錄感測元件接收的數值的同時,把數值儲存下來已經是非常重要的需求。除了透過網路模組送出以外,Arduino 開發板本身並不像 Raspberry Pi 有外接 SD 或 Micro SD 卡的介面,可以儲存在記憶卡上,要寫入資料到 SD卡上,就必須透過 SPI 的介面,才能將感測的資料寫入 SD卡。

接線圖
Arduino Uno×1、SD 卡模組×1、連接線×6
註:DI 代表 SD卡模組 MOSI,DO代表 SD卡模組 MISO
SD 卡模組連接線路

SD 卡模組Arduino
GNDGND
+5V5V
CSPin 4
MOSIPin 11
SCK(CLK)Pin 13
MISOPin 12

SPI匯流排規定了 4 個保留邏輯訊號介面:(維基百科)

  • SCLK (Serial Clock):串行時鐘,由主機發出
  • MOSI (Master Output,Slave Input):主機輸出從機輸入訊號,由主機發出
  • MISO (Master Input,Slave Output):主機輸入從機輸出訊號,由從機發出
  • SS (Slave Selected) 或 Chip Select (CS):由主機發出,低電位有效
SD Library

SD Class

  • 初始設定 SD卡及函式庫:SD.begin(cspin)
  • cspin (選項):Arduino 連接SD卡模組 SS 或 CS的 Pin腳
  • 檢查括號內的檔案名稱是否存在:exists()
  • 建立目錄:SD.mkdir(filename)
  • 開啟檔案:SD.open(filepath, mode)
  • mode:檔案開啟模式
    FILE_READ 開啟唯讀檔案,從檔案開始處讀取
    FILE_WRITE 開啟讀寫檔案,從檔案最後處開始寫入
  • 移除檔案:SD.remove(filename)
  • 移除目錄:SD.rmdir(filename)

File Class

  • 關閉檔案:file.close()
  • 將 data 字串寫到檔案中,不跳行:file.print(data)
  • 將 data 字串寫到檔案中,不跳行:file.println(data)
  • 將 data 字串寫入檔案中:file.write(data)
程式碼
#include <SPI.h>
#include <SD.h>
//--- #include <SdFat.h>
//--- SD.h 與 SdFat.h 為不同 Library 所提供,使用時擇一使用

File myFile;
//--- SdFat SD;
//--- 必須與 SdFat.h 搭配使用

//--- 設定 SD library 功能變數:

Sd2Card card;
SdVolume volume;
SdFile root;

const int chipSelect = 4;

void setup()
{
  Serial.begin(9600); //--- 開啟通訊串列埠開啟
  while (!Serial) {} //--- 等待串列埠連線

  //--- 寫入檔案
  Serial.print("\nWaiting for SD card ready...");

  if (!SD.begin(4))
  {
    Serial.println("Fail!");
    return;
  }
  kSerial.println("Success!");

  //--- 開啟檔案,一次僅能開啟一個檔案
  myFile = SD.open("card.txt", FILE_WRITE

  if (myFile)
  { //--- 檔案開啟正常
    Serial.print("Write to card.txt...");
    myFile.println("Test to write data to SD card..."); //--- 繼續寫在檔案後面
    myFile.close(); //--- 關閉檔案
    Serial.println("Completed!");
  }
  else
  { // 無法開啟時顯示錯誤
    Serial.println("\n open file error");
  }

  //--- 顯示 SD 卡資訊

  if (!card.init(SPI_HALF_SPEED, chipSelect))
  {
    Serial.println("initialization failed. Check: SD Card");
    return;
  }
  else
  {
    Serial.println("============= Card Information ==================");
  }

  //--- 顯示 SD 卡類型

  Serial.print("Card type: ");
  switch (card.type())
  {
    case SD_CARD_TYPE_SD1:
      Serial.println("SD1");
      break;
    case SD_CARD_TYPE_SD2:
      Serial.println("SD2");
      break;
    case SD_CARD_TYPE_SDHC:
      Serial.println("SDHC");
      break;
    default:
      Serial.println("Unknow");
  }

  //--- Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
  if (!volume.init(card))
  {
    Serial.println("Could not find FAT16/FAT32 partition.");
    return;
  }

  //--- 顯示類型和 FAT 空間大小
  uint32_t volumesize;
  Serial.print("Volume type is FAT");
  Serial.println(volume.fatType(), DEC);
  Serial.println();

  volumesize = volume.blocksPerCluster(); // clusters are collections of blocks
  volumesize *= volume.clusterCount();    // we'll have a lot of clusters
  volumesize *= 512;                      // SD card blocks are always 512 bytes

  Serial.print("Volume size (bytes): ");
  Serial.println(volumesize);
  Serial.print("Volume size (Kbytes): ");
  volumesize /= 1024;
  Serial.println(volumesize);
  Serial.print("Volume size (Mbytes): ");
  volumesize /= 1024;
  Serial.println(volumesize);
        
  Serial.println("\nFiles found on the card (name, date and size in bytes): ");
  root.openRoot(volume);
        
  //--- list all files in the card with date and size
  root.ls(LS_R | LS_DATE | LS_SIZE);
        
  Serial.println("================= Finished =====================");
}

void loop() {} //--- 在Setup()執行完成後,就等結束
SD Library Documents
  • SD.h Library

    The SD library allows for reading from and writing to SD cards, e.g. on the Arduino Ethernet Shield. It is built on sdfatlib by William Greiman. The library supports FAT16 and FAT32 file systems on standard SD cards and SDHC cards. It uses short 8.3 names for files. The file names passed to the SD library functions can include paths separated by forward-slashes, /, e.g. "directory/filename.txt". Because the working directory is always the root of the SD card, a name refers to the same file whether or not it includes a leading slash (e.g. "/file.txt" is equivalent to "file.txt"). As of version 1.0, the library supports opening multiple files.

    The communication between the microcontroller and the SD card uses SPI, which takes place on digital pins 11, 12, and 13 (on most Arduino boards) or 50, 51, and 52 (Arduino Mega). Additionally, another pin must be used to select the SD card. This can be the hardware SS pin - pin 10 (on most Arduino boards) or pin 53 (on the Mega) - or another pin specified in the call to SD.begin(). Note that even if you don't use the hardware SS pin, it must be left as an output or the SD library won't work.

  • SdFat.h Librry

    The Arduino SdFat Library is a minimal implementation of FAT16 and FAT32 file systems on SD flash memory cards. Standard SD and high capacity SDHC cards are supported.

    1. The SdFat only supports short 8.3 names.
    2. The main classes in SdFat are Sd2Card, SdVolume, and SdFile.

    The Sd2Card class supports access to standard SD cards and SDHC cards. Most applications will only need to call the Sd2Card::init() member function.

    The SdVolume class supports FAT16 and FAT32 partitions. Most applications will only need to call the SdVolume::init() member function.

    The SdFile class provides file access functions such as open(), read(), remove(), write(), close() and sync(). This class supports access to the root directory and subdirectories. A number of example are provided in the SdFat/examples folder. These were developed to test SdFat and illustrate its use. SdFat was developed for high speed data recording. SdFat was used to implement an audio record/play class, WaveRP, for the Adafruit Wave Shield. This application uses special Sd2Card calls to write to contiguous files in raw mode. These functions reduce write latency so that audio can be recorded with the small amount of RAM in the Arduino.

  • Arduino SD記憶卡模組之讀與寫

再論 - 微型物聯網 (IoT) 架構 - MQTT

IoT 最重要的莫過於網路通訊了,機器與機器 ( M2M ) 間需要透過 HTTP 來傳輸資料,了解比較深入的人,可能還知道 CoAP 或 MQTT 等。

MQTT 是一種物聯網的通訊協定,最初是由 IBM 和 Eurotech 主導開發,並已在 2014 年正式成為了 OASIS 國際標準,開發的目的是為了在窄寬帶以及低耗能條件下,傳送與接收處理訊息,採用 Publish/Subscribe 的方式,透過 Broker 做訊息溝通。

MQTT 的標頭 ( header ),僅佔 2 個 bytes ,縮小了傳輸量。整體來說,資料封包也比要傳送同等訊息的 HTTP 來的小,這種特性讓 MQTT 非常適合運用在現行的 IoT 架構中。

Publisher、Subscriber、Broker 又是什麼?

Publish / Subscribe 為一種訊息規範的模式 :

發布者:Publisher,不會將訊息直接傳送給訂閱者:Subscriber,而是將訊息分為不同的主題:Topic,訂閱者只接收已訂閱的主題。

MQTT 即是採用 Publish/Subscribe 模式,其中 Publisher 和 Subscriber 都是用戶端(Client),Broker是伺服器端(Server) 負責轉發 Topic。

Publish / Subscribe 示意圖

Subscriber 告知 Broker 想要訂閱的 Topic,每當 Publisher 發布訊息時,Broker 會依照 Topic,傳送給訂閱的 Subscriber。

由於 PublisherSubscriber 之間有 Broker 當作中繼站,所以兩邊並不需要知道彼此的 IP。

IBM Watson IoT Platform from https://www.ibm.com/developerworks/
Topic 主題格式

MQTT 的 Topic 是字串 ( String ),並支援階層式命名如下範例:

outside/temperature/temp_Device01
inside/humidity/humid_Device07
Taiwan/Taipei/Datong/ChengdeRoad/Traffic
...

階層數沒有固定,但是要特別注意的是,英文的大小寫是有區別的,且長度不可超過 65536 個字元。

品質 ( Qos ) 設定

MQTT 定義了三個層級的品質 ( Qos:0、1、2 ),分別適用於不同的情況:

Qos 0:at most once 最多傳一次
在「Qos:0」的設定下,訊息送出後就不管了,由於 MQTT 是屬於網路架構中的應用層,它並不會知道底層的網路斷線與否,所以 Broker 是有可能沒收到訊息的。如果你的目標裝置接的是有線網路,或者,遺漏幾筆資料也不會對結果造成太大影響的情況下 ( 樣本數夠多 ),可以使用這個設定。

Qos 0:示意圖

Qos 1:at least once 至少傳一次
Broker 從 Publisher 收到「Qos:1」的訊息之後,會回應一個 PUBACK,以確認有收到訊息。如果連線中斷或其他狀況導致 Publisher 沒有收到 PUBACK,Publisher 就會重新發送,保證訊息至少傳送至 Broker 一次。

Qos 1:示意圖
然而,假設 Broker 有收到訊息,但在回應給 Publisher 時,中間發生斷線或故障。Publisher 會認為 Broker 沒有收到而重送,結果導致 Broker 重複收到同一份訊息。
QoS1:Broker 有收到訊息 但 Publisher 沒收到 PUBACK

Qos 2:exactly once 確實傳送一次
「Qos:2」設定比「Qos:1」更嚴謹了一些。它將發送訊息分成了幾段:

Broker 收到「Qos:2」的訊息之後,將回覆 PUBREC 給 Publisher,表示收到了即將發布的訊息,並暫存訊息的封包識別碼,以防止因斷線或逾時,需重新傳送而造成的重複。

Publisher 如果收到了 PUBREC,會再傳送 PUBREL 給 Broker,告訴 Broker 可以釋放訊息了。此時 Broker 會把訊息傳送給 Subscriber,然後回應 PUBCOMP 告訴 Publisher 已經發送完畢,並刪除暫存訊息。

Qos 2:示意圖

相較於「Qos:1」,「Qos:2」會佔用比較多的網路和傳送時間,但能確實傳送一次訊息。

最後,究竟為什麼要使用 MQTT?
IoT picture from https://www.freepik.com/

在 IoT 的世界裡,末端的裝置動輒數百上千,一來一往的數據傳輸量很是驚人,流量那麼大的情況下,網路使用費計算起來也是相當可觀,並不是每個地方都可以享有吃到飽的服務,因此,封包傳輸量較小、能一對多的 MQTT 就成了主流之一。

除此之外,品質 Qos 的設計也可以讓不同的裝置甚至不同的架構需求,使用合適的品質設定來傳送資料,以符合最佳的應用情境。

2021年6月10日 星期四

SPI (Serial Peripheral Interface) 串列 (序列) 週邊介面

隨著科技的演進,在單晶片微控制器及 SoC 的領域中,SPI 及 I2C 這二種串列 (序列) 介面變得十分常見。這二者與主機間通訊用的非同步串列通訊埠 RS-232 (UART) 非常不一樣。

  • 二個都是同步傳輸介面,主要是用於 CPU 和週邊晶片之間。
  • SPI 及 I2C 二者設計的主要目的在於減少 CPU 和週邊晶片之間的接腳數。
  • SPI 一般需要 4 條接線 (至少三條),而 I2C 則只要二條線,這和早期常用的並列匯流排動輒十數條接線有著明顯的差異。
  • SPI 的硬體結構簡單而且傳輸速度快,一般是 5M/10M/20Mbps 或是更快 (可以到 200Mbps),I2C 的傳輸速度則只有 100Kbps/400Kbps/1Mbps(/3.4Mbps/單向 5Mbps)。
  • SPI 是全雙工,I2C 則是半雙工.
  • SPI 使用硬體線路來指定 slave 晶片,I2C 則在傳送的第一個位元組上指定 (7bit位址)。
  • SPI 不提供交握機制,無法確認 slave 晶片是否有跟上。I2C 則有雙向的確認機制。

SPI 簡介

SPI 是 Serial Peripheral Interface 的縮寫,中文意思是串列週邊介面,該介面是由 Motorola 公司設計發展的高速同步串列介面,原先是應用在其 68xx 系列的 8 位元處理器上 (1985 年首次出現在 M68HC11 處理器上,並提供了完整之說明文件),用以連接 ADC、DAC、EEPROM、通訊傳輸 IC...等週邊晶片。由於具備有低接腳數,結構單純,傳輸速度快,簡單易用...等特性,目前已經成為業界慣用標準。不只是單晶片微控制器上有,許多新的 SoC 晶片直接就支援多組 SPI 介面,甚至普及到連模組化的產品 (如:手機用的 LCD 模組 (SDI 介面),相機模組) 及 3C 產品 (如: 數位相機用的記憶卡) 也都是使用 SPI 介面。

SPI 架構及介面接腳

SPI 為一主從式架構,通常有一個 Master (主設備) 和一個 (或多個) Slave (從設備)。介接方法及內部硬體結構很簡單, 如下面的示意圖:

SPI 結構示意圖

SPI 接腳名稱及意義

接腳名稱中文名稱說明
MOSI主出從入master 數據輸出、slave 數據輸入。
MISO主入從出master 數據輸入、slave 數據輸出入。
SCLK時脈訊號時脈信號、由 master 產生並控制。
/SS晶片致能slave 選擇信號,由 master 控制。slave 只有在 /SS 信號為低電位時,才會對 master 的操作指令有反應。

介接時只要把相同名稱接腳接在一起即可。

    SPI 的接腳有另一套常用的名稱:
  • SDO:Serial Data Out, 資料輸出 (不分主從)
  • SDI:Serial Data In, 資料輸入 (不分主從)
  • SCK:對應 SCLK
  • /CS:對應 /SS

由於這種標示方法不分主從,資料輸出都是 SDO,資料輸入都是 SDI,所以介接時必需把二個設備的 SDO 和 SDI 接腳對接:master 的 SDO 接到 slave 的 SDI,master 的 SDI 接到 slave 的 SDO。

在實際應用上,如果不需要由 slave 讀回資料時 (如:Output Port Expander,或者 DAC 晶片),則 MISO 接腳可以省略。有看到一部份資料說:只有一主一從時,/SS 訊號可以省略,只要將 slave 的 /SS 直接接地即可。對某些 slave 晶片來說這是不對的,因為有些 slave 晶片會拿 /SS 訊號的下降緣來識別 master 送來的第一個 bit。這是非常重要的錯誤回復機制,遇到這類 slave 晶片只把 /SS 接腳直接接地是不會動作的。另外後述的 Daisy-Chain 接法也需要這個 /SS 訊號的上昇緣才能運作。Daisy-Chain 模式用它 (/SS 訊號的上昇緣) 來栓鎖指令,如此多個 slave 晶片才能同時載入不同的指令。

由上面的結構示意圖,我們很清楚的看出它其實是利用二組頭尾相連的位移暫存器 (Shift Rigister) 來完成 master 和 slave 之間的資料交換,而且是接收與發送同時進行 (全雙工)。而 SCLK 就是用來控制二者同步位移的時脈信號,它是由 master 產生送出給雙方的位移暫存器。這樣子的結構允許資料一位元一位元的傳送,可快可慢,甚至允許暫停 (可以暫停在任意點上,完全沒有限制),因為 SCLK 沒有變動時,雙方的 shift register 是不會有動作的。所以如果你的微控制器沒有硬體式的 SPI 可以用,直接用軟體加一般的 GPIO 接腳,就可以模擬 SPI master。(用軟體模擬 SPI slave 則不建議,因為如果我們不知道對方會送多快的 SCLK)。

在介接方面,除了簡單的一主一從架構之外,SPI 也可以一個 master 連接多個 slave。接法上有二種,一種是利用多條獨立的 /SS 訊號,另一種則利用 Daisy-Chain 的方式。

SPI Master 介接多個 Slave 的二種接法

使用 Daisy-Chain 的方法,不需要額外的硬體接線腳來擴充 /SS 訊號,但是傳送及接收程式需要大改,非常的麻煩。另外還有一個障礙是,slave 晶片必需在同一個模式下工作,還有也不能有接腳缺省的情形 (有許多 DAC 晶片沒有 MISO 接腳),也因此常見的應用 Daisy-Chain 的例子都是用來串連多個同一型號的晶片,例如: 需要多組 DAC 或者是 ADC 一起工作的情況。另外要注意的是:並非每一顆 SPI 晶片都可以支援 Daisy-Chain 的接法,必需詳細檢視晶片的使用手冊是否有支援 Daisy-Chain 介接方法,或者是檢視讀取的指令格式是否符合 Daisy-Chain 的需求。否則到時候硬體接線接好了,Master 卻怎樣也無法命令 slave 晶片正常的工作。

Daisy-Chain 的運作方法是以倍數擴充每次傳輸的指令/資料長度 (有二個 slave 晶片串在一起,傳輸長度就 x2;三個 slave 晶片串在一起,傳輸長度就 x3)。由於上一顆 slave 晶片的 MISO 是連接到下一顆 slave 晶片 MOSI,所以多出來的傳輸訊號很自然把先前的訊號擠出來,送到下一顆晶片。等到所有訊號都送出去了,再來就是利用 /SS 訊號的上昇緣來通知每一顆 slave 晶片:要給你的指令/資料已經準備好了,請把它栓鎖 (latch) 起來了。同樣的讀回資料也會串在一起。所以原本的驅動程式必需把要給各個 slave 晶片的指令暫存起來然後依 slave 晶片串連的次序將指令串起來再送出,收回來的資料也必需先裁切好再依 slave 晶片串連的次序分配給對應的接收單元。

是故比較常用的是多條獨立的 /SS 訊號。有的 master 晶片是直接提供額外的 /SS1,/SS2,/SS3... 等接腳。有的則需要利用原本的 /SS 再以一組 3 to 8 解碼器 (或者是 4 to 16 解碼器) 配合 3~4 條 GPIO 接腳來產生需要的多組 /SS。更有的 SoC 乾脆提供多組 SPI 單元及多條獨立的 /SS 訊號 (當然的,代價是 master 晶片的接腳數比較多,單價拉高)。

SPI 工作模式及時序圖

使用 SPI,最麻煩一件事是確認周邊晶片的工作模式。SPI 一共有 4 種工作模式,但這 4 種模式的時序圖差異非常小,常令初學者 '霧剎剎' 抓不到重點。如下圖:

各種工作模式下的 SPI 時序圖

上圖的上半部是 SCLK 的時序,有兩種選擇: CPOL=0 或者 CPOL=1。中間及下半部是 MOSI MISO 的時序,一樣是有兩種選擇:中間是 MOSI MISO 是在 SCLK 的奇數次變化栓鎖資料,下半部則是 MOSI MISO 是在 SCLK 的偶數次變化栓鎖資料。

    解說:
  • SPI slave 晶片一般只支援一種工作模式,所以通常 master 必需牽就 slave 把工作模式設成和 slave 一致,才能正常運作。(因為二者內部均是位移暫存器,所以 MOSI 和 MISO 的時序需求是一樣)
  • 首先,第一個不同是 SCLK 的極性 (polarity),所謂極性其實是指 SPI 不工作時,SCLK 是停留在高電位還是低電位。CPOL=0 是 SCLK 在不工作時停留在低電位,CPOL=1 則是停留在高電位。
  • 再來要注意 slave 晶片是在 SCLK 的下降緣栓鎖資料,還是在 SCLK 的上升緣。要讓對方栓鎖資料,我們就必需把資料 Hold 住,保持穩定,所以 Slave 晶片在 SCLK 的下降緣栓鎖資料,相對的 master 就必需要在上升緣送出資料。反之,slave 晶片在 SCLK 的上升緣栓鎖資料,相對的 master 就必需要在下降緣送出資料。
  • 但是 CPHA 的定義並不是依上升緣/下降緣。CPHA 設定的是晶片栓鎖資料的時機是在 /SS 訊號下降之後,SCLK 的奇數次變化 (CPHA=0),還是偶數次變化 (CPHA=1)。
  • 所以 CPHA 配合 CPOL (SCLK 的極性) 設定,組合起來一共有 4 種工作模式。不過模式的命名順序出現了分歧沒有統一,現有的晶片出現了二種順序 (參閱 Wiki 網站 Serial Peripheral Interface Bus)。所以撰寫程式時不要只是把 Mode 設成相同的數值,小心 master 晶片和 slave 晶片二者的模式順序不同。不過仔細對照一下,差異只有 mode 的數值不同而已,CPOL 及 CPHA 的定義並無不同。
    SPI Mode
    Mode # of
    Model A
    Mode # of
    Model B
    CPOL CPHA 資料栓鎖時機MISO MOSI
    時序圖
    1 0 CPOL=0 CPHA=0 奇數次 上升緣 上半組
    0 1 CPOL=0 CPHA=1 偶數次 下升緣 下半組
    3 2 CPOL=1 CPHA=0 奇數次 下升緣 上半組
    2 3 CPOL=1 CPHA=1 偶數次 上升緣 下半組
    Model A:For ARM-based and Microchip PIC
    Model B:其他
  • 一旦正確辨識出 slave 晶片的工作模式,只要把 master 設定成一樣的工作模式即可正確的傳送指令及接收資料。

結語:

    總結一下辨識的技巧:
  • 先確認 slave 晶片需求的 SCLK 極性,不工作時是在低電位還是高電位, 由此確認 CPOL 為 0 或 1。
  • 再由 slave 晶片 datasheet 中的時序圖確認 slave 晶片是在 SCLK 的下降緣栓鎖資料,還是在 SCLK 的上升緣。
  • 直接比對上面的模式表,確認出 CPHA 值為 0 或 1。
  • 確認傳輸時序圖是否正確 (是奇數組 SCLK 變化,還是偶數組 SCLK 變化)。

補充說明一點,從 /SS 的下降緣到第一個 SCLK latch edge 有一小段時間的 delay,這一小段 delay 必需滿足 slave device 的規格需求,尤其是 SCLK 的頻率偏高的時候。否取 slave device 會無法正常工作。有些 MCU 的 SPI 硬體模組把這一段設成固定的 CLK 數; 有些 MCU 的 SPI 硬體模組則可以讓使用者自行調整。(2018/06/14)

還有一點在圖中有標示,但上文沒有提到的是: SPI 的資料是 msb (Most Significant Bit)bit7 (MSB) 先傳 (傳輸的資料格式是 8 Bits 就 bit7 先傳,是 16 Bits 就 bit15 先傳,是 24 Bits 就 bit23 先傳),如果你是用軟體來模擬 SPI master 需注意到這一點。話雖如此,有些許 MCU 的 SPI port 是允許你把它改為 lsb (Least Significant Bit) 先傳,這是為了要能應付一些沒有依照標準設計的晶片,並不是標準狀況,也請小心。

另外 SPI 只是硬體的傳輸協定,完全沒有提及定址(選擇晶片/暫存器位址),指令,資料長度...等等,這一部份是完全由 salve 晶片制定,master 想要控制 slave 動作必需完全依據 slave 晶片 datasheet 上的規範。

下二張圖是 SPI Daisy-Chain 部份的接線圖及其時序圖。其中 SPI Master (MCU) 串接了二顆 Slave 晶片,Slave 晶片的指令/資料格式為 16 bits。接線圖上我們標示了 ➀ ➁ 二個點,就是後面時序圖的量測點。時序圖中的 (DOUTM)DINS1 是 SPI Master (MCU) 的 MOSI 及 Slave1 的 MOSI (DIN) 輸入接腳連線上的時序 (接線圖上點 ➀ 所量測到的訊號),(DOUTS1)DINS2 是 Slave1 的 MISO (DOUT) 輸出接腳和 Slave2 的 MOSI (DIN) 輸入接腳連線上的時序 (接線圖上點 ➁ 所量測到的訊號)。

如前面說的 Slave 晶片的指令/資料格式為 16 bits,所以時序圖上的 Byte1 及 Byte2 二個合起來才是一組 16 bits 的 SPI 指令/資料。Byte3 及 Byte4 則是另一組。由於 SPI 的電路結構使用的是串列位移暫存器,所以我們其實可以把 Slave1 和 Slave2 合起來看,變成一個長度為 32 bits 的串列位移暫存器 (點 ➀ 的時序圖)。由 Slave1 的觀點來看: 前 16 bits 資料 ( Byte1 及 Byte2) 當然是先把自己的串列位移暫存器填滿,可是 Master 又接著送 Byte3 及 Byte4 進來,所以先前的 Byte1 及 Byte2 就又都被擠出去給 Slave2 了。由 Slave2 的觀點來看: 狀況看 Slave1 是一樣的,只不過前 16 bits 是原先 Slave1 肚子裡的垃圾,後 16 bits 才是由 Master 送來的 Byte1 及 Byte2

所以 Master 送完 4 bytes 資料時,Slave1 收到了後二個 bytes,Slave2 收到了前二個 bytes。接著 /SS 接腳訊號向上拉,Slave1 和 Slave2 一起把資料栓鎖,開始解碼及相關動作。

    總結 SPI Daisy-Chain 的注意事項有三:
  1. 傳輸時,給遠端晶片 (離 MCU 的 MOSI 接腳比較遠的,即 Slave2) 的資料要先送 (Byte1 及 Byte2),再接著送給近端 (Slave1) 的資料 (Byte3 及 Byte4)。
  2. 每一次傳輸都必需是完整的 N 組晶片所需的指令/資料 (N 為 Chain 裡的 slave 晶片數,圖示為 N=2),否則遠端晶片會收到別人的資料 (上一次傳輸所送出的資料),產生不正確的動作。
  3. 還有一個狀況是除非晶片有支援 NOP (no operation) 的指令,否則無法只針對 chain 裡其中一顆晶片下達指令/資料 (原因如上一項所述)。例如:74HC595 就沒有 NOP 指令;但是 MAX 7219/7221 就有 NOP 指令可用。

瞭解 Arduino 變數占用的記憶體大小

Arduino 的記憶體空間非常寶貴,Arduino 也不擅長處理大量的資料,因此為變數宣告合適的資料型態,盡量節省記憶體空間,將可以提高程式的執行效率。

幾款常見的 Arduino 規格

Arduino UNO
  • Microcontroller:ATmega328P
  • Operating Voltage:5V
  • Input Voltage (recommended):7-12V
  • Input Voltage (limit):6-20V
  • Digital I/O Pins:14 (of which 6 provide PWM output)
  • PWM Digital I/O Pins:6
  • Analog Input Pins:6
  • DC Current per I/O Pin:20 mA
  • DC Current for 3.3V Pin:50 mA
  • Flash Memory32 KB of which 0.5 KB used by bootloader
  • SRAM:2 KB (ATmega328P)
  • EEPROM:1 KB (ATmega328P)
  • Clock Speed:16 MHz
  • LED_BUILTIN:13

Arduino Nano
  • Microcontroller:ATmega328
  • Architecture:AVR
  • Operating Voltage:5 V
  • Flash Memory32 KB of which 2 KB used by bootloader
  • SRAM:2 KB
  • Clock Speed:16 MHz
  • Analog IN Pins:8
  • EEPROM:1 KB
  • DC Current per I/O Pins:40 mA (I/O Pins)
  • Input Voltage:7-12 V
  • Digital I/O Pins:22 (6 of which are PWM)
  • PWM Output:6
  • Power Consumption:19 mA

Arduino Leonardo
  • Microcontroller:ATmega32u4
  • Operating Voltage:5V
  • Input Voltage (Recommended):7-12V
  • Input Voltage (limits):6-20V
  • Digital I/O Pins:20
  • PWM Channels:7
  • Analog Input Channels:12
  • DC Current per I/O Pin:40 mA
  • DC Current for 3.3V Pin:50 mA
  • Flash Memory32 KB of which 4 KB used by bootloader
  • SRAM:2.5 KB (ATmega32u4)
  • EEPROM:1 KB (ATmega32u4)
  • Clock Speed:16 MHz

Arduino MEGA 2560
  • Microcontroller:ATmega2560
  • Operating Voltage:5V
  • Input Voltage (recommended):7-12V
  • Input Voltage (limit):6-20V
  • Digital I/O Pins:54 (of which 15 provide PWM output)
  • Analog Input Pins:16
  • DC Current per I/O Pin:20 mA
  • DC Current for 3.3V Pin:50 mA
  • Flash Memory256 KB of which 8 KB used by bootloader
  • SRAM:8 KB
  • EEPROM:4 KB
  • Clock Speed:16 MHz
  • LED_BUILTIN:13

資料類型的補充說明

從上面的規格介紹我們知道,除了 Arduino MEGA 2560 以外,其他的幾個型號的 Flash Memory 都只有 32 KB,而且還需要再從這裡面分出一些給 bootloader,所以,程式可以用到的記憶體真的不多,因此節約記憶體就成為一件重要的事。

ESP32 處理器是 32 位元的處理器,其 int 和 double 類型佔用的記憶體容量和 8 位元處理器不同。如下表所示,淺藍色是 8 位元 (Uno 板),深藍色 32 位元 (ESP32 版):

類型中文名稱佔用記憶體大小可表示數值範圍

int
整數
     16 bits
         32 bits
-32768~32767
-2147483648~2147483647

double
雙倍精度
浮點數
         32 bits
                 64 bits
±3.4E+38 
±1.7E+308

根據文獻資料,Arduino 程式也支援 1999 年制定的 C 程式語言 C99 標準的整數類型寫法,採資料佔用的位元數來定義。如此可避免不同微控制器,對整數數字範圍定義不一致的情況。定義如下表:

類型等同的類型
int8_tchar
uint8_tbyte
int16_t8 位元處理器的 int
uint16_t8 位元處理器的 unsigned nint
int32_tlong 或 32 位元處理器的 int
uint32_tunsigned long 或 32 位元處理器的 unsigned int

在 Arduino UNO 裏宣告 int 會占用 16bit(2byte),若是在 Arduino Due 裏宣告 int 則會占用 32bit(4byte)。

如果使用 uint8_t 來宣告變數,只會占用 8bit(1byte),雖然 Arduino 沒有提到 uint8_t 也可以用來宣告變數,但因為 Arduino 是走 C 家族的編譯環境,而 uint8_t 就是 C 家族合法的資料型態,因此您可以放心地使用 unint8_t 來宣告變數,如此可以節省不少記憶體空間。

底下有一個程式可以列出資料型態所占用的記憶體空間是多少:

範例程式

// Arduino DatatypeSize v2 - Display information about data types used in Arduino
// Created by Michael 'TeX' Hex - http://www.texhex.info/
//
// Full list of all data types:
// http://arduino.cc/en/Reference/HomePage -> Section "Data Types"
//
// Additonal references:
// http://www.arduino.cc/playground/Code/DatatypePractices
// http://arduino.cc/en/Reference/VariableDeclaration
//
// Any text in this sketch is taken from the Arduino homepage,
// licensed under a Creative Commons Attribution-ShareAlike 3.0 License.

void setup()
{  
  Serial.begin(9600);
}

void loop()
{
  Serial.println("--- Variable type: Size in SRAM (bytes) ---");

  // C standard data type "unsigned int length 8 bits".
  // Occupies one byte of memory.
  // http://arduino.cc/forum/index.php/topic,41590.0.html
  // http://en.wikipedia.org/wiki/Stdint.h#Fixed_width_integer_types
  Serial.print("uint8_t: ");
  Serial.println(sizeof(uint8_t));

  // A boolean holds one of two values, true or false.
  // Each boolean variable occupies one byte of memory.
  // http://arduino.cc/en/Reference/BooleanVariables
  Serial.print("boolean: ");
  Serial.println(sizeof(boolean));

  // A data type that takes up 1 byte of memory that stores a character value.
  // Character literals are written in single quotes, like this: 'A' (for multiple
  // characters - strings - use double quotes: "ABC").
  // http://arduino.cc/en/Reference/Char
  Serial.print("char: ");
  Serial.println(sizeof(char));

  // An unsigned data type that occupies 1 byte of memory. Same as the byte datatype.
  // The unsigned char datatype encodes numbers from 0 to 255.
  // For consistency of Arduino programming style, the byte data type is to be preferred.
  // http://arduino.cc/en/Reference/UnsignedChar
  Serial.print("unsigned char: ");
  Serial.println(sizeof(unsignedu char));

  // A byte stores an 8-bit unsigned number, from 0 to 255.
  // http://arduino.cc/en/Reference/Byte
  Serial.print("byte: ");
  Serial.println(sizeof(byte));

  // Integers are your primary datatype for number storage, and store a 2 byte value.
  // This yields a range of -32,768 to 32,767 (minimum value of -2^15 and a maximum value of (2^15) - 1).
  // http://arduino.cc/en/Reference/Int
  Serial.print("int: ");
  Serial.println(sizeof(int));

  // Unsigned ints (unsigned integers) are the same as ints in that they store a 2 byte value.
  // Instead of storing negative numbers however they only store positive values, yielding a
  // useful range of 0 to 65,535 (2^16) - 1).
  // http://arduino.cc/en/Reference/UnsignedInt
  Serial.print("unsigned int: ");
  Serial.println(sizeof(unsigned int));

  // A word stores a 16-bit unsigned number, from 0 to 65535.
  // Same as an unsigned int.
  // http://arduino.cc/en/Reference/Word
  Serial.print("word: ");
  Serial.println(sizeof(word));

  // Long variables are extended size variables for number storage, and store 32 bits (4 bytes),
  // from -2,147,483,648 to 2,147,483,647.
  // http://arduino.cc/en/Reference/Long
  Serial.print("long: ");
  Serial.println(sizeof(long));

  // Unsigned long variables are extended size variables for number storage, and store 32 bits (4 bytes).
  // Unlike standard longs unsigned longs won't store negative numbers, making their range from 0
  // to 4,294,967,295 (2^32 - 1).
  // http://arduino.cc/en/Reference/UnsignedLong
  Serial.print("unsigned long: ");
  Serial.println(sizeof(unsigned long));

  // Datatype for floating-point numbers, a number that has a decimal point.
  // Floating-point numbers can be as large as 3.4028235E+38 and as low as -3.4028235E+38.
  // They are stored as 32 bits (4 bytes) of information.
  // http://arduino.cc/en/Reference/Float
  Serial.print("float: ");
  Serial.println(sizeof(float));

  // Double precision floating point number. Occupies 4 bytes.
  // The double implementation on the Arduino is currently exactly the same as the float,
  // with no gain in precision.
  // http://arduino.cc/en/Reference/Double
  Serial.print("double: ");
  Serial.println(sizeof(double));

  delay(8500);
}

2021年6月1日 星期二

在 MacBook 上設定 zsh

從 macOS Catalina 版開始,Mac 使用 zsh 做為預設登入 shell 。想要把舊版 macOS 中將 zsh 設為預設 shell。更改的方式大概有兩種。

A. 從『使用者與群組』

  1. 從系統偏好設定進去後,找到「使用者與群組」,解開左下角的鎖頭。
  2. 按住 control 鍵,將滑鼠移到你的帳號上面,並點左鍵,就會出現「進階選項」。
  3. 更改「登入 sheel」即可。

B. 使用命令列

直接開啟終端機,輸入以下指令:

$ which zsh
/bin/zsh
$ chsh -s /bin/zsh

改用了 zsh 之後接著就請參考以下的說明,來修改 zsh 原生平凡的外觀。

zsh 真正簡單設定

起因

Apple 宣佈在 macOS 10.15 Catalina 中,默認 shell 將是 zsh。zsh(我相信它的發音是 zee-shell,雖然 zish 說起來很有趣)將取代 bash 作為默認 shell。 自 Mac OS X 10.3 Panther 以來,bash 一直是默認 shell,所以從 bash 改變到 zsh 是未來的趨勢。

為了達成目標,在網路上找到了一個系列文章 - Moving to zsh
作者一樣是 Mac 的使用者,也是一樣的原因出了這一系列的文章。建議有機會詳讀。

這一系列的其中一篇 part 6 – Customizing the zsh Prompt 正是介紹外觀的客制化設定。而且確定沒有叫我安裝任何多餘的東西。

在你的 .zshrc 文件中,新增

PROMPT='你的設定'
其中:
  • PROMPT: 代表 bash 的 export PATH
  • '你的設定': 設定的值必須要是字串格式

查色碼

若想使用不同的顏色在 zsh 的 console 裡面顯示,必須先查閱這個顏色的色碼,這個色碼可以從網站「256 COLORS - CHEAT SHEET」裏查詢。

設定顏色

%F{2}%m%f
其中:
  • %F{色碼}:用來設定某個顏色的開始。
  • %f:用來設定成預設的樣式,也可以說是設定好的顏色結束。
  • %m:主機名稱,在此只是示範夾在顏色中間的東西。

zsh Prompt Expansion

網址:zsh:13 Prompt Expansion, 13.2.5 Visual effects

我的 PROMPT 長這樣

PROMPT='%F{226}%n%f%F{82}%B@%b%f%F{226}%m%f%F{82}%B:%b%f%F{226}%/%f > '
  • %n:值為 $USERNAME
  • %B:開始粗體字
  • %b:結束粗體字
  • %m:值為第一個 . 之前的 hostname
  • %/:值為從根目錄開始的 $PATH