使用 C++ Design Patterns 和 ESP32 實現多組 I2C 設備管理


本文將介紹如何在 ESP32 上使用 ESP-IDF,配合 C++ 設計模式 (Design Patterns) 來設計穩定的 I2C BUS 管理架構,並使用 esp_log 進行系統狀態與數據操作的日誌記錄。

在物聯網和嵌入式系統中,I2C (Inter-Integrated Circuit) 是一種廣泛應用的通訊協定。當我們的系統中有多組 I2C bus 並且每個 bus 上接有多個 I2C 裝置時,管理這些總線與裝置的讀寫操作可能會變得相當複雜。

Design Patterns

設計目標與架構

目標是在 ESP32 上利用設計模式 (Design Patterns) 構建一個靈活且可擴展的 I2C BUS 管理系統,達到以下需求:

  1. 多組 I2C BUS:支援多組 I2C BUS。
  2. 多個 I2C 裝置:每組 BUS 上可接多個 I2C 裝置。
  3. 掃描功能:可以掃描 I2C BUS 上所有裝置的地址,便於確認元件連接情況。
  4. 資料讀寫功能:可向特定裝置進行資料讀寫。
  5. 可觀察數據變化:支援裝置觀察者模式 (Observer Pattern),當數據更新時通知觀察者。

開發環境設置

使用的設計模式 (Design Patterns)

  • Factory 工廠設計模式:動態管理多組 I2C BUS 的初始化和實例化。
  • Observer 觀察者設計模式:允許 I2C BUS 上的裝置作為觀察者,在數據變動時接收通知。

程式設計方式

以下步驟將詳細說明如何使用 ESP32 的 ESP-IDF 和 C++ 設計模式 (Design Patterns) 來架設多組 I2C BUS,並進行裝置管理。並以 esp_log 日誌管理來記錄 I2C 操作與錯誤狀態和觀察設計模式 (Design Patterns),便於開發者追蹤和除錯。

定義 I2C BUS 的管理類別 I2CBusManager

I2CBusManager 是一個專門用來管理單一 I2C BUS 的類別,負責該 BUS 的初始化、數據讀寫、裝置掃描以及通知所有註冊的觀察者(即在該 BUS 上的裝置)。

#include "driver/i2c.h"
#include "esp_log.h"
#include <vector>

class I2CDeviceObserver {
public:
    virtual void onDataReceived(uint8_t* data, size_t length) = 0;
};

class I2CBusManager {
private:
    i2c_port_t i2c_port;
    int sda_pin;
    int scl_pin;
    std::vector<I2CDeviceObserver*> observers;

public:
    I2CBusManager(i2c_port_t port, int sda, int scl, uint32_t clk_speed) 
        : i2c_port(port), sda_pin(sda), scl_pin(scl) {
        i2c_config_t config;
        config.mode = I2C_MODE_MASTER;
        config.sda_io_num = sda_pin;
        config.scl_io_num = scl_pin;
        config.sda_pullup_en = GPIO_PULLUP_ENABLE;
        config.scl_pullup_en = GPIO_PULLUP_ENABLE;
        config.master.clk_speed = clk_speed;

        ESP_ERROR_CHECK(i2c_param_config(i2c_port, &config));
        ESP_ERROR_CHECK(i2c_driver_install(i2c_port, config.mode, 0, 0, 0));
        ESP_LOGI("I2CBusManager", "I2C bus initialized on I2C_NUM_%d with SDA:%d, SCL:%d", i2c_port, sda_pin, scl_pin);
    }

    void scanBus() {
        ESP_LOGI("I2CBusManager", "Starting I2C bus scan on I2C_NUM_%d...", i2c_port);
        for (uint8_t address = 0x03; address < 0x78; ++address) {
            i2c_cmd_handle_t cmd = i2c_cmd_link_create();
            i2c_master_start(cmd);
            i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true);
            i2c_master_stop(cmd);
            esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 10 / portTICK_PERIOD_MS);
            i2c_cmd_link_delete(cmd);

            if (ret == ESP_OK) {
                ESP_LOGI("I2CBusManager", "Device found at address 0x%X on I2C_NUM_%d", address, i2c_port);
            }
        }
        ESP_LOGI("I2CBusManager", "I2C bus scan completed on I2C_NUM_%d", i2c_port);
    }

    void addObserver(I2CDeviceObserver* observer) {
        observers.push_back(observer);
    }

    void notifyObservers(uint8_t* data, size_t length) {
        for (auto observer : observers) {
            observer->onDataReceived(data, length);
        }
    }

    void writeToDevice(uint8_t address, uint8_t* data, size_t length) {
        i2c_cmd_handle_t cmd = i2c_cmd_link_create();
        i2c_master_start(cmd);
        i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true);
        i2c_master_write(cmd, data, length, true);
        i2c_master_stop(cmd);
        esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_PERIOD_MS);
        i2c_cmd_link_delete(cmd);

        if (ret == ESP_OK) {
            ESP_LOGI("I2CBusManager", "Data written to device at 0x%X on I2C_NUM_%d", address, i2c_port);
        } else {
            ESP_LOGE("I2CBusManager", "Failed to write to device at 0x%X on I2C_NUM_%d", address, i2c_port);
        }
    }

    void readFromDevice(uint8_t address, uint8_t* data, size_t length) {
        i2c_cmd_handle_t cmd = i2c_cmd_link_create();
        i2c_master_start(cmd);
        i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, true);
        i2c_master_read(cmd, data, length, I2C_MASTER_LAST_NACK);
        i2c_master_stop(cmd);
        esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_PERIOD_MS);
        i2c_cmd_link_delete(cmd);

        if (ret == ESP_OK) {
            ESP_LOGI("I2CBusManager", "Data read from device at 0x%X on I2C_NUM_%d", address, i2c_port);
            notifyObservers(data, length);
        } else {
            ESP_LOGE("I2CBusManager", "Failed to read from device at 0x%X on I2C_NUM_%d", address, i2c_port);
        }
    }
};

