🐤

【ESP32】SPIの使い方(C言語)

に公開

こんにちは、星杜なぎさです。
今回は、ESP32-WROOM-32Eを使ってC言語でSPI通信を使ったサンプルを動作させてみました。

環境

  • PC:Windows11 Home
    • ESP-IDF : v5.5.1
    • 使用言語:C言語
  • マイコン:ESP32-WROOM-32E

1. SPIとは?

以下、参考サイト参照のこと

SPIの基本を学ぶ
https://www.analog.com/jp/resources/analog-dialogue/articles/introduction-to-spi-interface.html

2. ESP32でSPIを使う上での知識

  • ESP32には4つのSPIが存在する(SPI0/1/2/3)
  • SPI0とSPI1は、内蔵フラッシュなどで予約・使用済みのため、極力避けるべし
  • つまり、SPI2とSPI3を使うのがベター(画像中の赤枠・青枠の部分)

3. 実用例

ここからは、SPI通信を使ってデバイスと通信するサンプルコードを記載していく。

3.1 microSDcard Adapter

以下の動作をするプログラムを作成しました。

  • microSDcardにファイル"HELLO.TXT"を作成
  • 文字列"Hello, ESP32 SPI microSD!"を書き込む
  • 再度"HELLO.TXT"を読み込んで、内容を表示する

microSDcardの準備

microSDカードのフォーマットを"FAT32"にしてください(32GB以下推奨)

具体的なフォーマット方法
  1. USBなどで接続した後、エクスプローラーから「フォーマット」を選択
  2. フォーマットをFAT32にして、開始をクリック
  3. フォーマットが完了するまで待つ
  4. プロパティを見たときに、フォーマットがFATになってればOK

配線図

以下のように配線する。

ESP32 microSDcardAdapter
3V3 3V3
GPIO 5 CS
GPIO 23 MOSI
GPIO 18 CLK
GPIO 19 MISO
GND GND

プログラム

簡単に解説すると、以下の流れである

  • 24~37行目:SPI通信のHOST(master)側の設定
  • 39~42行目:SPI通信のDEVICE(slave)側の設定
  • 44~51行目:microSDcardのマウント処理
  • 59~67行目:microSDcard内にtxtファイルを作成し、書き込む
  • 71行目:microSDcardをアンマウント処理
長いので折り畳み
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_common.h"
#include "driver/sdspi_host.h"
#include "driver/gpio.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#include "esp_log.h"

#define MOUNT_POINT "/sdcard"
static const char *TAG = "SD_SPI";

void app_main(void)
{
    esp_err_t ret;
    sdmmc_card_t* card;
    const char *text = "Hello, ESP32 SPI microSD!\n";

    // SPIの設定
    sdmmc_host_t host = SDSPI_HOST_DEFAULT();
    spi_bus_config_t bus_cfg = {
        .mosi_io_num = 23,
        .miso_io_num = 19,
        .sclk_io_num = 18,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4000,
    };
    ret = spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA);
    if (ret != ESP_OK){
        ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret));
        return;
    };

    // SDカードの設定
    sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
    slot_config.gpio_cs = 5;  // CSピン
    slot_config.host_id = host.slot;

    // FATファイルシステムのマウント設定
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };

    ret = esp_vfs_fat_sdspi_mount(MOUNT_POINT, &host, &slot_config, &mount_config, &card);
    if (ret != ESP_OK){
        ESP_LOGE(TAG, "Failed to mount filesystem: %s", esp_err_to_name(ret));
        return;
    };
    ESP_LOGI(TAG, "SD card mounted");

    // ファイルの書き込み
    FILE* f = fopen(MOUNT_POINT"/Hello.txt", "w");
    if (f == NULL){
        ESP_LOGE(TAG, "Failed to open file for writing");
        esp_vfs_fat_sdcard_unmount(MOUNT_POINT, card);
        return;
    };
    fprintf(f, "%s", text);
    fclose(f);
    ESP_LOGI(TAG, "File written");

    // アンマウント
    esp_vfs_fat_sdcard_unmount(MOUNT_POINT, card);
    ESP_LOGI(TAG, "Card unmounted");
}

