ESP-WROOM-02のSPI処理をRaspberry Pi Pico Wに移植する

　前々回、「Raspberry Pi Pico W サーミスタで気温を計算する」で、Rapberry Pi Pico のADCサンプルプログラムを改造してサーミスタの気温を測定するプログラム(C言語)を作成する方法を紹介しました。
[Raspberry Pi Pico W サーミスタで気温を計算する (Qiita@pipito-yukio)](https://qiita.com/pipito-yukio/items/59f7a853c27bfbccd21e) 

以降 Raspberry Pi をラズパイとして説明します。

　今回は**ESP-WROOM-02用に作成したSPI接続のADコンバータ制御プログラム(cpp)をラズパイPico用 (C言語) に移植する方法**を紹介します。
　下記投稿が元ネタになります。 
[ESP-WROOM-02 サーミスタの温度をアナログコンバーターで取得する (Qiita@pipito-yukio)](https://qiita.com/pipito-yukio/items/9a1bff2c9333d7351f5a)

### ターゲット

+ ラズパイ Pico (or Pico W)
※Wi-Fi機能を使っていないので Raspberry Pi Pico でも動作可能

### 開発環境
+ OS: Ubuntu 22.04
+ ラズパイ Pico SDK とサンプルがセットアップ済み
+ VScode for Linux
    + Microsoft C/C++ Extension Pack がインストール済み
    + Micfosoft CMake Tools がインストール済み
    + Micfosoft のシリアルモニターがインストール済み

### SPIのサンプル一覧

使用するソースは下記 ```mpu9250_spi``` ディレクトリの ```mpu9250_spi.c``` です。

```
$ tree -d pico-examples/spi
pico-examples/spi
├── bme280_spi
├── max7219_32x8_spi
├── max7219_8x7seg_spi
├── mpu9250_spi        # 9軸(加速度/ジャイロスコープ)センサー 
├── spi_dma
├── spi_flash
└── spi_master_slave
    ├── spi_master
    └── spi_slave
```


#### 1-1. PICO用サンプルソース

利用した関数の抜粋のみ下記に示します。

+ Chip Select 切り替え関数: ```cs_select()``` | ```cs_deselect()```
※そのまま利用
+ レジスター読み取り関数: ```read_registers()```
※SPI接続ADコンバータ(MCP3002) からの読み出し処理に合わせて改造
+ SPI初期化処理: ```main()```
※最低限必要な処理のみ流用

```mpu9250_spi.c

static inline void cs_select() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 0);  // Active low
    asm volatile("nop \n nop \n nop");
}

static inline void cs_deselect() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 1);
    asm volatile("nop \n nop \n nop");
}

static void read_registers(uint8_t reg, uint8_t *buf, uint16_t len) {
    // For this particular device, we send the device the register we want to read
    // first, then subsequently read from the device. The register is auto incrementing
    // so we don't need to keep sending the register we want, just the first.

    reg |= READ_BIT;
    cs_select();
    spi_write_blocking(SPI_PORT, &reg, 1);
    sleep_ms(10);
    spi_read_blocking(SPI_PORT, 0, buf, len);
    cs_deselect();
    sleep_ms(10);
}

// ...一部省略...

int main() {
    stdio_init_all();

    // This example will use SPI0 at 0.5MHz.
    spi_init(SPI_PORT, 500 * 1000);
    gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);

    // Chip select is active-low, so we'll initialise it to a driven-high state
    gpio_init(PIN_CS);
    gpio_set_dir(PIN_CS, GPIO_OUT);
    gpio_put(PIN_CS, 1);
    //... 以降省略 ...
```

#### 1-2. ESP-WROOM-02用のSPI処理(Arduino: C++)

+ ADコンバータからの読み出し処理: ```readValue```
+ SPIクラスの初期化処理: ```setup```

```cpp
#include <Arduino.h>
#include <SPI.h>

uint16_t readValue(uint8_t ch) {
   SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
   digitalWrite(PIN_SPI_SS, LOW);
   // Read analog value.
   byte _highByte = SPI.transfer(0b01101000 | (ch << 4));
   byte _lowByte = SPI.transfer(0x00);
   digitalWrite(PIN_SPI_SS, HIGH);
   SPI.endTransaction();
   u_int16_t adcVal = ((_highByte & 0x03) << 8) | _lowByte;
   return adcVal;
}

void setup() {
  //... SPIの初期化部分のみ ...
  pinMode(PIN_SPI_SS, OUTPUT);
  digitalWrite(PIN_SPI_SS, HIGH);
  SPI.begin();
  // ...省略...
}
```

### 2. SPI接続ADコンバータ(MCP3002)用の処理

#### 2-1.初期化処理の比較

<table>
<tr>
<th>処理内容</th>
<th>Raspberry Pico SDK (C)</th>
<th>Arduino (C++) </th>
</tr>

<tr>
<td>spi初期化(ピン)</td>
<td>
spi_init(spi0, 100*1000);<br/>
gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);<br/>
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);<br/>
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
</td>
<td>SPI.bigin();<br/>
※ESP-WROOM-02ではSPIは1系統のみ
</td>
</tr>

