TOPに戻る

Raspberry Pi Picoでプログラミング ⑬ spi APIとA-DコンバータMCP3008

 SPIバスは、I2Cよりデータの転送速度を高くできます。 Picoにはspi0とspi1のバスがあります。チップ・セレクトCS信号は、ユーザがGPIOピンを独自に制御するプログラムを書きます。

 クロック(ボーレート)は2MHz以上がサポートされていると書かれていますが、サンプル・プログラムは500kHzが見られます。最大値は11.083MHzと思われます。中途半端な周波数の設定を行ったとき、クロックと分周比から、それに近い値が設定されます。

 SPIでは、クロックの立ち上がりと立ち下がり、HighのときもしくはLowのとき、とデータが確定する条件が4種類あります。Picoでは一般的なMotorola SPI frame formatが採用され、クロックの極性(cpol)とクロックの位相(cpha)により、四つの動作モードが指定できます。

 多くのデバイスでは、モード0(cpol=0、cpha=0)が使われます。Picoのマニュアルの中で、デフォルトがこのモードであるという記述は見つかっていませんが、このモードにセットされていると思われます。

 Motorolaの規格以外に、National Semiconductor Microwire frame formatもサポートされているようですが、見かけません。

APIファンクション

ファンクション 要約
void spi_init (spi_inst_t *spi, uint baudrate) ほかの関数の前に呼び出し、SPIインスタンスを初期化する
void spi_deinit (spi_inst_t *spi) SPIを無効状態にする。 デバイスの機能を再度有効にするには、initを呼び出す必要がある
uint spi_set_baudrate (spi_inst_t *spi, uint baudrate) SPIボーレートを設定。Hz。2Mbps以上
static uint spi_get_index (spi_inst_t *spi) ハードウェアのインスタンス番号0もしくは1が返る
static void spi_set_format (spi_inst_t *spi, uint data_bits, spi_cpol_t cpol, spi_cpha_t cpha, __unused spi_order_t
order)

SPIの構成。

*spi spi0またはspi1
data_bits 有効な値4..16
cpol SSPCLKOUT極性
cpha SSPCLKOUT位相
order MSB_FIRSTだけ

static void spi_set_slave (spi_inst_t *spi, bool slave) slave trueでスレーブに設定、できないときはデフォルトのマスタ
static size_t spi_is_writable (spi_inst_t *spi) SPIデバイスで書き込みができるかどうかを確認。0以外ならOK
static size_t spi_is_readable (spi_inst_t *spi) SPIデバイスで読み取りを実行できるかどうかを確認。0以外ならOK
int spi_write_read_blocking (spi_inst_t *spi, const uint8_t *src, uint8_t *dst, size_t len)

srcからSPIにlenバイトを書き込む。同時に、SPIからdstへlenバイトを読み取る

int spi_write_blocking (spi_inst_t *spi, const uint8_t *src, size_t len) srcからSPIにlenバイトを書き込み、すべてのデータが転送されるまでブロック、受信したデータは破棄する
int spi_read_blocking (spi_inst_t *spi, uint8_t repeated_tx_data, uint8_t *dst, size_t len) dstからlenバイト分読み取る。RXからデータを読み込むと、TXでrepeated_tx_dataが繰り返し出力される。 通常、これは0にすることができるが、一部のデバイスではここで特定の値が必要。 SDカードは0xffを期待
int spi_write16_read16_blocking (spi_inst_t *spi, const uint16_t *src, uint16_t *dst, size_t len) srcからSPIにlenハーフ・ワード(2バイト?)を書き込む。 SPIからdstへのlenハーフ・ワードを同時に読み取る
int spi_write16_blocking (spi_inst_t *spi, const uint16_t *src, size_t len) srcからSPIにlenハーフ・ワード(2バイト?)を書き込む。 受信したデータを破棄する
int spi_read16_blocking (spi_inst_t *spi, uint16_t repeated_tx_data, uint16_t *dst, size_t len) dstからlenバイト分読み取る。RXからデータを読み込むと、TXでrepeated_tx_dataが繰り返し出力される。 通常、これは0にすることができるが、一部のデバイスではここで特定の値が必要。 SDカードは0xffを期待

A-DコンバータMCP3008

 Picoには12ビットA-Dコンバータが内蔵されています。ここでは、10ビットA-DコンバータMCP3008を外付けして、同じ電圧を読み込んで比較します。

  データシート