出力

正常に動作すれば、microSDカードに、HELLO.TXTという名前のファイルが書き込まれる。

3.2 加速度センサ:ADXL345

私の持っているADXL345は、SPI通信も対応していたため、使用してみた。

配線図

  • SPI通信にありがちなMOSIやMISOではないため分かりづらいが、以下の配線でOK
ESP32 ADXL345
GND GND
3V3 VCC
GPIO 5 CS
GPIO 19 SDO
GPIO 23 SDA
GPIO 18 SCL

こんな感じ。

プログラム

重要なところだけ解説する

  • void spi_init(void)
    • spi_bus_initialize()でHOST側の設定
    • spi_bus_add_device()でSPI通信用のデバイスを追加する
  • デバイスへの書き込みはspi_device_transmit()で行う
長いので折り畳み
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"

#define TAG "ADXL345_SPI"

// ピン定義
#define PIN_NUM_MISO 19  // SDOに繋ぐ
#define PIN_NUM_MOSI 23  // SDAに繋ぐ
#define PIN_NUM_CLK  18  // SCLに繋ぐ
#define PIN_NUM_CS   5   // CSに繋ぐ

#define SPI_HOST SPI3_HOST

// ADXL345 レジスタ
#define REG_DEVID        0x00
#define REG_POWER_CTL    0x2D
#define REG_DATA_FORMAT  0x31
#define REG_DATAX0       0x32

static spi_device_handle_t spi;


// SPI初期化
void spi_init(void)
{
    spi_bus_config_t buscfg = {
        .miso_io_num = PIN_NUM_MISO,
        .mosi_io_num = PIN_NUM_MOSI,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };

    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 1 * 1000 * 1000, // 1MHz
        .mode = 3,                         // SPI Mode 3
        .spics_io_num = PIN_NUM_CS,
        .queue_size = 1,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
    ESP_ERROR_CHECK(spi_bus_add_device(SPI_HOST, &devcfg, &spi));
}

// 1バイト書き込み
void adxl_write(uint8_t reg, uint8_t data)
{
    spi_transaction_t t = {0};
    uint8_t tx[2];

    tx[0] = reg & 0x3F; // 書き込み
    tx[1] = data;

    t.length = 8 * 2;
    t.tx_buffer = tx;

    spi_device_transmit(spi, &t);
}

// 複数バイト読み込み
void adxl_read(uint8_t reg, uint8_t *data, int len)
{
    spi_transaction_t t = {0};
    uint8_t tx[1 + len];
    uint8_t rx[1 + len];

    tx[0] = reg | 0xC0;
    memset(&tx[1], 0, len);

    t.length = (1 + len) * 8;
    t.tx_buffer = tx;
    t.rx_buffer = rx;

    spi_device_transmit(spi, &t);

    memcpy(data, &rx[1], len);
}

// 初期設定
void adxl_init(void)
{
    // Standby
    adxl_write(REG_POWER_CTL, 0x00);

    // フル解像度 ±2g
    adxl_write(REG_DATA_FORMAT, 0x08);

    // 測定モード
    adxl_write(REG_POWER_CTL, 0x08);
}

// 加速度取得
void read_accel(int16_t *x, int16_t *y, int16_t *z)
{
    uint8_t data[6];
    adxl_read(REG_DATAX0, data, 6);

    *x = (int16_t)(((uint16_t)data[1] << 8) | data[0]);
    *y = (int16_t)(((uint16_t)data[3] << 8) | data[2]);
    *z = (int16_t)(((uint16_t)data[5] << 8) | data[4]);
}