使用 I2CManagerFactory 動態管理多組 I2C BUS

I2CManagerFactory 使用工廠模式管理 I2C BUS 的初始化和實例化,確保每組 BUS 只被初始化一次。

#include <map>

class I2CManagerFactory {
private:
    std::map<i2c_port_t, I2CBusManager*> i2c_buses;

public:
    I2CBusManager* getI2CBusManager(i2c_port_t port, int sda, int scl, uint32_t clk_speed = 100000) {
        if (i2c_buses.find(port) == i2c_buses.end()) {
            i2c_buses[port] = new I2CBusManager(port, sda, scl, clk_speed);
            ESP_LOGI("I2CManagerFactory", "Created new I2CBusManager for I2C_NUM_%d", port);
        } else {
            ESP_LOGW("I2CManagerFactory", "I2C_NUM_%d already initialized", port);
        }
        return i2c_buses[port];
    }

    ~I2CManagerFactory() {
        for (auto& pair : i2c_buses) {
            delete pair.second;
        }
    }
};

定義裝置觀察者 I2CDeviceObserver

這個觀察者模式允許特定的 I2C 裝置在數據更新時接收通知。

class SensorDevice : public I2CDeviceObserver {
public:
    void onDataReceived(uint8_t* data, size_t length) override {
        // Handle the received data
        ESP_LOGI("SensorDevice", "Received data of length %d", length);
    }
};

主程式範例

最後,將這些組件放在主程式中進行整合,並掃描每組 I2C BUS 以確認連接的裝置。

extern "C" void app_main() {
    I2CManagerFactory factory;

    // Initialize two I2C buses
    I2CBusManager* bus0 = factory.getI2CBusManager(I2C_NUM_0, GPIO_NUM_21, GPIO_NUM_22);
    I2CBusManager* bus1 = factory.getI2CBusManager(I2C_NUM_1, GPIO_NUM_18, GPIO_NUM_19);

    // Perform scanning
    bus0->scanBus();
    bus1->scanBus();

    // Create and register a device observer
    SensorDevice sensor;
    bus0->addObserver(&sensor);

    // Example of writing and reading data
    uint8_t data_to_write[2] = {0x01, 0x02};
    bus0->writeToDevice(0x48, data_to_write, 2);

    uint8_t data_to_read[2];
    bus0->readFromDevice(0x48, data_to_read, 2);
}

輸出

在 main.cpp 文件中執行此程式碼並成功上傳至 ESP32 開發板後,您可以透過 ESP-IDF 插件工具來檢視日誌輸出。以下是預期的日誌輸出範例:

I (100) I2CBusManager: I2C bus initialized on I2C_NUM_0 with SDA:21, SCL:22
I (110) I2CBusManager: I2C bus initialized on I2C_NUM_1 with SDA:18, SCL:19
I (120) I2CBusManager: Starting I2C bus scan on I2C_NUM_0...
I (150) I2CBusManager: Device found at address 0x48 on I2C_NUM_0
I (160) I2CBusManager: I2C bus scan completed on I2C_NUM_0
I (170) I2CBusManager: Starting I2C bus scan on I2C_NUM_1...
I (200) I2CBusManager: I2C bus scan completed on I2C_NUM_1
W (210) I2CManagerFactory: I2C_NUM_0 already initialized
I (220) I2CBusManager: Data written to device at 0x48 on I2C_NUM_0
I (230) I2CBusManager: Data read from device at 0x48 on I2C_NUM_0
I (240) SensorDevice: Received data of length 2

解說

I2C BUS 初始化:這顯示 I2C BUS I2C_NUM_0I2C_NUM_1 已分別使用 SDA 和 SCL 引腳初始化。

I2C BUS 掃描:在掃描 I2C_NUM_0 BUS 時,偵測到位於 0x48 的 I2C 裝置。

資料寫入與讀取:已成功寫入資料至 I2C_NUM_0 上的 0x48 裝置並成功從裝置讀取數據。

觀察者接收資料:當讀取資料成功後並通知了 SensorDevice 裝置和顯示接收到數據。

這些日誌訊息可以清楚了解我們以 C++ 設計模式 (Design Patterns) 來實現 I2C BUS 的初始化過程、掃描結果、資料讀取與寫入等操作是否成功,並觀察裝置數據變化的通知情況。

總結

在 ESP32 上使用 C++ 設計模式 (Design Patterns) 建立多組 I2C BUS 管理的架構,並透過 esp_log 實現完善的日誌記錄。通過將 I2CBusManagerI2CManagerFactory 和觀察者模式相結合,可以輕鬆管理多組 I2C BUS 及其上多個 I2C 裝置的讀寫操作,並動態處理數據變化通知。