MCP3008のおもなスペック

  • 分解能 10ビット
  • 入力数 シングルエンド;8チャネル、疑似差動;4チャネル
  • 動作電圧 2.7~5.5V
  • サンプリング速度 200ksps
  • 変換方式 逐次変換SAR(Successive Approximation Register)
  • 動作時の電流 500uA(5V時)
  • 動作温度範囲 -40~85℃
  • インターフェース SPI(クロック 最大3.6MHz;5V時)
  • パッケージ DIP、SOIC、TSSOP

 基準電圧Vref入力、アナログ・グラウンドAGNDとディジタル・グラウンドDGNDが独立した端子で用意されています。

接続

 基準電圧源VrefはVccにつなぎます。アナログ・グラウンドAGNDはディジタル・グラウンドDGNDへつなぎます。

ソースの作成

 pico/worksフォルダにmcp3008フォルダを作ります。なかに、CMakeLists.txtとmcp3008.cを入れます。

 pico/worksフォルダのCMakeLists.txtの内容です。


cmake_minimum_required(VERSION 3.12)

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

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

set(PICO_EXAMPLES_PATH ${PROJECT_SOURCE_DIR})

# Initialize the SDK
pico_sdk_init()

# Add blink example
add_subdirectory(cmake)
add_subdirectory(blink)
add_subdirectory(serial)
add_subdirectory(clock)
add_subdirectory(i2cscanner)
add_subdirectory(lps25hb)
add_subdirectory(tmp117)
add_subdirectory(aht20)
add_subdirectory(mcp3008)

 pico/works/mcp3008フォルダのCMakeLists.txtの内容です。


add_executable(mcp3008
        mcp3008.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(mcp3008 pico_stdlib 
        hardware_spi hardware_gpio hardware_adc)

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

 mcp3008.cの内容です。VrefはDMMの岩通VOAC7602の読み取り値です。

 チャネルのデータの作り方は、

  A-Dコンバータ その2 10ビットSPI MCP3008 -(1)

に詳しい解説があるので参照してください。

 じっさいにSPIバスでデータを読んでいるのが、

  spi_write_read_blocking(SPI_PORT, writeData, buffer, 3);

です。writeData[]は3バイトのチャネル指定のデータです。最後の1バイトはダミーです。3バイト送ると3バイトのデータが読み出されます。buffer[]に読み出された最初のバイトはごみなので捨てます。


#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"

#define PIN_SCK  2
#define PIN_MOSI 3
#define PIN_MISO 4
#define PIN_CS   5

#define SPI_PORT spi0
static float Vref = 3.2562;


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");
}

void setup_SPI(){
    // This example will use SPI0 at 2MHz.
    spi_init(SPI_PORT, 2 * 1000 * 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);
}

int readADC(uint8_t ch){
    uint8_t writeData[] = {0b00000001, 0x00, 0x00};
    switch(ch){
      case 0:
        writeData[1] = 0b10000000; 
      break;
      case 1:
        writeData[1] = 0b10010000; 
      break;
      case 2:
        writeData[1] = 0b10100000; 
      break;
      case 3:
        writeData[1] = 0b10110000; 
      break;
      case 4:
        writeData[1] = 0b11000000; 
      break;
      case 5:
        writeData[1] = 0b11010000; 
      break;
      case 6:
        writeData[1] = 0b11100000; 
      break;
      case 7:
        writeData[1] = 0b11110000; 
    }
    // printf("\n %0b %0b %0b\n",writeData[0],writeData[1],writeData[2]);
    uint8_t buffer[3];
    cs_select();
    sleep_ms(1);
    spi_write_read_blocking(SPI_PORT, writeData, buffer, 3);
    sleep_ms(1);
    cs_deselect();

    return (buffer[1] & 0b00000011) << 8 | buffer[2];
}

int main() {
    stdio_init_all();

    printf("\nHello, MCP3008 Reading raw data from registers via SPI...\n");

    setup_SPI();

    for (uint8_t i=0; i<8; i++){
        printf("ch%d is %.4fV\n", i, Vref * readADC(i) / 1024);
        sleep_ms(10);
    }
    return 0;
}

 ターミナルで、pico/works/buildにおります。

cmake ..

 pico/works/build/mcp3008におります。