// メイン
void app_main(void)
{
    spi_init();
    adxl_init();

    ESP_LOGI(TAG, "ADXL345 Initialized");

    while (1) {
        int16_t x, y, z;

        read_accel(&x, &y, &z);

        ESP_LOGI(TAG, "X=%d Y=%d Z=%d", x, y, z);

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

出力

このようにX, Y, Zの計測値が表示される。

3.3 OLEDディスプレイ:ssd1306

nopnop2002さんのgithubからesp-idf-ssd1306を使用させていただきました。
https://github.com/nopnop2002/esp-idf-ssd1306

準備

  1. git cloneで、リポジトリをローカルに保存する
git clone https://github.com/nopnop2002/esp-idf-ssd1306.git
  1. mainフォルダのある階層にgit cloneした中にあるcomponentsフォルダをコピーする

  2. mainフォルダがある方のCMakeLists.txtを以下の1文を追記する。

set(EXTRA_COMPONENT_DIRS ../components/ssd1306)

  1. menuconfigの設定を書き換え
    SSD1306 Configuration / Interface (I2C Interface) / SPI Interfaceにチェックを入れる
具体的なmenuconfigの操作画面

SSD1306 Configurationで右矢印キーを押す

Interface (I2C Interface)で右矢印キーを押す

( )SPI Interface で右矢印キーを押す

Escキーでホームまで戻り、Yボタンで設定を反映する

配線図

ESP32 SSD1306(SPI)
GND GND
3V3 VCC
GPIO 18 D0
GPIO 23 D1
GPIO 15 RES
GPIO 4 DC
GPIO 5 CS

つまり、こんな感じ。

プログラム

nopnop2002さんのgithub上のプロジェクト「TextDemo」を使用しました。

出力

3.4 RFIDタグ:RFID-RC522

Sponge5さんのgithubを使わせていただきました。
https://github.com/Sponge5/rc522

準備

mainフォルダと同じ階層にcomponentsフォルダを作成し、そこでgit cloneする。

mkdir components
cd components
git clone https://github.com/Sponge5/rc522.git

依存関係を修正する。
rc522フォルダ内のCMakeLists.txtidf_component.yml

mainフォルダの中のCMakeLists.txtに追記

配線図

SPI通信で接続する場合は、以下のように配線する
IRQ(割り込み)は、今回使用しないため未接続にしている

ESP32 RFID_RC522
GPIO 5 SDA
GPIO 18 SCL
GPIO 23 MOSI
GPIO 19 MISO
-(N.C) IRQ
GND GND
GPIO 22 RST
3V3 3.3V

こんな感じ。

プログラム

githubのexampleから、GPIOのPIN番号変更のみ行っています。

短いけど折り畳み
#include <esp_log.h>
#include <inttypes.h>
#include "rc522.h"

static const char* TAG = "rc522-demo";
static rc522_handle_t scanner;

static void rc522_handler(void* arg, esp_event_base_t base, int32_t event_id, void* event_data)
{
    rc522_event_data_t* data = (rc522_event_data_t*) event_data;

    switch(event_id) {
        case RC522_EVENT_TAG_SCANNED: {
                rc522_tag_t* tag = (rc522_tag_t*) data->ptr;
                ESP_LOGI(TAG, "Tag scanned (sn: %" PRIu64 ")", tag->serial_number);
            }
            break;
    }
}

void app_main()
{
    rc522_config_t config = {
        .spi.host = VSPI_HOST,
        .spi.miso_gpio = 19,
        .spi.mosi_gpio = 23,
        .spi.sck_gpio = 18,
        .spi.sda_gpio = 5,
    };

    rc522_create(&config, &scanner);
    rc522_register_events(scanner, RC522_EVENT_ANY, rc522_handler, NULL);
    rc522_start(scanner);
}

出力

カードやタグを近づけると、読み取った番号が表示されます。

4. まとめ

  • ESP32でC言語を用いたSPI通信を使った事例を試した
  • github上にある先駆者のコードを使うことで、簡単に使用することができた
  • menuconfigなどの設定は見落としがちなので注意!

Discussion