昨天介紹完可以用 C# 寫 ESP32 開發板程式的美妙開源平台 - nanoFramework,我迫不及待想把先前 Arduino C++ 寫的作品移植過來,但光是想接 I2C OLED 顯示器輸出文字就卡住三、四個小時...

其實早有心理準備,nanoFramework 仍在發展初期,用的 NuGet 套件全是 Preview 版,一方面參考文件、討論少,常缺乏現成可用的程式庫;另一方面找到的教學或範例也常因 API 規格修改失效,這是當先鋒部隊一定會面對的挑戰。跟寫 Arduino C++ 相比,過去那種「遇到任何問題,隨手一查就有滿滿的教學範例跟程式庫任你挑」的尊榮體驗不復存在,像是貴公子逃家,開始粗茶淡飯過日子。但是! 能用 C# 寫程式就是爽,付出一些代價也是值得的。

網路上 nanoFramework 整合 I2C 範例不多,用 ESP32 的更少。找到網友三年前寫的 SSD1306 OLED nanoFramework NuGet 程式庫,但因核心版本差異過大無法執行,加上原作者只測過 128x32 解析度的版本,不確定跟手上在用的 128x64 是否有差異。於是改了抓原始碼回來改,參考已在 Arduino C++ 驗證 OK 的 Adafruit 程式庫寫法進行調整,但怎麼改 OLED 顯示器都毫無動靜。搞了很久,直到我回頭寫了小程式去掃 I2C 位址,才發現問題出在 ESP32 偵測不到 I2C 裝置。朝此方向深入研究,這才知道,ESP32 因開發板種類眾多,Pin 腳設定依版本不同,故要參照 nanoFramework.Hardware.Esp32 套件使用 Configuration.SetPinFunction(..., DeviceFunction.I2C_DATA) 重新定義 I2C 腳位(我的板子,I2C1_DATA 是 21、I2C1_CLOCK 是 22),這才測試成功,感動~

thumbnail

整理這次的心得:

  • nanoFramework 處理 I2C 有兩套 NuGet 程式庫,nanoFramework.Windows.Devices.I2cnanoFramework.System.Device.I2c,Windows.Devices.I2c 下載次數多,更新日期新,網路範例幾乎也都是用 Windows.Devices.I2c,但它已被標註為過時(This package has been deprecated),這類 UWP API 將由 .NET IoT API 取代,使用時別西瓜偎大邊,建議改用 System.Device.I2c。
  • 寫 nanoFramework 不像 Arduino C++ 有一堆現成範例、程式庫裝了就可以跑。故測試時請從根本開始做起,例如要接 I2C 設備,建議從偵測 I2C 位址是否存在開始,不要像我誤以為是程式庫與裝罝不相容,查了大半天才發現是漏設 Pin 腳沒設,白花時間。
    附上掃瞄 I2C 位址的程式範例:
      using nanoFramework.Hardware.Esp32;
      using System;
      using System.Collections;
      using System.Device.I2c;
    
      namespace Guineapig.nfEsp32Lib.I2C
      {
          /// <summary>
          /// I2C device address scanner
          /// </summary>
          public class Scanner
          {
              /// <summary>
              /// Scan I2C device addresses
              /// </summary>
              /// <param name="busId">I2C bus id, 1 or 2. default 1</param>
              /// <param name="clockPin">Clock pin</param>
              /// <param name="dataPin">Data pin</param>
              /// <returns></returns>
              public static byte[] Scan(int busId = 1, int clockPin = 22, int dataPin = 21)
              {
                  switch (busId) 
                  {
                      case 1:
                          Configuration.SetPinFunction(clockPin, DeviceFunction.I2C1_CLOCK);
                          Configuration.SetPinFunction(dataPin, DeviceFunction.I2C1_DATA);
                          break;
                      case 2:
                          Configuration.SetPinFunction(clockPin, DeviceFunction.I2C2_CLOCK);
                          Configuration.SetPinFunction(dataPin, DeviceFunction.I2C2_DATA);
                          break;
                      default:
                          throw new ArgumentException($"Unsupported busId - {busId}");
                  }
    
                  ArrayList list = new();
                  SpanByte b = new SpanByte(new byte[1]);
                  // I2C address range from https://learn.adafruit.com/i2c-addresses/the-list
                  for (byte addr = 0x0E; addr <= 0x77; addr++)
                  {
                      using (var dev = I2cDevice.Create(new I2cConnectionSettings(busId, addr)))
                      {
                          var r = dev.Read(b);
                          if (r.Status == I2cTransferStatus.SlaveAddressNotAcknowledged) 
                              continue;
                          if (r.Status == I2cTransferStatus.FullTransfer ||
                              r.Status == I2cTransferStatus.PartialTransfer)
                          {
                              list.Add(addr);
                          }
                      }
                  }
                  return list.ToArray(typeof(byte)) as byte[];
              }
          }
      }
    
    實測如下:(雖然是很簡單的範例,我還是預先切好類別程式庫專案方便未來共用。這才是寫專案的正確姿勢,換回 C# 我終於擺脫老鳥魔咒了)
  • 關於常見的 I2C 位址,Adafruit 有份完整清單可以參考,不愧是 Arduino 界的善心員外。
  • 受限於嵌入式裝置的運算能力及記憶體、儲存空間,加上尚在發展初期,nanoFramework 與完整 .NET 功能有很大的差距,沒有 LINQ 在預期之內,連泛型(如 List<T>)也仍在開發中,未來才會加入,現階段要先用古老的 ArrayList 頂著,甚至也沒有 StringBuilder,有重回 .NET 1.0 的感覺。但老話一句,能用 C# 就是開心。

When using ESP32 + nanoFramework to connect I2C device, don't forget to Configuration.SetPinFunction() to set I2C pins.


Comments

Be the first to post a comment

Post a comment