make -j4

 Resetボタンを押したまま、BOOTSELボタンを押し、Resetボタンを離してから、BOOTSELボタンを離します。RPI-RP2ドライブがマウントされました。

 RPI-RP2ドライブへ、mcp3008.uf2をドラッグします。

 ch0には、Analog Discovery Pro ADP3450のSupplies出力をつないでいます。ch1からch7は何もつないでいません。Supplies出力をDMMで測った電圧は3.24465Vです。

内蔵のアナログ入力のデータを読む

 内蔵のA-Dコンバータを読むように修正したmcp3008.cの内容です。


#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"


#define PIN_SCK  2
#define PIN_MOSI 3
#define PIN_MISO 4
#define PIN_CS   5

#define SPI_PORT spi0
static float Vref = 3.2562;


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");
}

void setup_SPI(){
    // This example will use SPI0 at 2MHz.
    spi_init(SPI_PORT, 2 * 1000 * 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);
}

int readADC(uint8_t ch){
    uint8_t writeData[] = {0b00000001, 0x00, 0x00};
    switch(ch){
      case 0:
        writeData[1] = 0b10000000; 
      break;
      case 1:
        writeData[1] = 0b10010000; 
      break;
      case 2:
        writeData[1] = 0b10100000; 
      break;
      case 3:
        writeData[1] = 0b10110000; 
      break;
      case 4:
        writeData[1] = 0b11000000; 
      break;
      case 5:
        writeData[1] = 0b11010000; 
      break;
      case 6:
        writeData[1] = 0b11100000; 
      break;
      case 7:
        writeData[1] = 0b11110000; 
    }
    // printf("\n %0b %0b %0b\n",writeData[0],writeData[1],writeData[2]);
    uint8_t buffer[3];
    cs_select();
    sleep_ms(1);
    spi_write_read_blocking(SPI_PORT, writeData, buffer, 3);
    sleep_ms(1);
    cs_deselect();

    return (buffer[1] & 0b00000011) << 8 | buffer[2];
}

int main() {
    stdio_init_all();
    adc_init();
    adc_gpio_init(26);
    adc_select_input(0);
    const float conversion_factor = Vref / (1 << 12);

    printf("\nHello, MCP3008 Reading raw data from registers via SPI...\n");

    setup_SPI();

    while (1) {
        for (uint8_t i=0; i<8; i++){
            printf("ch%d is %.4fV\n", i, Vref * readADC(i) / 1024);
            sleep_ms(10);
        }
        uint16_t result = adc_read();
        printf("ADC voltage: %f V\n", result * conversion_factor);
        sleep_ms(5000);
    }
    return 0;
}

 実行した様子です。ADC voltageが内蔵のA-Dコンバータの測定結果です。

電圧を変えてデータを取ってグラフにする

 入力の電圧を0.01~3.24Vまで変化させ、DMM(横軸)、MCP3008(青色)、Pico内蔵のA-Dコンバータ(赤色)の出力を記録し、グラフにしました。

 Picoのグラフが直線的でないことがわかります。

 次の図は、DMMの値に対してどのくらい離れた値かを抽出したグラフです。0.01Vのときは、どちらも誤差が大きいです。それ以外でもPicoのA-Dコンバータの誤差は大きいように見えます。どちらも、0.5V以下の電圧は誤差が大きくなるようです。

 カタログ上、Picoは12ビットA-Dコンバータですが、ENOB(有効ビット数)は9ビットと書かれています。10ビットA-DコンバータのMCP3008のENOBは約9.8ビットです。

 実験結果からは、10ビットの外付けA-Dコンバータのほうが、内蔵の12ビットA-Dコンバータより、安心して利用できるように思えます。

連載 Raspberry Pi Picoでプログラミング

(1) ラズパイ4の準備(1) USBブートの設定

(2) ラズパイ4の準備(2) 標準入出力の用意

(3) ラズパイ4の準備(3) LチカとHello, world!の実行

(4) ラズパイ4の準備(4) リモート環境の設定

(5) プログラミングの環境整備とLチカ

(6) Hello, World!

(7) 使用するピンと機能

(8) クロックの値の表示

(9) i2cscanner

(10) i2c APIと気圧センサLPS25

(11) i2c 温度センサTMP117

(12) i2c 湿度センサAHT20

(13) spi APIとA-DコンバータMCP3008

(14) spi A-DコンバータMCP3208

(15) gpioファンクション