メインコンテンツへスキップ
見出し画像

マイコン用Python: Micropython(MicroPico)

    1.概要

     Raspberry Pi PicoのGPIOピンを操作するためのライブラリである「MicroPython」を紹介します。

    1-1.Raspberry Pi Picoとは

     詳細は下記記事に記載の通りです。ポイントとして複数のGPIOピンを持ち制御などが出来ますが、Pythonのようにメモリを多く消費するものは適しておりません。
     そこでメモリの消費を抑えてPythonライクでコードを作成できるMicroPythonを利用※してPico上でプログラムを実行していきます。
    ※C言語や多言語(JavaScriptやRuby)でも作成可能

    1-2.MicroPythonとは

     MicrotpythonとはPython3の文法を使ってマイコンや組み込み機器のプログラミングができる言語であり、REPL※を通じて対話的にプログラムを実行できます。
    ※REPL:「Read(読み取り)、Eval(評価)、Print(印字)、Loop(ループ)」の頭字語であり、UART (USB経由)を介して通信
    (出典:Raspberry Pi Documentation What is MicroPython?)。

     MicroPythonは下記で管理されており、GitHubも公開されています。2024年3月時点ではβ版のためAPI変更の可能性があります。

     詳細は公式ドキュメントやGithub Discussionsがあるため、記事はこちらに従って説明していきます。

    1-3.MicroPicoとは

     MicroPico(旧Pico-W-Go)はVisual Studio Codeの拡張機能でMicrotpyhonを利用できるものです。

    2.Raspberry Pi PicoのGPIO

    2-1.GPIOの仕様

     ”Raspberry Pi Pico Datasheet”と”Raspberry Pi Pico W Datasheet”より、Picoの仕様は下記の通りです。

    1. I/O電圧:DC3.3V(Raspberry Piのように5Vは無し)

    2. 電源の供給電圧:DC1.8~5.5V

    3. インターフェース

      • 1× USB 1.1 micro B(電源供給、データ通信、プログラム書込)

      • 26× GPIO ピン (23×デジタル専用、3×アナログ/デジタル)

      • 2× SPI

      • 2× I2C

      • 2× UART

      • 3× ADC 12bit 500ksps

      • 16× PWM (8×A/Bチャンネル、Bは入力対応)

      • 8× PIO(プログラマブルI/O)※

      • 1× タイマー(4×アラーム, 1×Real Time Counter)

      • 1× 温度センサー

      • 1× LED

      • 1× 3ピンARMシリアル ワイヤ デバッグ(SWD)ポート

    ※PIO:SDカードやVGAなどのI/Fをエミュレート可能

    2-2.GPIOの配置

     Raspberry PiのGPIOの配置は下記の通りです(半分に分けて表示)。

    画像
    https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
    画像
    https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
    画像

    3.環境構築

     環境構築は下記記事の通りです。ThonnyかVS Code(MicroPico)を利用していきます。

    4.MicroPythonの概要

     まずはMicropyhonの概要を紹介します。本記事の内容は下記公式Docsを参照しました。

    4-1.MicroPython 言語と処理系

     MicroPython の言語の構文に関しては Python 3.4 標準の実装を目指しており、ほとんどの機能は「docs.python.org 」の言語リファレンスと同じです。

      ただし一部の機能はPythonと比較して挙動に違いがあることがあります。詳細は下記記事をご確認ください。

    4-2.MicroPython ライブラリ(組み込み)

     MicroPythonのライブラリは大きく分けて2つあります。

     それぞれのライブラリ一覧は下記の通りです。これらはMicropythonの機能に含まれているため、追加インストール無しで使用できます。
     詳細は次章以降で説明していきます。

    画像
    https://micropython-docs-ja.readthedocs.io/ja/latest/library/index.html

     4-2-1.machineモジュール

     MicroPythonライブラリにある"machineモジュール"はハードウェア関連の関数です。特にGPIOを利用する時に重要なモジュールとなります。

     machine モジュールは特定のボード上のハードウェアに関連する固有の関数を含んでおり、システム上のハードウェアブロック(CPU、タイマー、バスなど)へのアクセスと制御を実現します。
    ※誤って使用すると、誤動作、ロックアップ、ボードのクラッシュ、および極端な場合にはハードウェアの損傷を招く可能性があります。

     machineライブラリには下記クラスが存在します。

     4-2-2.ポート/ボード固有のライブラリ

     MicroPythonはPicoだけでなく様々なシステム/ハードウェアで動作します(例:Pyboard, Wipyなど)。

    画像

     Raspberry Pi Pico(RP2040)に固有のライブラリは"rp2"のみです。

    4-3.外部ライブラリ

     PythonではPyPI(pip install)を通じて外部からライブラリをインポートして使用することが出来ます。

     Micropythonでも同様に外部のライブラリ(モジュール)を利用することができます(※Python同様、自作したり別の場所からインストールも可能)。

     インストール方法は下記の通りです。参考としてssd1306をインストールした時の画像も追加しました。

    【外部ライブラリインストール方法:Thonny】

    1. ”ツール”タブの”パッケージを管理”を選択

    2. 検索BOXに検索したいライブラリ/モジュール名を記入

    3. ”Micropython-lib/PyPIで検索”ボタンを押す

    4. インストールボタンを押し、閉じるを押す

    5. ”表示”タブの”ファイル”を選択すると、Raspberry Pi Picoのlib内にインストールされているのを確認できる。

    画像

    【コラム:外部ライブラリはどこで管理されているのか?】
     Pythonの外部ライブラリはPyPIで管理されていますが、Micropythonの外部ライブラリはGitHub上のmicropython-libで管理されています。今回インストールしたライブラリは”micropython/micropython-lib/micropython/drivers/
    display/ssd1306”で管理されていました。

    画像
    https://github.com/micropython/micropython-lib/tree/master/micropython/drivers/display

    5.API|基本操作

    5-1.情報確認:help()

     基本的な情報確認は"help()"を使用します。

    [IN]
    help()
    [OUT]
    Welcome to MicroPython!
    
    For online docs please visit http://docs.micropython.org/
    
    For access to the hardware use the 'machine' module.  RP2 specific commands
    are in the 'rp2' module.
    
    Quick overview of some objects:
      machine.Pin(pin) -- get a pin, eg machine.Pin(0)
      machine.Pin(pin, m, [p]) -- get a pin and configure it for IO mode m, pull mode p
        methods: init(..), value([v]), high(), low(), irq(handler)
      machine.ADC(pin) -- make an analog object from a pin
        methods: read_u16()
      machine.PWM(pin) -- make a PWM object from a pin
        methods: deinit(), freq([f]), duty_u16([d]), duty_ns([d])
      machine.I2C(id) -- create an I2C object (id=0,1)
        methods: readfrom(addr, buf, stop=True), writeto(addr, buf, stop=True)
                 readfrom_mem(addr, memaddr, arg), writeto_mem(addr, memaddr, arg)
      machine.SPI(id, baudrate=1000000) -- create an SPI object (id=0,1)
        methods: read(nbytes, write=0x00), write(buf), write_readinto(wr_buf, rd_buf)
      machine.Timer(freq, callback) -- create a software timer object
        eg: machine.Timer(freq=1, callback=lambda t:print(t))
    
    Pins are numbered 0-29, and 26-29 have ADC capabilities
    Pin IO modes are: Pin.IN, Pin.OUT, Pin.ALT
    Pin pull modes are: Pin.PULL_UP, Pin.PULL_DOWN
    
    Useful control commands:
      CTRL-C -- interrupt a running program
      CTRL-D -- on a blank line, do a soft reset of the board
      CTRL-E -- on a blank line, enter paste mode
    
    For further help on a specific object, type help(obj)
    For a list of available modules, type help('modules')

     また利用できるモジュール一覧を取得する場合は”help('modules')”を使用します。

    [IN]
    help('modules')
    [OUT]
    __main__          array             framebuf          random
    _asyncio          asyncio/__init__  gc                re
    _boot             asyncio/core      hashlib           requests/__init__
    _boot_fat         asyncio/event     heapq             rp2
    _onewire          asyncio/funcs     io                select
    _rp2              asyncio/lock      json              socket
    _thread           asyncio/stream    lwip              ssl
    _webrepl          binascii          machine           struct
    aioble/__init__   bluetooth         math              sys
    aioble/central    builtins          micropython       time
    aioble/client     cmath             mip/__init__      uasyncio
    aioble/core       collections       neopixel          uctypes
    aioble/device     cryptolib         network           urequests
    aioble/l2cap      deflate           ntptime           webrepl
    aioble/peripheral dht               onewire           webrepl_setup
    aioble/security   ds18x20           os                websocket
    aioble/server     errno             platform
    Plus any modules on the filesystem

    5-2.MicroPython 内部のアクセスと制御

     参考までにリンク添付しておきます。


    6.API|I/O ピン制御:machine.Pin

     machineモジュールのクラスPinについて説明します。
     ピンオブジェクトは、I/O ピン (GPIO - 汎用入出力)を制御するために使われ、出力電圧を駆動したり、入力電圧を読み取ることができます。

     なおPin クラスを拡張したSignal クラスもあります。今回はPinのみ紹介しますが、条件(下記Docs参照)に応じて選定してよいと思います。

    6-1.Pinの初期化/設定:Pin()

     Pinクラスからオブジェクトを作成して下記を設定します。

    [API]
    classmachine.Pin(id, 
                     mode=-1, 
                     pull=-1, 
                     *, 
                     value=None, 
                     drive=0, 
                     alt=-1)
    1. id(チャネル)*:使用するGPIOピンの位置を指定

      • ピン番号は物理的でなくBCM numbersを指定

      • "LED"のようにピン名での指定や、タプル指定も可能

    2. mode(I/O設定))*:Input(信号を取得)かOutput(信号を出力)するか設定

      • 入力:Pin.IN(Pin.INの出力は整数0のため数値での指定も可)

      • 出力:Pin.OUT(Pin.OUTの出力は整数1のため数値での指定も可)

      • Pin.OPEN_DRAINやPin.ALTなどもある

    3. pull(プルアップ抵抗/プルダウン抵抗):pullup/pulldownを指定

      • None:指定なし

      • Pin.PULL_UP:プルアップ抵抗を有効化

      • Pin.PULL_DOWN:プルダウン抵抗を有効化

    4. value:Pin.OUT と Pin.OPEN_DRAIN モードでのみ有効で、指定されている場合は初期出力ピン値になる

    5. drive:ピンの出力電流を指定

      • 引数には Pin.DRIVE_0, Pin.DRIVE_1 などの定数のいずれかを指定

    6. alt:ピンの代替機能を指定し、ポートに依存する値を取る

     まずはPin0をDO(出力)、Pin2をDI(入力)として設定しました。

    [IN]
    from machine import Pin
    
    pin0 = Pin(0, Pin.OUT) #GPIO0を出力ピンに設定
    pin2 = Pin(2, Pin.IN) #GPIO2を入力ピンに設定
    
    print(pin0, pin2) 
    [OUT]
    Pin(GPIO0, mode=OUT) Pin(GPIO2, mode=IN)

    【コラム:プルアップ抵抗/プルダウン抵抗とは】
     ピンの初期電圧をLow(0V)にするかHighにしておくか指定する方法です。詳細は下記記事をご確認ください。 

    6-2.初期値の再設定:Pin.init()

     指定したパラメータを使ってピンを再初期化します。

    [API]
    Pin.init(mode=-1, pull=-1, *, value=None, drive=0, alt=-1)

    6-3.Inputsの取得/設定:Pin.value()

     ピンの状態(0/1)確認や出力設定するには"Pin.value([x])"を使用します。引数を省略した場合はピンの状態を取得し、引数を指定するとピンの状態を変更できます。

    [IN]
    from machine import Pin
    
    pin0 = Pin(0, Pin.OUT) #GPIO0を出力ピンに設定
    pin2 = Pin(2, Pin.IN) #GPIO2を入力ピンに設定
    
    # ピンの状態を取得
    print(f'PIN0(DO):{pin0.value()}')
    print(f'PIN2(DI):{pin2.value()}')
    [OUT]
    PIN0(DO):0
    PIN2(DI):0

    【コラム:電源ON/OFFの検出】
     LEDにツイッチを付けLEDの電源状態(ON/OFF)を検出してみました。今回はスイッチの動きに合わせて文字を出力させていますが、Timerと合わせて一定時間で音を鳴らしたり電源を切ったすることもでき省エネ設計もできると思います。

    画像
    [IN]
    from machine import Pin
    
    pin = Pin(16, Pin.IN) # GPIO16
    flag = False #LEDのON /OFFの状態把握用フラグ
    
    while True:
        if pin.value() == 1:
            if flag == False:
                flag = True
                print('LED ON')
        elif pin.value() == 0:
            if flag == True:
                flag = False
                print('LED OFF')
    画像

    6-4.Outputsの設定

     ピンの出力(電圧)を0/1(0V/3.3V)に変更可能です。設定方法は複数あるため好きなもの(可読性)を選定したらよいと思います。

    • ピンの出力レベルを "1" に設定:Pin.on(), Pin.value(1), Pin.high()

    • ピンの出力レベルを "0" に設定:Pin.off(), Pin.value(0), Pin.low()

      • Pin.high/lowの可用性はnrf, rp2, stm32 ポートのみ

     OUTPUTの動作が目視できるようにLEDを点灯させてみました。詳細は下記記事に記載しておりますので、抜粋の未記載しました。ブレッドボードを使用して「PIN(GPIO0)▶330Ω抵抗器▶LED▶GND」と繋ぎました。

    画像

     下記を実行するとHIGHでLEDが点灯し(電流が流れ)、LOWで消灯(電流が停止)していることが確認できます。また終了時はGPIO.cleanup()により消灯されることも確認できました。

    [IN]
    from machine import Pin
    import time
    
    pin0 = Pin(0, Pin.OUT) #GPIO0を出力ピンに設定
    
    #Lチカ実装
    try:
        while True:
            pin0.on() #pin0.value(1)でも可
            print(f'PIN0:{pin0.value()}')
            time.sleep(0.5)
            pin0.off() #pin0.value(0)でも可
            print(f'PIN0:{pin0.value()}')
            time.sleep(0.5)
    except KeyboardInterrupt:
        print('終了')
    
    [OUT]
    PIN0:1
    PIN0:0
    PIN0:1
    PIN0:0
    画像


    7.API|パルス幅変調:machine.PWM

     PWM(Pulse Width Modulation)出力はmachine.PWMを使用します。PINに関しては出力が出せるピンならどれでも問題ありません。

    画像
    https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf

     PWMの詳細は下記記事に記載の通りです。

    7-1.PWMピンの設定:machine.PWM()

     PWMオブジェクトを作成し、初期設定を行います。

    [API]
    classmachine.PWM(dest, *, freq, duty_u16, duty_ns, invert)
    • dest : PWM が出力される実体であり通常は machine.Pin オブジェクトを指定

    • freq : PWM サイクルの周波数を Hz 単位で設定する整数

    • duty_u16 :デューティ比を dutyu1665535\frac{duty_u16 }{65535}の比率で設定

    • duty_ns :デューティ比をナノ秒単位で設定

    • invert :True を指定するとそれぞれの出力を反転させる

     まずはGPIO15ピンにPWMを割り当てました。

    [IN]
    from machine import PWM 
    from machine import Pin
    import time
    
    #初期設定
    pin15 = Pin(15, Pin.OUT) #GPIO15を出力に設定
    set_freq = 50 #設定周波数[Hz]
    pwm = PWM(pin15, freq=set_freq) #PWMオブジェクトを作成
    print(pwm)
    [OUT]
    <PWM slice=7 channel=1 invert=0>

    7-2.PWMオブジェクトの設定変更

     PWMオブジェクトの設定を変更(再初期化)したい場合はPWM.init()を使用します。

    [API]
    PWM.init(*, freq, duty_u16, duty_ns)

    7-3.周波数の変更・確認:freq()

     周波数の確認及び変更は”PWM.freq([value])”を使用します。これにより、PWM 出力の現在の周波数を取得または設定します。

    [IN]
    from machine import PWM 
    from machine import Pin
    import time
    
    #初期設定
    pin15 = Pin(15, Pin.OUT) #GPIO15を出力に設定
    set_freq = 50 #設定周波数[Hz]
    pwm = PWM(pin15, freq=set_freq) #PWMオブジェクトを作成
    
    pwm.duty_u16(0) #デューティ比0%に設定
    print(f'周波数:{pwm.freq()}[Hz]')
    print(f'デューティ比:{pwm.duty_u16()}[%]')
    [OUT]
    周波数:50[Hz]
    デューティ比:0[%]

    7-3.デューティ比の変更・確認:duty_u16()

     デューティ比の確認及び変更は”PWM.duty([value])”を使用します。これにより、PWM 出力の現在のデューティ比を、0〜65535 の範囲の符号なし16ビット値として取得または設定します。
     単一の value 引数を指定すると、デューティ比が value / 65535 の比率として測定されます。イメージは下図の通りです。

    デューティ比[%]=16bit値65535デューティ比[%]=\frac{16bit値}{65535}

    画像

     参考として後述するサーボモーター制御において、角度から16ビット値のデューティ比を計算する関数を作成しており出力は下記の通りです。

    [IN]
    # 動作角度からデューティ比を算出する関数
    def convert_deg2duty_u16(deg, verbose=False):
        max_pulse = 2.4  # 90°時のパルス幅(ms)
        zero_pulse = 1.45  # 0°時のパルス幅(ms)
        min_pulse = 0.5  # -90°時のパルス幅(ms)
    
        # 角度からパルス幅を計算
        pulse_width = (max_pulse - min_pulse) / 180 * deg + zero_pulse
        duty_u16 = int((pulse_width / 20) * 65535)  # デューティ比を65535スケールの16ビット値で算出
        duty_cycle = duty_u16 / 65535 * 100  # デューティ比を%で算出
        
        if verbose:
            print(f'角度: {deg}°, パルス幅: {pulse_width:.3f}ms, デューティ比(16bit):{duty_u16}, デューティ比: {duty_cycle:.2f}%')
    
        return duty_u16
    
    print(convert_deg2duty_u16(-45, verbose=True))
    print(convert_deg2duty_u16(0, verbose=True))
    print(convert_deg2duty_u16(45, verbose=True))
    [OUT]
    角度: -45°, パルス幅: 0.975ms, デューティ比(16bit):3194, デューティ比: 4.87%
    3194
    角度: 0°, パルス幅: 1.450ms, デューティ比(16bit):4751, デューティ比: 7.25%
    4751
    角度: 45°, パルス幅: 1.925ms, デューティ比(16bit):6307, デューティ比: 9.62%
    6307

    7-4.停止:PWM.deinit()

     PWM 出力の無効化は”PWM.deinit()”を使用します。

    7-5.コラム:PWM出力によるサーボモーター制御

     下記記事で実施したサーボモーター(SG90)をPWM出力で制御しました。計算式や各機器の詳細仕様は記事をご確認ください。

     下記設計思想でコード作成し、SG90の制御を確認できました。

    1. PWM信号をだすGPIO番号を指定してPINオブジェクトを作成

    2. machine.PWM()でPWMオブジェクトを初期化

    3. 動かしたい角度からデューティ比を、0〜65535 の範囲の符号なし16ビット値として計算する関数作成(詳細は下図参照)

    4. 繰り返し動作実装

    5. 修了用の動作実装

    画像
    [IN]
    from machine import PWM 
    from machine import Pin
    import time
    
    #初期設定
    pin15 = Pin(15, Pin.OUT) #GPIO15を出力に設定
    set_freq = 50 #設定周波数[Hz]
    pwm = PWM(pin15, freq=set_freq) #PWMオブジェクトを作成
    
    pwm.duty_u16(0) #デューティ比0%に設定
    print(f'周波数:{pwm.freq()}[Hz]')
    print(f'デューティ比:{pwm.duty_u16()}[%]')
    
    # 動作角度からデューティ比を算出する関数
    def convert_deg2duty_u16(deg, verbose=False):
        max_pulse = 2.4  # 90°時のパルス幅(ms)
        zero_pulse = 1.45  # 0°時のパルス幅(ms)
        min_pulse = 0.5  # -90°時のパルス幅(ms)
    
        # 角度からパルス幅を計算
        pulse_width = (max_pulse - min_pulse) / 180 * deg + zero_pulse
        duty_u16 = int((pulse_width / 20) * 65535)  # デューティ比を65535スケールの16ビット値で算出
        duty_cycle = duty_u16 / 65535 * 100  # デューティ比を%で算出
        
        if verbose:
            print(f'角度: {deg}°, パルス幅: {pulse_width:.3f}ms, デューティ比(16bit):{duty_u16}, デューティ比: {duty_cycle:.2f}%')
    
        return duty_u16
    
    print(convert_deg2duty_u16(-45, verbose=True))
    print(convert_deg2duty_u16(0, verbose=True))
    print(convert_deg2duty_u16(45, verbose=True))
    
    # サーボモーター制御:0°->45°->0°->-45°
    try:
        while True:
            duty_u16 = convert_deg2duty_u16(0, verbose=True)
            pwm.duty_u16(duty_u16)
            time.sleep(1)
            duty_u16 = convert_deg2duty_u16(45, verbose=True)
            pwm.duty_u16(duty_u16)
            time.sleep(1)
            duty_u16 = convert_deg2duty_u16(0, verbose=True)
            pwm.duty_u16(duty_u16)
            time.sleep(1)
            duty_u16 = convert_deg2duty_u16(-45, verbose=True)
            pwm.duty_u16(duty_u16)
            time.sleep(1)
    
    except KeyboardInterrupt:
        pwm.deinit()  # PWM信号を停止し、ピンの設定を初期化
    [OUT]
    周波数:50[Hz]
    デューティ比:0[%]
    角度: -45°, パルス幅: 0.975ms, デューティ比(16bit):3194, デューティ比: 4.87%
    3194
    角度: 0°, パルス幅: 1.450ms, デューティ比(16bit):4751, デューティ比: 7.25%
    4751
    角度: 45°, パルス幅: 1.925ms, デューティ比(16bit):6307, デューティ比: 9.62%
    6307
    
    角度: 0°, パルス幅: 1.450ms, デューティ比(16bit):4751, デューティ比: 7.25%
    角度: 45°, パルス幅: 1.925ms, デューティ比(16bit):6307, デューティ比: 9.62%
    角度: 0°, パルス幅: 1.450ms, デューティ比(16bit):4751, デューティ比: 7.25%
    角度: -45°, パルス幅: 0.975ms, デューティ比(16bit):3194, デューティ比: 4.87%
    角度: 0°, パルス幅: 1.450ms, デューティ比(16bit):4751, デューティ比: 7.25%
    角度: 45°, パルス幅: 1.925ms, デューティ比(16bit):6307, デューティ比: 9.62%
    画像

     LEDをPWM出力で制御している記事もあるのでご参考までに。

    8.シリアル通信:UART

     UART は標準の UART/USART 二重シリアル通信プロトコルを実装しており、物理レベルでは RX と TX の2線で構成されています。通信の単位は 8 または 9 ビット幅の文字です。
     UART オブジェクトは以下をように作成・初期化できます。(詳細は追って)

    [IN]
    from machine import UART
    
    uart = UART(1, 9600)                         # 与えたボーレートで初期化
    uart.init(9600, bits=8, parity=None, stop=1) # 与えたパラメータで初期化

    9.シリアル通信:SPI

     SPI はコントローラによって駆動される同期シリアルプロあり、物理レベルにおいて、バスは SCK、MOSI、MISO 3本の線で構成されています。
     複数のデバイスが同じバスを共有でき、各デバイスには通信を行うバス上の特定のデバイスを選択するための個別の4番目の信号 CS (Chip Select)が必要です。CSシグナルの管理はユーザーコードで(machine.Pin クラス経由で)行う必要があります。

     SPI実装は下記クラスを使用します。

    1. ハードウェア SPI :machine.SPI クラス

      • システムの基盤ハードウェアサポートを使って読み書き

      • 効率的で高速だが、利用できるピンに制限のある場合がある

    2. ソフトウェア SPI :machine.SoftSPIあ

      • ビットバンギングによって実装されており、どのピンでも利用可

      • 効率的ではない

     使用例は下記の通りです。(詳細は追って)

    [IN]
    from machine import SPI, Pin
    
    spi = SPI(0, baudrate=400000)           # 周波数 400kHz で SPI ペリフェラル 0 を作成
                                            # ユースケースによっては、追加のパラメータが必要な場合があります。使用するバスの
                                            # 特性やピンを選択するための追加のパラメータが必要になる場合があります。
    cs = Pin(4, mode=Pin.OUT, value=1)      # ピン 4 でチップセレクトを作成。
    
    try:
        cs(0)                               # ペリフェラルを選択。
        spi.write(b"12345678")            # 8 バイトを書き出し、受信データについては無視。
    finally:
        cs(1)                               # ペリフェラルを選択解除。
    
    try:
        cs(0)                               # ペリフェラルを選択。
        rxdata = spi.read(8, 0x42)          # 0x42 をバイトごとに書き出しながら、合計 8 バイトを読み込む。
    finally:
        cs(1)                               # ペリフェラルを選択解除。
    
    rxdata = bytearray(8)
    try:
        cs(0)                               # ペリフェラルを選択。
        spi.readinto(rxdata, 0x42)          # 0x42 をバイトごとに書き出しながら、合計 8 バイトを指定場所に読み込む。
    finally:
        cs(1)                               # ペリフェラルを選択解除。
    
    txdata = b"12345678"
    rxdata = bytearray(len(txdata))
    try:
        cs(0)                               # ペリフェラルを選択。
        spi.write_readinto(txdata, rxdata)  # バイト列の書出しと読込みを同時に行う。
    finally:
        cs(1)                               # ペリフェラルを選択解除。

    9-1.SPIの設定:machine.SPI()

     SPIはハードウェアとソフトウェアの2種があります。

    【ハードウェアSPIオブジェクト】
     
    以下パラメータを使用して、 I2C オブジェクトを作成します。

    [API]
    classmachine.SPI(id, ...)
    1. id:指定したバスで SPI オブジェクトを構築

      • id に指定できる値はポートとそのハードウェアに依存

      • 値 0, 1 などは一般的に、ハードウェア SPI ブロック #0 , #1 などを選択するために使われる

    2. baudrate:SCK のクロックレート

      • ハードウェア SPIの場合、クロック周波数は装置側のハードウェアに依存するため指定値より低くなる可能性あり

      • 実際の速度はSPIオブジェクトをprintで出力することで確認可能

    3. polarity:アイドリング状態のときのクロックのレベルを指定(選択は0か1 )

    4. phase:選択肢は0 か 1▶1番目または2番目のクロックエッジでのデータ読取りを指定

    5. bits:各転送のビット幅

      • 全ハードウェアでサポートされることが保証されているのは 8 のみ

    6. firstbit:SPI.MSB か SPI.LSB

    7. sck, mosi, miso*:バス信号に使用するピン(machine.Pin)オブジェクト

      • ほとんどのハードウェア SPI ブロック(コンストラクタのパラメータ id で指定)では、ピンが固定されていて変更できない。場合によっては、ハードウェアブロックが、1つのハードウェアSPIブロックに対して2~3の代替ピンセットを許している

      • 任意のピン割り当ては、ビットバンギング SPI ドライバー(id = -1)に対してのみ可能

    8. pins:WiPy ポートには sck, mosi, miso 引数がありません。その代わり pins パラメータでタプルとしてそれらを指定できます。

    【ソフトウェアSPIオブジェクト】
     
    以下パラメータを使用して、 SPIオブジェクトを作成します。
     パラメータの説明は上記参照

    [API]
    class machine.SoftSPI(baudrate=500000, *, 
                            polarity=0, 
                            phase=0, 
                            bits=8, 
                            firstbit=MSB, 
                            sck=None, 
                            mosi=None, 
                            miso=None)

    9-2.SPIの初期化/停止:SPI.init()/deinit()

     一般的なメソッドとしてSPIバスの初期化および停止があります。

    • SPI.init():SPIバスの初期化

      • パラメータの説明はハードウェアSPIを参照

    • SPI.deinit():SPI バスをオフ

    [API]
    SPI.init(baudrate=1000000, 
             *, 
             polarity=0, 
             phase=0, 
             bits=8, 
             firstbit=SPI.MSB, 
             sck=None, 
             mosi=None, 
             miso=None, 
             pins=(SCK, MOSI, MISO))

    9-3.データの読み込み:read/readinto

     シリアル通信(SPI)で得られたデータを読みます。バイト数を読む場合はread()、指定したバッファに読み込む場合はreadinto()を使用します。

    [API]
    SPI.read(nbytes, write=0x00)
    [API]
    SPI.readinto(buf, write=0x00)

    9-3.データの書き込み:write/writeinto

     シリアル通信(SPI)でデータを書き込みます。
     buf に含まれる bytes 型オブジェクトを書き込むならwrite()を使用します。
     read_buf に読込みながら write_buf の bytes 型オブジェクトを書き込むならwriteinto()を使用します。両バッファは同じでも異なっていてもかまいませんが、同じ長さでなければなりません。

    [API]
    SPI.write(buf)
    [API]
    SPI.write_readinto(write_buf, read_buf)

    9-4.定数

     定数を取得/設定する場合に下記を使用します。

    • SPI.CONTROLLER:SPI バスをコントローラに初期化するためのものです。これは WiPy のみで使います。

    • SPI.MSB/SoftSPI.MSB:最初のビットを最上位ビットに設定します

    • SPI.LSB/SoftSPI.LSB:最初のビットを最下位ビットに設定します

    10.シリアル通信:I2C

     I2C はデバイス間通信のための2線式プロトコルであり、物理レベルでは、クロックラインである SCL とデータラインである SDA の2本のワイヤで構成されています。
     I2Cは最大2個まで取得でき、ピン端子は比較的どこからでもいけます。本章では温度センサーADT7410を利用しながら使用方法を説明します。

     I2C オブジェクトは特定のバスに接続して作成されます。作成時に初期化することも、後で初期化することもできます。実装は下記の通りです。
     なお、注釈記載の通り動作にはプルアップ回路が必要となります。

    1. ハードウェア I2C:machine.I2C クラス

      • システムの基盤ハードウェアサポートを使って読み書き

      • 効率的で高速だが、利用できるピンに制限のある場合がある

    2. ソフトウェア I2C:machine.SoftI2C クラス

      • ビットバンギングによって実装されており、どのピンでも利用可

      • 効率的ではない

    画像

     次節以降では下図の通り温度センサーのADT7410(I2Cデバイス)を繋いだ状態で実行していきます。

    画像
    https://note.com/kiyo_ai_note/n/n2785cf43be68

    【コラム:指定するGPIOピンに関して】
     
    下図より基本的にはどこからでもいけるはずですが、後述の温度センサー試験時においてPIN0, PIN1以外の場所は「Bad SCL pin」エラーが発生しました。初学者はまずはPIN0, PIN1で試すのが安牌だと思います。

    画像
    画像

    10ー1.I2Cの設定:machine.I2C()

     I2Cはハードウェアとソフトウェアの2種があります。

    【ハードウェア I2C オブジェクト】
     
    以下パラメータを使用して、 I2C オブジェクトを作成します。

    [API]
    classmachine.I2C(id, *, scl, sda, freq=400000, timeout=50000)
    • id:特定の I2C ペリフェラルを識別します。指定できる値はポートやボードに依存します

    • scl:SCL に使用するピン▶ピンオブジェクトで指定

    • sda:SDA に使用するピン▶ピンオブジェクトで指定

    • freq:SCL の最大周波数を設定(整数)

    • timeout:マイクロ秒単位の最大時間で、I2C のトランザクションに対応(一部のポートでは使えない)

    【ソフトウェア I2C オブジェクト】
     
    以下パラメータを使用して、 I2C オブジェクトを作成します。

    [API]
    classmachine.SoftI2C(scl, sda, *, freq=400000, timeout=50000)
    • scl:SCL に使用するピン▶ピンオブジェクトで指定

    • sda:SDA に使用するピン▶ピンオブジェクトで指定

    • freq:SCL の最大周波数を設定(整数)

    • timeout:クロックストレッチ(clock stretching: (バス上の相手のデバイスが SCL を LOW にしている状態)を待つ最大時間(マイクロ秒)です。この時間を経過すると OSError(ETIMEDOUT) 例外が発生します。

    10ー2.I2Cの初期化/停止:I2C.init()/deinit()

     一般的なメソッドとしてI2Cバスの初期化および停止があります。

    • I2C.init():I2Cバスの初期化

      • APIは下記の通り(freq は SCL のクロックレート)

    • I2C.deinit():I2C バスをオフ

      • WiPyしか使えないかもしれないです。

    [API]
    I2C.init(scl, sda, *, freq=400000)

    10ー3.アドレスの取得:I2C.scan()

     I2Cアドレス(0x08 ~0x77まで)をスキャンし、応答したもののリストとして返します。デバイスは、アドレス(書き込みビットを含む)がバスに送信された後に SDA ラインをローに引き下げると応答します。

     下記に実行結果を示します。I2Cアドレスは10進数で得られるため16進数に変換するにはhex()関数を使用します。

    [IN]
    from machine import I2C, Pin
    import time
    
    pin_SDA = Pin(0)
    pin_SCL = Pin(1)
    
    #I2Cインスタンスの初期化
    i2c = I2C(0, scl=pin_SCL, sda=pin_SDA)
    
    #I2Cデバイスのスキャン
    devices = i2c.scan()
    devices_hex = [hex(i) for i in devices] #10進数を16進数に変換
    print(devices)
    print(devices_hex)
    [OUT]
    [72]
    ['0x48']

    10ー4.標準バスオペレーション

     下記で特定のデバイスをターゲットとした標準 I2C コントローラの読取り/書込み操作ができます。

    1. I2C.readfrom(addr, nbytes, stop=True, /):addr で指定されたペリフェラルから nbytes 分を読み取る

    2. I2C.readfrom_into(addr, buf, stop=True, /):addr で指定されたペリフェラルから buf に読み込む

    3. I2C.writeto(addr, buf, stop=True, /):buf から addr で指定されたペリフェラルに bytes 型オブジェクトを書き込む

    4. I2C.writevto(addr, vector, stop=True, /):vector に指定した bytes 型オブジェクトを addr に指定したペリフェラルに書き込む

    10ー5.メモリ操作

     一部の I2C デバイスは、読み書き可能なメモリデバイス(またはレジスタセット)として機能します。この場合、I2C トランザクションに関連付けられた2つのアドレス(ペリフェラルアドレスとメモリアドレス)があり、下記メソッドで通信できます。

    1. I2C.readfrom_mem(addr, memaddr, nbytes, *, addrsize=8):

      • addr で指定したペリフェラルより、 memaddr で指定したメモリアドレスから nbytes 分を読み込む

    2. I2C.readfrom_mem_into(addr, memaddr, buf, *, addrsize=8):

      • addr で指定したペリフェラルより、 memaddr で指定したメモリアドレスから buf に読み込む

    3. I2C.writeto_mem(addr, memaddr, buf, *, addrsize=8):

      • addr で指定したペリフェラルに、 memaddr で指定したメモリアドレスへ buf を書き込む

    【実習:ADT7410温度センサーでI2C.readfrom_mem】
     温度センサー:ADT7410から温度データを取得します。ADT7410の仕様は下記の通りです。

    1. I2Cバスアドレス:0x48

    2. (温度データを読み取る)レジスタのアドレス:0x00

    3. ADT7410は通信により2byteのデータが得られる

    [IN]
    from machine import I2C, Pin
    import time
    
    i2c = I2C(id=0, scl=Pin(1), sda=Pin(0)) #I2Cインスタンスの初期化
    
    #ADT7410から温度データを読み込む
    data = i2c.readfrom_mem(0x48, 0x00, 2)
    #結果表示
    print(type(data))
    print(data)
    print(len(data))
    print(f'block:{[data[0], data[1]]}') 
    [OUT]
    <class 'bytes'>
    b'\x0e\x18'
    2
    block:[14, 24]

     結果から下記が確認できます。

    1. メモリ(0x00)から(指定通り)2bytesのデータ("\x0e"と"\x18")を取得

      • \x0e:16進数だと0E、10進数だと14

      • \x18:16進数だと18、10進数だと24

    2. byteデータもリストのように抽出でき、1個だと整数として表示される

    10ー6.コラム:温度センサー(ADT7410)でI2C

     下記で実施した温度センサー(ADT7410)とI2C通信を使用した温度データ取得をPico×Micropythonで実施しました。機器や通信の詳細は下記記事をご確認ください。

     設計思想は下記の通りです。結果として温度データが得られました。

    1. I2Cアドレスや温度センサのレジスタアドレスは仕様書から参照

    2. machineのI2Cクラスを用いてI2C通信で温度センサから情報を取得

    3. 得られたデジタル出力(ADC_code)は仕様書記載の計算式で温度に変換

    温度=ADCCode(dec)16=0.0625×ADCCode(T≥0) 温度=\frac{ADC Code(dec)}{16} = 0.0625\times ADC Code (T\geq0) 

    温度=ADCCode(dec)−819216  (T<0)温度=\frac{ADC Code(dec)-8192}{16}   (T<0)

    [IN]
    from machine import I2C, Pin
    import time
    
    # I2C初期化(SDAとSCLピンを設定)
    i2c = I2C(0, scl=Pin(1), sda=Pin(0))
    
    #条件設定
    address = 0x48 # ADT7410のI2Cアドレス
    register = 0x00 # 温度レジスタのアドレス
    num_bytes = 2 #読み込むバイト数: 2バイト
    
    #バイナリの動きを可視化
    def format_binary(num, bits=8):
        return f'{num:0{bits}b}'
    
    #ADT7410から温度データを読み込む関数
    def read_temperature(verbose=False):
        data = i2c.readfrom_mem(address, register, num_bytes) #2bytesのデータをByte型で取得
        # データの結合と13ビットにシフト※抽出時はdataは整数
        temp_raw = (data[0] << 8 | data[1]) >> 3
        # 温度計算
        if temp_raw & 0x1000:  # MSBが1の場合は負の温度
            temp_raw -= 8192
        temperature = temp_raw * 0.0625
        
        #可視化用
        if verbose:
            print(f'data: {data}, temp_raw: {temp_raw}, temp: {temperature}')
            print(f'data0: {format_binary(data[0])} , data1: {format_binary(data[1])}')
            print(f'data0 <<8: {format_binary(data[0] <<8)}')
            print(f'data0 <<8 | data1: {format_binary(data[0] <<8 | data[1])}')
            print(f'(data[0] <<8 | data[1]) >>3: {format_binary((data[0] <<8 | data[1]) >>3)}', end='\n\n')
            
        return temperature
    
    try:
        temp = read_temperature(verbose=False)
        while True:
            print(f'Temperature: {temp:.2f} °C')
            temp = read_temperature(verbose=True)
            time.sleep(1)
    except KeyboardInterrupt:
        print('Measurement stopped')
    [OUT]
    Temperature: 24.63 °C
    data: b'\x0cP', temp_raw: 394, temp: 24.625
    data0: 00001100 , data1: 01010000
    data0 <<8: 110000000000
    data0 <<8 | data1: 110001010000
    (data[0] <<8 | data[1]) >>3: 110001010
    
    Temperature: 24.63 °C
    data: b'\x0cH', temp_raw: 393, temp: 24.5625
    data0: 00001100 , data1: 01001000
    data0 <<8: 110000000000
    data0 <<8 | data1: 110001001000
    (data[0] <<8 | data[1]) >>3: 110001001
    
    Temperature: 24.56 °C
    data: b'\x0cP', temp_raw: 394, temp: 24.625
    data0: 00001100 , data1: 01010000
    data0 <<8: 110000000000
    data0 <<8 | data1: 110001010000
    (data[0] <<8 | data[1]) >>3: 110001010

    11.1-wire

     1-ware バスは(グランドと電源用の線に加えて)通信用に単線を使用するシリアルバスです。本機能は「ESP8266用 MicroPythonチュートリアル」に記載があるため、おそらくPicoでは利用できないのでご参考までに。

    12.RTC/タイマー

     Raspberry Pi PicoはRP2040内にRTC(リアルタイム・クロック)を内蔵しています。RTCとは時計機能であり主電源がオフの間も時間を計測できます。単純に時刻を知らせる時計機能だけでなく、ファイルの更新時のタイムスタンプ、ログデータの時間管理、など常に高精度な時間管理がシステム上でなされています。

    画像
    https://qiita.com/totuto/items/d3bc435d3d2f545c1c02

     MicroPythonでは下記2つの機能を用いて日時情報やタイマー機能を利用できます。

    • クラス RTC -- リアルタイムクロック:machine.RTC()

    • クラス Timer -- ハードウェアタイマーの制御:machine.Timer()

    12-1.リアルタイムクロック(RTC)

     RTCオブジェクトは”machine.RTC(id=0, ...)”で作成し、初期化は"RTC.init(datetime)"です。

     RTC の日付と時刻を取得または設定は"RTC.datetime([datetimetuple])"を使用します。引数がない場合、このメソッドは現在の日時を含む8項目のタプルを返します。
    (year, month, day, weekday, hours, minutes, seconds, subseconds)

    [IN]
    from machine import RTC
    
    rtc = RTC()
    print(rtc.datetime())
    
    [OUT]
    (2024, 3, 31, 6, 20, 41, 48, 0)

    12-2.タイマー

     ハードウェアタイマーは周期処理や時間イベントを扱います。MicroPython の Timer クラスは、指定した周期で(または少し経過後に1回だけ)コールバックを実行するベースライン操作を定義します(一部制約あり)。

    画像

     ”machine.Timer()”でオブジェクトを作成し、"Timer.init()"で初期化します。init()のAPI/引数は下記の通りです。
     なお、タイマーの解放は"Timer.deinit()"です。

    [API]
    Timer.init(*, 
               mode=Timer.PERIODIC, 
               freq=-1, 
               period=-1, 
               callback=None)
    1. mode:次のいずれかを選択のこと

      • Timer.ONE_SHOT:設定したチャネル周期が経過した後に1回だけ実行

      • Timer.PERIODIC:チャネルの設定した周期で定期的に実行

    2. freq:タイマーの周波数(単位:Hz)。周波数の上限はポートに依存する。

      • freq と period の両方の引数を与えた場合、 freq の方を優先し、 period は無視される

    3. period:タイマー期間(ミリ秒単位)

    4. callback:タイマー期間終了時に呼び出されるコールバック

      • コールバックは1つの引数を取らなければならず、その引数にはタイマーオブジェクトが渡される

      • コールバックの引数は指定が必須。無いとタイマー満了時に次の例外が発生します: TypeError: 'NoneType' object isn't callable

     定期実行と1回だけ実行のサンプルは下記の通りです。

    [IN]
    from machine import Timer
    
    def test_callback(t):
        print('実行')
    
    timer = Timer()
    #周期:0.5秒(500ms)で定期的に実行
    timer.init(period=500, 
               mode=Timer.PERIODIC, 
               callback=test_callback)
    画像
    [IN]
    from machine import Timer
    
    def test_callback(t):
        print('実行')
    
    timer = Timer()
    #周期:1.0秒(1000ms)に1回実行
    timer.init(period=1000, 
               mode=Timer.ONE_SHOT,
               callback=test_callback)
    画像

    13.ADC

     ADC(Analog to Digital Converter:アナログ-デジタルコンバーター)とはアナログ信号をデジタル信号に変換する機器です。

     Raspberry Pi Picoにおいては、通常のPinだと0/1(ON/OFF)でしか取れなかった値を「0~3.3Vの電圧値を0-65535の整数値として取得」できます。
     ADC クラス()はADCへのインターフェイスを提供し、ADC サンプリングの追加制御はクラス ADCBlockが対応します。

    13-1.ADCの設定:machine.ADC()

     ADCオブジェクトの作成は"machine.ADC()"を使用します。APIは下記の通りです。

    [API]
    machine.ADC(id, *, sample_ns, atten)
    • id:一般的にはPinオブジェクトを指定

    • sample_ns :ナノ秒単位のサンプリング時間

    • atten:入力減衰率を指定

     ADCオブジェクトの設定変更は”ADC.init()”を使用します。

    [API]
    ADC.init(*, sample_ns, atten)¶

    13-2.アナログ値の読み込み:read_u16/read_uv

     ADCでアナログ値の読み込みは下記2種があります。

    1. ADC.read_u16():読み込んだアナログ値を0-65535 の範囲の整数で返す

      • 最大値は16bitより216−1=65536−1=655352^{16}-1=65536-1=65535

      • 分解能が16bitでないADCも0-65535としてスケーリングされ出力する

    2. ADC.read_uv():アナログ値をマイクロボルト単位で整数値を返す

      • 値の校正の有無などは特定のポートに任されている

     13-2-1.PicoのADCとread_u16の関係

     ADCを用いて電圧を計算する場合は下記式を使用します。

    電圧[V]=(電圧範囲の最大値×デジタル値)ADコンバーターの分解能電圧[V]=\frac{(電圧範囲の最大値\times デジタル値)}{ADコンバーターの分解能}

     PicoのADCは0~3.3V電圧を12bit(212=40962^{12}=4096)のデジタル値(0~4095)として出力されます。つまり値が1変化するごとに0.8mV(3.34096×1000=0.806\frac{3.3}{4096}\times 1000=0.806)変化します。よって通常であれば下記計算式となります。

    電圧[V]=(電圧範囲の最大値×デジタル値)ADコンバーターの分解能=3.3×デジタル値(12bit)4096\begin{aligned} 電圧[V]&=\frac{(電圧範囲の最大値\times デジタル値)}{ADコンバーターの分解能}\\ &=\frac{3.3\times デジタル値(12bit)}{4096} \end{aligned}

     しかしADC.read_u16()は0-65535として出力されます。分解能はADC側に依存するため、実際の計算式は下記の通りです(値が16変化するごとに0.8mVが変化)。

    電圧[V]=3.3×デジタル値(16bit)65536電圧[V]=\frac{3.3\times デジタル値(16bit)}{65536}

    画像

     13-2-2.ADC.read_u16()

     ADC.read_u16()で”電圧値をデジタル値として取得”してみます。状態としてADCに”何もしない状態(左図)”と"3.3Vをかけた状態(右図)"で実行しました。

    画像

     結果として無負荷と負荷状態を数値として取得することが出来ました。

    [IN]
    from machine import Pin, ADC
    
    adc = ADC(Pin(26)) #GP26pinのADコンバータ設定 
    val_raw = adc.read_u16() #ADコンバータの値を取得 (0~65535)
    val_volt = val_raw/65535 * 3.3 #電圧 [V]に変換(0~3.3V)
    print(f'16bit: {val_raw}, Voltage: {val_volt:.2f}V')
    [OUT]
    16bit: 7745, Voltage: 0.39V
    16bit: 65087, Voltage: 3.28V

     参考として下記コード実行中に3.3V電源を共有し変化を確認しました。値が振れているのはピンそのものの電圧の振れやADCの変換抵抗など様々な要因があると思われます。

    [IN]
    from machine import Pin, ADC
    import time
    
    adc = ADC(Pin(26)) #GP26pinのADコンバータ設定 
    
    while True:
        try:
            val_raw = adc.read_u16() #ADコンバータの値を取得 (0~65535)
            val_volt = val_raw/65535 * 3.3 #電圧 [V]に変換(0~3.3V)
            print(f'16bit: {val_raw}, Voltage: {val_volt:.2f}V')
            time.sleep(1.0) 
        except KeyboardInterrupt:
            break
    
    [OUT]
    16bit: 7745, Voltage: 0.39V
    16bit: 6545, Voltage: 0.33V
    16bit: 6417, Voltage: 0.32V
    16bit: 65535, Voltage: 3.30V
    16bit: 65535, Voltage: 3.30V
    16bit: 65087, Voltage: 3.28V
    16bit: 65535, Voltage: 3.30V
    16bit: 65535, Voltage: 3.30V
    16bit: 65535, Voltage: 3.30V
    16bit: 65359, Voltage: 3.29V
    画像

    14.ネットワーク

     追って



    参考資料

    あとがき

     まずはやりたいこと調べるほうが先だったかもしれないけど、取り急ぎ。ADCはおそらくすぐに使うことになると思う。

    変更履歴

    2024年3月31日:初版発行
    2024年4月7日:ADC追加

     
     

    KIYO

     
     
    普段は製造業で企画/開発/設計しております。記事はプログラミング・機械学習、IoT関係の記事をメインで作成し、なるべく1つの記事で知りたいことを網羅していきます。内容は学術的より実装・アウトプット(ほしくなるもの)を重視して作成しています。 面白そうな仕事があればやりたいです!

    あなたへのおすすめ