<tr>
<td>CSピンモード設定</td>
<td>
gpio_init(PIN_CS);<br/>
gpio_set_dir(PIN_CS, GPIO_OUT);
</td>
<td>pinMode(PIN_SPI_SS, OUTPUT);</td>
</tr>

<tr>
<td>デバイス選択初期化※解除</td>
<td>gpio_put(PIN_CS, 1);</td>
<td>digitalWrite(PIN_SPI_SS, HIGH);<td>
</tr>
</table>

#### 2-2. データ取得処理の比較

　Arduino環境の **SPI.transfer**()に対応する **Pico SDK の関数は spi_write_read_blocking**() になります。
　以下は 公式ドキュメント「Raspberry Pi Pico C/C++ SDK」の該当関数の抜粋です。
![PicoSDK_API_spi_spi_write_read_blocking.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/f70bcae1-1a2b-f806-a1b7-05e8f6371b99.png)


<table>
<tr>
<th>処理内容</th>
<th>Raspberry Pico SDK (C)</th>
<th>Arduino (C++) </th>
</tr>
<tr>
<td>デバイス選択</td>
<td>
cs_select()　※サンプルソース内
</td>
<td>digitalWrite(PIN_SPI_SS, LOW);<td>
</tr>

<tr>
<td>データ取得</td>
<td>
spi_write_read_blocking(spi0, &cmd, &data)
</td>
<td>byte data = SPI.transfer(cmd)<td>
</tr>

<td>デバイス選択解除</td>
<td>
cs_deselect()　※サンプルソース内
</td>
<td>digitalWrite(PIN_SPI_SS, HIGH);<td>
</tr>

</table>



### 3. 移植後のSPI接続ADコンバーターの処理

#### 3-1.データ取得処理

```c
static inline void cs_select() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 0);  // Active low
    asm volatile("nop \n nop \n nop");
}

static inline void cs_deselect() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 1);
    asm volatile("nop \n nop \n nop");
}

// Only MCP3002 ADC
static uint16_t analogRead(int8_t ch) {
   uint8_t cmd_1st = 0b01101000 | (ch << 4);
   uint8_t cmd_2nd = 0x00;
   uint8_t highByte;
   uint8_t lowByte;
   cs_select();
   spi_write_read_blocking(SPI_PORT, &cmd_1st, &highByte, 1);
   spi_write_read_blocking(SPI_PORT, &cmd_2nd, &lowByte, 1);
   cs_deselect();
   return ((highByte & 0x03) << 8) | lowByte;
}
```

#### 3-2.SPI初期化

使用するハードウェアSPIポート: **spi0** (or spi1)

```c
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/spi.h"

#define PIN_MISO 4
#define PIN_CS   5
#define PIN_SCK  6
#define PIN_MOSI 7
#define PIN_ADC0 26

#define SPI_PORT spi0

int main() {
   stdio_init_all();

   // MCP3002: 3.3v use SPI0 at 100KHz.
   spi_init(SPI_PORT, 100 * 1000);
   gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
   gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
   gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);

   // Chip select is active-low, so we'll initialise it to a driven-high state
   gpio_init(PIN_CS);
   gpio_set_dir(PIN_CS, GPIO_OUT);
   gpio_put(PIN_CS, 1);
```


### 4. 複数サーミスタの周辺温度測定

　移植したSPI接続ADコンバータ (MCP3002) とラズパイPico内蔵のADコンバータのそれぞれに接続したサーミスタの周辺温度を計算するプログラムを作成し、ラズパイPico内蔵のADコンバータとの差を比較します。

#### 4-0. 配線図
+ サーミスタ1 -> MCP3002 (SPI) -> Pico W SPI
  [GPIO4] MISO, [GPIO5] CS, [GPIO6] SCK, [GPIO7] MOSI
+ サーミスタ2 -> Pico W ADC0

![PicoW_02_mcp3002_adc_thermistors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/59afcbba-0558-58b9-4ae6-96f6bf8e2fdb.png)

#### 4-1. プロジェクト一覧

```
Raspi_pico/
├── CMakeLists.txt         # (1)プロジェクトルート
├── pico_sdk_import.cmake
└── pico_w
    ├── CMakeLists.txt     # (2) pico_wサブブロジェクト
    └── spi_mcp3002_adc
        ├── CMakeLists.txt # (3) 本ブロジェクト
        └── main.c         # (4) ソース本体
```

(1) プロジェクトルートのCmake構成ファイル

