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 |
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コンバータより、安心して利用できるように思えます。