鳥なき里のマイコン屋(138) ラズパイPico、外付けDAC、MCP4725接続

Joseph Halfmoon

別シリーズにてSTM32マイコンのAD、DAを動かしているので、ラズパイPicoでもやってみるか、と思い立ちました。しかし、ラズパイPicoにはADの搭載はあるものの、DAはありません(PWMはあり。)また別件にてラズパイPico用の1ビットDACボードも頂いたのですが、今回は外付け12bit DAを接続してみます。

(末尾に実験で使用したCのソース全文を掲げました。)

今回接続してみる外付け12bit DAコンバータについては、1度投稿させてもらったことがあります。以下です。

部品屋根性(44) マイクロチップ MCP4725、12bit DAコンバータ

でもそのときは、購入後「とりあえず」電源入れて、初期設定の出力電圧がちゃんと出ているよね、という静的な確認だけでした。なんといってもI2C接続のDAコンバータなので、どちらかというと何かの電圧の調整向け?という感じ。動的な波形を作るのには向いていないのではないか、という理解であります。

しかし、別件でやっているのは音声周波数帯域(10kHzサンプリングですが)の動的な信号です。このI2C接続のMCP4725とラズパイPicoでどのくらいできそうなのか?今回確かめてみることにいたしました。

まずMCP4725については、マイクロチップ社の以下のページに情報があります。

mcp4725 | Microchip Technology

ラズパイPicoとの接続は簡単です(といいながら端子番号を間違えて動かん、と悩みましたが。)PicoのハードウエアI2Cは2チャンネルですが、ほとんどの端子にどちらかをアサインできます。今回はボード末端の接続しやすい側の端子から取り出しやすかったI2C1を接続しています。MCP4725は端子でI2Cアドレスを切り替えられますが、下1ビットだけです。制御をみるとアドレスは下3ビットを設定できるようですが、データシート読むと上の2ビットは工場オプションみたいです。今回はA0端子にHighを加えて、I2Cアドレス0x61番地ということで使っています。なお下の接続図上はI2Cのプルアップ抵抗など見えないですが、秋月製のDIP化モジュールを使用しているためです。DIP化モジュール上にプルアップやパスコンなど必要なものが皆のっています。らくちん。配線するだけ。
RPiPicoMCP4725_Schematic

I2Cバスの速度制限

調べてみると、MCP4725のI2Cは以下の3モード対応でした。

  1. Standard 100kHz
  2. Fast 400kHz
  3. High-Speed 3.4MHz

なお、High-Speedを利用するためには設定必要です。しかし、ラズパイPicoのハードウエアI2Cは1,2 の対応はありますが、3はありません(他に1000kHzの規格に対応みたいです。残念ながらそちらはMCP4725データシートには書かれていません。)

確実に使える通信速度の上限は400kHz、ちと遅い感じがします。サンプリング速度の目標10kHzなのですが、10kHz=100μ秒の間に、最低2回、実際にはもっと多くの回数、データを送信しなければなりません。送信ビット列は30ビットくらいはあるので、Standardの100kHzクロックでは300μ秒以上はかかる計算。4倍速でもほぼほぼ無理そう。まあ、その辺を「だましだまし」設定しながら実地に確かめていきます。

ラズパイPico側が3.4MHzのインタフェースに対応できれば良いのだよね、という考えも浮かびますが、本日はやりませぬ。また、そのうち(って何時だ?)

実験に使用したソフトウエア

ラズパイPicoのC/C++SDKを使用させてもらってビルドしています。骨子は以下のようです。

  • I2Cバス1からMCP4725に出力する12ビットの値(符号なし整数)を送る。その際転送バイト数が一番短くなる「簡素」なモード(垂れ流し)を使う。
  • ラズパイPicoの高水準APIのRepeatingタイマのコールバック関数を一定周期で動かしておく。コールバック関数の中で上記の転送を開始することで一定周期で送信する
  • DACからは階段状のノコギリ波を出力してみる。周期とDA出力していることが分かり易いので。

問題は、I2Cバスが遅いため、どれほどのレートで通信できるかということであります。まずはスタンダードの100kHzクロック利用、タイマのインターバルは余裕の1m秒として、1周期に16回DAへデータを送っているのがこちら。黄色がI2Cクロックで、青が出力アナログ信号です。黄色の太い縦棒に見えますがその区間に30発くらいのパルスが隠れています。