```Raspi_pico/CMakeLists.txt
cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pico_project C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialize the SDK
pico_sdk_init()

add_compile_options(-Wall
        -Wno-format          # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int
        -Wno-unused-function # we have some for the docs that aren't called
        )
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
    add_compile_options(-Wno-maybe-uninitialized)
endif()

# Add pico_w
add_subdirectory(pico_w)
```

(2) pico_wサブブロジェクトのCmake構成ファイル

```Raspi_pico/pico_w/CMakeLists.txt
add_subdirectory(spi_mcp3002_adc)
```


(3) 本プロジェクトのCmake構成ファイル

+ 開発PCからラズパイPicoにUSB接続し printf()出力をモニターする設定
    + USBシリアルを有効に設定
    + UARTシリアルを無効に設定

```Raspi_pico/pico_w/spi_mcp3002_adc/CMakeLists.txt
add_executable(spi_mcp3002_adc
        main.c
        )

# pull in common dependencies and additional spi hardware support
target_link_libraries(spi_mcp3002_adc pico_stdlib hardware_adc hardware_spi)

# From  ~/pico/pico-examples/hello_world/usb/CMakeLists.txt
# enable usb output, disable uart output
pico_enable_stdio_usb(spi_mcp3002_adc 1)
pico_enable_stdio_uart(spi_mcp3002_adc 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(spi_mcp3002_adc)
```

(4) ソース本体

+ サーミスタの周辺気温の計算
    + サーミスタ1: ADコンバータMCP3002から分圧抵抗の電圧を取得
    + サーミスタ2: ラズパイPico W のハードウェアADCから分圧抵抗の電圧を取得

サーミスタの計算式の詳細は下記投稿記事をご覧ください。
[ESP-WROOM-02 サーミスタの温度をアナログコンバーターで取得する](https://qiita.com/pipito-yukio/items/9a1bff2c9333d7351f5a)


```main.c
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
#include "pico/binary_info.h"
#include "hardware/spi.h"

/* 
   Measurement thermistor1 temperature from MCP3002 ADC
               thermistor2 temperature by builtin ADC0
*/
#define PIN_MISO 4
#define PIN_CS   5
#define PIN_SCK  6
#define PIN_MOSI 7
#define PIN_ADC0 26

#define SPI_PORT spi0

const uint8_t ADC_SAMPLES = 10;
// Builtin ADC0
const float ADC_VREF = 3.3;
// MCP3002 section
const uint8_t MCP_CH0 = 0;
const float MCP_VREF = 3.3;
// Thermister section
const float THERM_V = 3.3;
const float THERM_B = 3435.0;
const float THERM_R0 = 10000.0;
const float THERM_R1 = 10000.0; // Divide register 10k
const float THERM_READ_INVALID = -9999.0;
const unsigned long DELAY_TIME = 30000;

static inline void cs_select() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 0);  // Active low
    asm volatile("nop \n nop \n nop");
}

static inline void cs_deselect() {
    asm volatile("nop \n nop \n nop");
    gpio_put(PIN_CS, 1);
    asm volatile("nop \n nop \n nop");
}

// Only MCP3002 ADC
static uint16_t analogRead(int8_t ch) {
   uint8_t cmd_1st = 0b01101000 | (ch << 4);
   uint8_t cmd_2nd = 0x00;
   uint8_t highByte;
   uint8_t lowByte;
   cs_select();
   spi_write_read_blocking(SPI_PORT, &cmd_1st, &highByte, 1);
   spi_write_read_blocking(SPI_PORT, &cmd_2nd, &lowByte, 1);
   cs_deselect();
   return ((highByte & 0x03) << 8) | lowByte;
}

float getAdcVoltage() {
   uint16_t adcTotal = 0;
   for (int i = 0; i < ADC_SAMPLES; i++) {
      adcTotal += adc_read();
      sleep_ms(30);
   }
   uint16_t adcValue = round(1.0 * adcTotal / ADC_SAMPLES);
   printf("adc0.adcValue = %d\n", adcValue);
   return ADC_VREF * adcValue / (1 << 12);
}

float getMcpAdcVoltage(uint8_t ch) {
   uint16_t adcTotal = 0;
   for (int i = 0; i < ADC_SAMPLES; i++) {
      adcTotal += analogRead(ch);
      sleep_ms(30);
   }
   uint16_t adcValue = round(1.0 * adcTotal / ADC_SAMPLES);
   printf("mcp.adcValue = %d\n", adcValue);
   return MCP_VREF * adcValue / 1024;
}

float getThermTemp(float outVolt) {
   double rx, xa, temp;
   printf("Therm.outVolt = %.1f\n", outVolt);
   if (outVolt < 0.001 || outVolt >= THERM_V) {
      return THERM_READ_INVALID;
   }

   rx = THERM_R1 * ((THERM_V - outVolt) / outVolt);
   xa = log(rx / THERM_R0) / THERM_B;
   printf("rx: %.5f, xa: %.5f\n", rx, xa);
   temp = (1 / (xa + 0.00336)) - 273.0;
   return (float)temp;
}

int main() {
   stdio_init_all();

   // Initialize ADC HW
   adc_init();
   // Make sure GPIO is high-impedance, no pullups etc
   adc_gpio_init(PIN_ADC0);
   // Select ADC input 0 (GPIO26)
   adc_select_input(0);
   printf("ADC0 init.\n");

   printf("MCP3002 ADC Initialize with SPI...\n");
   // MCP3002: 3.3v use SPI0 at 100KHz.
   spi_init(SPI_PORT, 100 * 1000);
   gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
   gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
   gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);

   // Chip select is active-low, so we'll initialise it to a driven-high state
   gpio_init(PIN_CS);
   gpio_set_dir(PIN_CS, GPIO_OUT);
   gpio_put(PIN_CS, 1);
   printf("Complete Initialize.");

   while (true) {
      // Measure Thermister1 with MCP3002
      float outVolt = getMcpAdcVoltage(MCP_CH0);
      float temper = getThermTemp(outVolt);
      if (temper != THERM_READ_INVALID) {
         printf("MCP3002.Temper: %.1f ℃\n", temper);
      } else {
         printf("MCP3002 read invalid!\n");
      }
      sleep_ms(50);
      // Measure Thermister2 by ADC0
      outVolt = getAdcVoltage();
      temper = getThermTemp(outVolt);
      if (temper != THERM_READ_INVALID) {
         printf("ADC0.Temper: %.1f ℃\n", temper);
      } else {
         printf("ADC0 read invalid!\n");
      }
      sleep_ms(DELAY_TIME);
   }
}
```

#### 4-2. ビルド

ビルド方法の詳細については下記投稿記事の「3. ビルド」をご覧ください。
[Raspberry Pi Pico W サーミスタで気温を計算する (Qiita@pipito-yukio)](https://qiita.com/pipito-yukio/items/59f7a853c27bfbccd21e)

(1) Cmakeプリプロセス実行
```bash
cd build
$ cmake .. -DPICO_BOARD="pico_w" -DCMAKE_BUILD_TYPE=Release
```

(2) ビルド
```bash
cd pico_w/spi_mcp3002_adc
$ make -j4
```

(3) バイナリファイル
```bash
$ ls -lrt --time-style long-iso | grep uf2
-rw-rw-r-- 1 yukio yukio  66048 2024-03-11 15:58 spi_mcp3002_adc.uf2
```


### 5. バイナリの実行

#### 5-1. ラズパイPicoにバイナリをコピー 
+ [BOOTSEL] ボタンを押しながら USBケーブルを開発PCに接続
+ ラズパイPicoがマウントポイント(```/media/[ユーザー]/RPI-RP2```)にマウントされる
+ ファイルマネージャ(Nautilus)からバイナリファイルをドラック&コピー
+ 数秒後にラズパイPicoが自動的にアンマウントされる
+ 測定開始

![Ubuntu_pico_w_mcp3002_thermistor.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/ca61c9c1-ca01-2567-039d-f9759fab37cc.jpeg)

#### 5-2. VSCodeでシリアル出力のモニター

+ VSCodeのシリアルモニタータブに切り替える
+ ポートを ```/dev/ttyACM0```に切り替える
+ [監視の開始]ボタンを押下しモニターを開始する

![vscode_serialMon_with_mcp3002_adc0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/ecaca0f5-2d59-2d76-2e31-f4e8a29977bd.png)

#### 5-3. 測定値の比較
(1) ラズパイPico W の内蔵ADCの気温のほうが常に低めに出る傾向が見られました。
(2) ADコンバータMCP3002の測定した気温も若干低めに出ていますが簡易温度計に近い値を示しています。

### 最後に

　今回 ESP-WROOM-02用に作ったSPI接続ADコンバータ(MCP3002)の処理をラズパイPico W 用に移植しました。やはりESPモジュールの制御で作成した実装(C++) と比べるとラズパイPicoの実装はだいぶ低レベルな処理をコーディングしなければならず学習コストは少なからず高いと感じています。

　Qiitaで投稿されているラズパイPicoのアプリは Micro python で開発したものが多いようですが、乾電池駆動で省エネ運転を考慮したセンサーをラズパイPico W と一体化したモジュールを作成するとしたら個人的にはC言語で作成したいなと思っています。
 　C言語によるサンプルが豊富なので時間をかければなんとかなりそうな気がしていますが。

今回の記事のソースコードは下記GitHubで公開しています。

[GitHub: pipito-yukio/raspi_pico](https://github.com/pipito-yukio/raspi_pico/tree/main/src)