I2Cwave100Ktimer1K

とりあえず、思ったような波形を出せていることが分かりました。しかし、余裕の1m秒周期の筈だけれど、結構転送期間の割合多くね?さっさと400kHzクロックに変えましょう。以下はI2Cクロック400kHz、タイマのインターバルは100μSです(実際のインターバルは処理時間分が被っているので異なります。高水準APIのタイマは正確な周期タスクにはならないみたい。)

1周期16点データ送信の波形がこちら。ノコギリ波(階段状ですが)の周波数は実測にて340Hzほどです。転送時間もまだもう少し詰められそうではありますが、倍にはできそうにありません。そこで1周期16点を値切ってみることにいたしました。

I2Cwave400Ktimer10K1周期に4点のDA出力、かなり階段がアカラサマになりました。後はLPFお願いっていうところですか。でも意外と波形はキレイに階段しています。これでノコギリ波として見たときの周波数は1.36kHz。まだ、転送に使っていない時間が残っているので、もう少し追い込めそうではありますが。ハードウエアのI2Cで400kHzクロックでは、せいぜいこんなもんでしょうか。

I2Cwave400Ktimer10Kstep4通信速度さえ速くできれば、MCP4725の実力的にはもっと上の周波数でもいけそうです。3.4MHz化を考えますか。でもその前にADもみておかないと。

鳥なき里のマイコン屋(137) ラズパイPico、C/C++SDKでPIO、サイド出力 へ戻る

鳥なき里のマイコン屋(139) ラズパイPico、内蔵ADCから外付けDACへ「スルー」 へ進む

実験に使用したCのソースコード(Raspberry Pi Pico C/C++SDK利用)
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/i2c.h"
#include "hardware/timer.h"

// ON Board LED
#define LED_PIN (25)

// I2C defines
#define I2C_PORT i2c1
#define I2C_SDA 14
#define I2C_SCL 15
#define MCP4725_ADDR    (0x61)
// I2C clock [kHz]
#define I2C_SPEED       (400)
// timer interval [us]
#define TIMER_ITVL      (100)
// Dac step
#define DAC_STEP        (0x400)

volatile uint16_t    dacVal = 0; 

// Timer callback
bool repeating_timer_callback(struct repeating_timer *t) {
    uint8_t buf[2];
    buf[0] = (uint8_t)((dacVal & 0xF00) >> 8);
    buf[1] = (uint8_t)(dacVal & 0xFF);
    dacVal += DAC_STEP;
    if (dacVal > 0xFFF) {
        dacVal = 0;
    }
    i2c_write_blocking(I2C_PORT, MCP4725_ADDR, buf, 2, false);
    return true;
}

int main()
{
    stdio_init_all();

    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    // I2C Initialisation. Using it at 100Khz.
    i2c_init(I2C_PORT, I2C_SPEED*1000);
    
    gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
    gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
    gpio_pull_up(I2C_SDA);
    gpio_pull_up(I2C_SCL);

    struct repeating_timer timer;
    add_repeating_timer_us(TIMER_ITVL, repeating_timer_callback, NULL, &timer);

    for (int i=0; i<4096; i++) {
        printf("%d\n",i);
        gpio_put(LED_PIN, 1);
        sleep_ms(2000);
        gpio_put(LED_PIN, 0);
        sleep_ms(500);
    }
    bool cancelled = cancel_repeating_timer(&timer);

    bi_decl(bi_1pin_with_name(I2C_SDA, "I2C1 SDA"));
    bi_decl(bi_1pin_with_name(I2C_SCL, "I2C1 SCL"));
    puts("End of Excution.");
    return 0;
}
CMakeLists.txtファイル(ご参考)
# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# initalize pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/pi/pico/pico-sdk")

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

project(tstMCP4725 C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(tstMCP4725 tstMCP4725.c )

pico_set_program_name(tstMCP4725 "tstMCP4725")
pico_set_program_version(tstMCP4725 "0.1")

pico_enable_stdio_uart(tstMCP4725 0)
pico_enable_stdio_usb(tstMCP4725 1)

# Add the standard library to the build
target_link_libraries(tstMCP4725 pico_stdlib)

# Add any user requested libraries
target_link_libraries(tstMCP4725
        hardware_i2c
        hardware_timer
        )

pico_add_extra_outputs(tstMCP4725)