UMEHOSHI ITA TOP PAGE

[Raspberry Pi 3 Model A+]と[UMEHOSHI ITA]を乗せたモータ付き台車を使った実験

このページで示したロボットの倒立振子の検討の記録の続きのページです。
なお、EEPROMの内容は、このページで作成した内容と同じですが、その処理は使っていません。
(別途のRAMにSPI受信処理を埋め込む方式で検討しています)

ロボットの倒立振子の検討の記録 その3B

ここで使用するロボットは二輪車と、別途の一つの支柱が接地する構造で、電源を切っても倒れない構造になっている。
この状態から、支柱浮かせて二輪だけで動かす目標の検討です。

この 動作動画(Xのリンク) で示したように振動を止めることができませんでした。
この制御では6種類のPWM値に変更するためのエントリーポイントをUMEHOSHI ITA基板内に用意して、それをUSB利用の呼び出しで変更していました。
USBではレイテンシ(Latency:データの往復にかかる遅延時間(遅さ))や、ジッタ(Jitter:通信中にその遅延時間が変動する揺らぎ(ばらつき))が 大きいので、モーターのPWM制御には向かないということです。
このような場合、 この問題を回避するため、PWMの制御にUSBを使わずに別回線でコントロールします。
SPI(Serial Peripheral Interface:4本の信号線「SCLK, MOSI, MISO, CS」を使用した数Kbps〜数10Mbpsの高速通信が可能な全二重同期シリアル通信規格)の 使用を試みることにした。
SPIは、送信と受信が同時に行われる通信方式です。
クロック1回で、Masterからの1bit送信と、Slaveからの 1bit送信を 同時に起きます。
送信だけや、受信だけという動作は基本的にできません。
ですが送信だけに見えるケースとして、Slaveが何もデータを出さない場合があります。
SPIでは、受信だけはできません。なぜならクロックは Masterが送信するときにしか発生しないからです。
そのため受信だけしたい場合、ダミーデータを送信する必要があります。
SPIでの Master / Slave の違い
項目MasterSlave
クロック 生成する(出力端子) 受け取る(受信端子)
CS制御 制御する(出力端子) 選択される(受信端子)
通信開始 できる できない
通信速度 決定する 従う
MOSI(Master Out Slave In) 送信 受信
MISO(Master In Slave Out) 受信 送信
Raspberry Pi 3 Model A+をマスターとして使います。まずはその通信の確認から始めています。
そして、 PIC32MXもRaspberry Piの標準SPI(SPI0 / GPIO 7-11)も、SPIのビットの送り方は ハードウェアレベルで MSBファースト(上位ビットから)固定 になっているようです。
(UART(シリアル通信)は一般的に「LSBファースト(下位ビットから)」で送信されるので、これと逆です。)
なお、MSB: Most Significant Bit(最上位ビット)LSB: Least Significant Bit(最下位ビット)SPI通信のハードウェア設定(Bit Order)で使われる言葉です。


Raspberry Pi 3 Model A+と「UMEHOSHI ITA」をSPIで通信するためのハード追加

まず、「UMEHOSHI ITA」側ですがCN10のコネクタを介した接続を試みます。
このコネクタはデフォルトでUART1の通信を行うために用意されているのですが、PIC32のペリフェラルピンセレクト (PPS: Peripheral Pin Select)で 用途をSPI用に変更して使う訳です。次のようにすればSPI通信(スレーブ)として使えると試みました。
PIC32MX270の18,12,26,17番ピンをPPSで、SS2,SDI2,SCLK2,SDO2に変更してSPI通信(スレーブ)にして、マスター側をRaspberry Piとして使います。
CN10
端子番号
PIC32MXの端子番号変更前のUART1変更後のSPI(スレーブ)拡張基板内
コネクタ番号
Raspberry ピン番号とSPI端子(マスター)
118 RB9RTS(出力)RB9を SS2 イネーブル入力端子にPPSで変更1Pin24 GPIO8 (CE0)
228  Vcc(3.3v)Vcc(3.3v)
327  GNDGND2GND
412 RA4RX(入力)RA4をSDI2(入力)にPPSで変更3Pin19 GPIO10 (MOSI)
511 RB4
TX(出力)
RB4を単なる入力端子に変更
SCLK2 (PIC32のRB15をPPSで変更した26番ピンにジャンパー接続
4Pin23 GPIO11 (SCLK)
617 RB8CTS(入力) RB8をSDO2(出力)にPPSで変更 5Pin21 GPIO9 (MISO)
既存のUART用コネクタを経由させていますが、PIC32MX270F256B-50I/SPのSPI2用クロックの「26番ピン (RB15)」はコネクタCN-10のピン5にジャンパー接続する。

[Raspberry Pi 3 Model A+]と[UMEHOSHI ITA]を乗せたモータ付き台車で、
拡張ボードでSPI用のコネクタを追加した回路結線図&配置図

Raspberry Pi 3 Model A+側のSPIマスターで通信するための動作確認コード

まず、SPIが有効化されているか確認し、そうでなければ次のように有効します。
sudo raspi-config
Interface Options → SPI → Enable
そして、次のコマンドで
suzuki@raspberrypi:~ $ ls /dev/spidev*
/dev/spidev0.0  /dev/spidev0.1
suzuki@raspberrypi:~ $
suzuki@raspberrypi:~ $ lsmod | grep spi
spidev                 20480  0
spi_bcm2835            20480  0
suzuki@raspberrypi:~ $
/dev/spidev0.0と /dev/spidev0.1は、それぞれが SPIバス0のCE0がGPIO8(Pin24)で使えることと、CE1のGPIO7(Pin26)が使えることを示している。
lsmod は Linuxで現在ロードされているカーネルモジュール(デバイスドライバ)を表示するコマンドで、これでspi関連ドライバがあるか確認している。

以下は、
RaspberryPi3のPin24 GPIO8 (CE0)、Pin19 GPIO10 (MOSI)、Pin21 GPIO9 (MISO)、Pin23 GPIO11 (SCLK)として利用し、 SPIマスターの動作で、Enterですしずつ確認しながら送でするのシンプルなpythonコード(spimas.py)です。
import spidev
import time

spi = spidev.SpiDev() # SPI操作オブジェクト生成
spi.open(0, 0)        # bus=0 device=0 (CE0) でSPIオープン

spi.max_speed_hz = 7812500 # 7.8 MHzSPIクロック
spi.mode = 0 # クロック信号(SCLK)はLow待機、LowからHighに立ち上がる瞬間でデータを読み取り

v=0 # 送信データ(0,1,2・・・・と増やした値を Enterキー操作で少しずつ送る)
while True:
   send_data = [v] #  例えばビッグエンディアンで0x15AFを送るなら[0x15, 0xAF]のリストで送る。決まってないが一般の受信側のオーダーで送る。
   input("Enter>") # Enterで送信へ進む
   print("Send :", send_data)
   recv_data = spi.xfer2(send_data)# 送受信
   # print("Send :", send_data) # (send_dataの内容は、受信データ置き換わる挙動もある)
   print("Recv :", recv_data) 
   time.sleep(0.1)
   v += 1 # 送信データの後進



Raspberry Pi 3 Model A+と「UMEHOSHI ITA」をSPIで通信する

PIC32MX270側のSPIスレーブ確認コード(ポーリング版)

前述のRaspberry Pi 3のPythonのSPIマスターコードに対するPIC32MX270側のSPIスレーブ用のポーリング用コードです。
#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include <proc/p32mx270f256b.h>
#include <sys/attribs.h>//割込み関係の定義

int spi_out_v = 0x0ff; // SPI出力用データ

/*
 PIC32MX270F256B-50I/SP (SP は 28pin SPDIP パッケージ)のUART1を、
 18番ピンをRTS(出力)、12ピンをRX(入力)、11ピンをTX(出力)、17ピンRB8をCTS(入力)
 として使っているが、 これを、次のSPI2のスレーブモードに設定し直す
 18番ピンをCS GPIO入力、12ピンをSDI2(入力)、11ピンRB5をSDO2(出力)、17ピンをデジタル入力
 そのためのPPSは「ピン配線設定」関数。つまり **「配線設定」**です。
 */
void PPS_Init_SPI2_Slave(void)
{   
    /* ---------- UART1のフロー制御(CTS/RTS)を解除 ---------- */
    U1MODEbits.UEN = 0b00;  // UART1はTX/RXのみ使用(CTS/RTSピンを開放)
    U1MODEbits.ON = 0;      // UART1自体を使わない場合は停止

    ODCBbits.ODCB9 = 0; // RB9デフォルト設定のオープンドレイン無効
    CNPUBbits.CNPUB9 = 0;//CNPUB (Change Notice Pull-up B) レジスタの第9ビットを0
    //(これにより、RB9ピンの内部プルアップ抵抗を無効)
    CNPDBbits.CNPDB9 = 0;//CNPDB (Change Notice Pull-down B)レジスタの第9ビットを 0
    //(これにより、RB9ピンの内部プルダウン抵抗を無効) 
    LATBbits.LATB9 = 1; // RB9のRTS(出力)をHiにする
    TRISBbits.TRISB9 = 1;// RB9を入力ピンにする (基板のパターンが繋がっているため)
    
    LATBbits.LATB4 = 1;  // RB4を1にする。(必要ないかも?)
    TRISBbits.TRISB4 = 1; // RB4を入力用に指定
    ODCBbits.ODCB4 = 0; // デフォルト設定のオープンドレイン無効
    CNPUBbits.CNPUB4 = 0;//内部プルアップ抵抗を無効
    CNPDBbits.CNPDB4 = 0;//内部プルダウン抵抗を無効

    TRISAbits.TRISA4 = 1;   // RA4  (pin12) SDI2入力へ
    ANSELBbits.ANSB15 = 0;  // 26番ピンのRB15をデジタルモードに設定
    TRISBbits.TRISB15 = 1;  // スレーブの場合は入力、マスターなら0(出力)に設定

    TRISBbits.TRISB8 = 0; // RB8を出力用に指定
    CNPUBbits.CNPUB8 = 0;//内部プルアップ抵抗を無効

    /* ---------- PPS設定アンロック ---------- */
    SYSKEY = 0xAA996655;        // 書き込み保護解除キー1
    SYSKEY = 0x556699AA;        // 書き込み保護解除キー2
    CFGCONbits.IOLOCK = 0;      // PPSレジスタのロック解除

    /* ---------- 入力PPS設定 ---------- */
    SS2R  = 0b0100;             // SS2入力をRB9へ
    SDI2R = 0b0010;             // SDI2入力をRA4へ

    /* ---------- 出力PPS設定 ---------- */
    RPB8R = 0b0100; // SDO2へ
    asm("NOP");

    /* ---------- PPS再ロック ---------- */
    CFGCONbits.IOLOCK = 1;
    SYSKEY = 0;                 // 保護再有効
}

/*
 SPI2に関する周辺機能設定の初期設定
 * クロック極性、スレーブ設定、割り込みなど、ここだけ変更する可能性を考慮して
 * 「ピン配線設定」のPPS_Init_SPI2_Slaveを別関数にしている。 
 */
void Init_SPI2_Slave(void)
{
    _RB5 = 1; // D1 LED の初期点灯設定

    PPS_Init_SPI2_Slave(); // PIC32MX270F256B-50I/SP のSPI2スレーブ用端子にピン割り当てを変更

    SPI2CON = 0;                // SPI2制御レジスタ初期化
    SPI2STAT = 0;               // ステータス初期化

    /* ---------- SPIモード設定 ---------- */
    SPI2CONbits.MSTEN = 0;      // 0 = スレーブモード
    SPI2CONbits.SSEN  = 1;      // SS有効 Slave Select Enable (Slave mode) bit
    SPI2CONbits.MODE16 = 0;     // 8bit通信
    SPI2CONbits.MODE32 = 0;     // 32bit通信無効

    /* SPIモード0設定 (多くのマスターが使用) */
    SPI2CONbits.CKP = 0;        // クロックアイドルLow
    SPI2CONbits.CKE = 1;        // クロック立上りでデータ更新

    SPI2CONbits.SMP = 0;        // スレーブでは通常0

    SPI2BUF;                    // バッファをダミー読み出し
    SPI2STATbits.SPIROV = 0;    // 受信オーバーフロークリア

    // SPI用Enhanced Buffer(ENHBUF)を0(デフォルト)に指定(重要)
    SPI2CONbits.ENHBUF = 0;
    SPI2CONbits.SRXISEL = 1; //01受信バッファが1byteに指定
   
    SPI2CONbits.ON = 1;         // SPI2モジュール有効
    SPI2BUF = spi_out_v;// 最初の送信データのセット(重要)
}


void timer4() // SPIスレーブ のポーリング処理の確認用タイマー処理
{
	static int count  = 0;
	if(++count  % 1000 != 0) return;
	// count  が 1000の倍数時だけ(0.5秒ごと)に以下を実行
	
	_RB5 = ! _RB5;// このタイマーの動作確認用の D1 LED 点滅の出力を反転

	if (SPI2STATbits.SPIRBF == 1) { // SPI受信バッファが一杯になったら
		// ここを通るなら、SPI受信ができている(配線とSPI設定はOK)
		uint8_t rxData = SPI2BUF; // 受信データを読み出す(これでSPIRBFが0に戻る)
		_clear_beep_code();
		_debug_hex8(0, rxData , 1); // 受信データ確認
		spi_out_v-=1;
		while(SPI2STATbits.SPITBE == 0); // 送信バッファ空待ち
		SPI2BUF = spi_out_v;// 次の送信データのセット
	}
	if (SPI2STATbits.SPIROV == 1) { // エラーフラグが立っていないか
		SPI2STATbits.SPIROV = 0; // 一旦クリア
		_debug_hex16(0, SPI2BUF, 1);
	}
}

// UMEHOSHI ITA のRAMプログラムの設定希望プログラム(0x80005000番地より起動)
__attribute__((address( 0x80005000 ))) void start (void);
void start()
{
	_clear_beep_code(); // _debug_hex? 関数利用の初期化
	_UM_PTR_GOTO_BEEP = NULL; // debug_hex?の出力をループしない設定

	Init_SPI2_Slave();// SPI2の初期化

	// デフォルトで0.00005秒ごとに呼び出される関数に、SPIスレーブ のポーリング処理のtimer4を設定
	_HANDLES[_IDX_TIMER_4_FUNC] = timer4;
     	T4CONbits.ON = 1;// timer4割込みオン(SPIスレーブ のポーリングの起動)
	
 	_RB5 = 0; // D1 LED の消灯
	_debug_hex4(0,0x0f,1);//  _debug_hex? の動作確認用
	_send_string("start END\r\n");//起動関数終了の表示
}
上記はTimer4のインターバルタイマーの割り込みを使い、この繰り返しの中でSPIの受信を監視して、受信処理をする例です。

Raspberry Pi 3 Model A+と「UMEHOSHI ITA」をSPI割り込みで通信するためのソフト変更

PIC32MX270側のSPIスレーブ確認コード(割り込み版)

上記のTimer割り込みは使わずに、SPIの受信による割り込みです。
PIC32MX270側のSPI割り込み処理を作る場合、PIC32MX270側のfirmwareを追加、変更が必要になる。
今後の割り込み処理変更と、これまでの「UMEHOSHI ITA」基板利用コードに影響がないように、firmwareを変更する。
(このfirmware変更内容は、2026年5月移行に適用予定)
割り込み処理は、firmwareのコード(my_sys.c)に、次のコードを追加して実現している。
void __ISR(_SPI2_VECTOR, IPL3SOFT) SPI2_Handler(void)
{
    ((void (**)(void) )_PTR_HANDLERS)[_IDX_SPI2_FUNC]();
}
つまり、_PTR_HANDLERS)[_IDX_SPI2_FUNC]に、ユーザー作成の割り込み処理のポイントを記憶しておいて、それを実行させるコードです。
そのたために必要な次のマクロ定義を、common.hに追加しました。
#define _IDX_PPS_INIT_SPI2_SLAVE 70  // ★ SPI2のPPS 設定 PPS_Init_SPI2_Slave
#define _PPS_Init_SPI2_Slave()  (((void (**)(void) )_PTR_HANDLERS)[_IDX_PPS_INIT_SPI2_SLAVE]())
#define _IDX_INIT_SPI2_SLAVE 71 // ★ SPI2の初期設定 割り込みを含む初期化 Init_SPI2_Slave
#define _Init_SPI2_Slave()  (((void (**)(void) )_PTR_HANDLERS)[_IDX_INIT_SPI2_SLAVE]())
#define _IDX_SPI2_FUNC 72       // ★ SPI2割り込みで呼ぶ関数記憶用の添え字
そして、上記の_PTR_HANDLERS[_IDX_PPS_INIT_SPI2_SLAVE]に記憶する次のデフォルト関数を次のように定義しています。
(この記述用にmy_option.cのファイルを追加しました。 この「ピン配線設定」関数のvoid PPS_Init_SPI2_Slave(void)は、ポーリングで使った関数と同じです。)
/*
 PIC32MX270F256B-50I/SP (SP は 28pin SPDIP パッケージ)のUART1を、
 18番ピンをRTS(出力)、12ピンをRX(入力)、11ピンをTX(出力)、17ピンRB8をCTS(入力)
 として使っているが、 これを、次のSPI2のスレーブモードに設定し直す
 18番ピンをCS GPIO入力、12ピンをSDI2(入力)、11ピンRB5をSDO2(出力)、17ピンをデジタル入力
 そのためのPPSは「ピン配線設定」関数。つまり **「配線設定」**です。
 */
void PPS_Init_SPI2_Slave(void)
{   
    /* ---------- UART1のフロー制御(CTS/RTS)を解除 ---------- */
    U1MODEbits.UEN = 0b00;  // UART1はTX/RXのみ使用(CTS/RTSピンを開放)
    U1MODEbits.ON = 0;      // UART1自体を使わない場合は停止

    ODCBbits.ODCB9 = 0; // RB9デフォルト設定のオープンドレイン無効
    CNPUBbits.CNPUB9 = 0;//CNPUB (Change Notice Pull-up B) レジスタの第9ビットを0
    //(これにより、RB9ピンの内部プルアップ抵抗を無効)
    CNPDBbits.CNPDB9 = 0;//CNPDB (Change Notice Pull-down B)レジスタの第9ビットを 0
    //(これにより、RB9ピンの内部プルダウン抵抗を無効) 
    LATBbits.LATB9 = 1; // RB9のRTS(出力)をHiにする
    TRISBbits.TRISB9 = 1;// RB9を入力ピンにする (基板のパターンが繋がっているため)
    
    LATBbits.LATB4 = 1;  // RB4を1にする。(必要ないかも?)
    TRISBbits.TRISB4 = 1; // RB4を入力用に指定
    ODCBbits.ODCB4 = 0; // デフォルト設定のオープンドレイン無効
    CNPUBbits.CNPUB4 = 0;//内部プルアップ抵抗を無効
    CNPDBbits.CNPDB4 = 0;//内部プルダウン抵抗を無効

    TRISAbits.TRISA4 = 1;   // RA4  (pin12) SDI2入力へ
    ANSELBbits.ANSB15 = 0;  // 26番ピンのRB15をデジタルモードに設定
    TRISBbits.TRISB15 = 1;  // スレーブの場合は入力、マスターなら0(出力)に設定

    TRISBbits.TRISB8 = 0; // RB8を出力用に指定
    CNPUBbits.CNPUB8 = 0;//内部プルアップ抵抗を無効

    /* ---------- PPS設定アンロック ---------- */
    SYSKEY = 0xAA996655;        // 書き込み保護解除キー1
    SYSKEY = 0x556699AA;        // 書き込み保護解除キー2
    CFGCONbits.IOLOCK = 0;      // PPSレジスタのロック解除

    /* ---------- 入力PPS設定 ---------- */
    SS2R  = 0b0100;             // SS2入力をRB9へ
    SDI2R = 0b0010;             // SDI2入力をRA4へ

    /* ---------- 出力PPS設定 ---------- */
    RPB8R = 0b0100; // SDO2へ
    asm("NOP");

    /* ---------- PPS再ロック ---------- */
    CFGCONbits.IOLOCK = 1;
    SYSKEY = 0;                 // 保護再有効
}
同様で、my_option.cに_PTR_HANDLERS[__IDX_PPS_INIT_SPI2_SLAVE]に記憶する次のデフォルト関数を次のように定義しています。
/*
 SPI2に関する周辺機能設定の初期設定
 * クロック極性、スレーブ設定、割り込みなど、ここだけ変更する可能性を考慮して
 * 「ピン配線設定」のPPS_Init_SPI2_Slaveを別関数にしている。 
 */
void Init_SPI2_Slave(void)
{
    _RB5 = 1; // D1 LED の初期点灯設定

    PPS_Init_SPI2_Slave(); // PIC32MX270F256B-50I/SP のSPI2スレーブ用端子にピン割り当てを変更

    SPI2CON = 0;                // SPI2制御レジスタ初期化
    SPI2STAT = 0;               // ステータス初期化

    /* ---------- SPIモード設定 ---------- */
    SPI2CONbits.MSTEN = 0;      // 0 = スレーブモード
    SPI2CONbits.SSEN  = 1;      // SS有効 Slave Select Enable (Slave mode) bit
    SPI2CONbits.MODE16 = 0;     // 8bit通信
    SPI2CONbits.MODE32 = 0;     // 32bit通信無効

    /* SPIモード0設定 (多くのマスターが使用) */
    SPI2CONbits.CKP = 0;        // クロックアイドルLow
    SPI2CONbits.CKE = 1;        // クロック立上りでデータ更新

    SPI2CONbits.SMP = 0;        // スレーブでは通常0

    SPI2BUF;                    // バッファをダミー読み出し
    SPI2STATbits.SPIROV = 0;    // 受信オーバーフロークリア

    // SPI用Enhanced Buffer(ENHBUF)を0(デフォルト)に指定(重要)
    SPI2CONbits.ENHBUF = 0;
    SPI2CONbits.SRXISEL = 1; //01受信バッファが1byteに指定

    /* ---------- 割り込み設定 ---------- */
    IFS1bits.SPI2RXIF = 0;       // 受信割り込みフラグクリア
    IEC1bits.SPI2RXIE = 1;       // SPI2受信割り込み許可
    //IEC1bits.SPI2TXIE = 1;      // SPI2送信割り込み許可
    IEC1bits.SPI2EIE  = 1;      // SPI2エラー割り込み許可

    IPC9bits.SPI2IP = 3;  // 優先度(Priority)を 3 に設定 (1?7)
    IPC9bits.SPI2IS = 0;  // 副優先度(Sub-priority)を 0 に設定 (0?3)
   
    SPI2CONbits.ON = 1;         // SPI2モジュール有効
}
以上の関数定義は、my_sys.cのinit_handle_area関数内に、次のコード追加で行っています。
    extern void PPS_Init_SPI2_Slave(void);
    extern void Init_SPI2_Slave(void);
    extern void SPI2_Slave_func(void);
    handlers[_IDX_PPS_INIT_SPI2_SLAVE]=(void *)PPS_Init_SPI2_Slave; // SPI2利用のためのPPS設定
    handlers[_IDX_INIT_SPI2_SLAVE]=(void *)Init_SPI2_Slave; // SPI2の初期化デファルト関数登録
    handlers[_IDX_SPI2_FUNC]=(void *)SPI2_Slave_func;       // SPI2受信割り込みデファルト


以上を使ってumehoshiEdit.exeのツールで、 上述のRaspberryPi側のSPIマスターPythonプログラムからのSPIスレーブ受信を行うコードは次のようになりました。
#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include <proc/p32mx270f256b.h>
#include <sys/attribs.h>//割込み関係の定義

int spi_out_v = 0x0ff; // SPI出力用データ

void SPI2_Slave_func(void)// スレーブ用割り込み処理から呼ばれる
{
    /* ---- 受信割り込み ---- */ 
    if (IFS1bits.SPI2RXIF)// SPI2RXIF = 受信バッファにデータが入った 
    {
        uint8_t data;
        data = SPI2BUF;        // SPI受信データを読む
                               // 読まないとSPIROV(オーバーフロー)が発生する
        /* ここで data を処理する */
        _clear_beep_code();
        _debug_hex8(0, data, 1); // 受信データ確認
        spi_out_v-=1;
        while(SPI2STATbits.SPITBE == 0); // 送信バッファ空待ち
        SPI2BUF = spi_out_v;// 次の送信データのセット
    }

    /* ---- 送信割り込み ---- */
    /* PIC32(および多くのマイコン)において、「割り込みフラグ(IFS)」と「割り込み許可ビット(IEC)」は独立して動いているため
       受信で割り込みがあると、送信割り込みを不許可にしても、「割り込みフラグ(IFS)」がセットされる。
   (書かない方はよい。)
    if (IFS1bits.SPI2TXIF)// SPI2TXIF = 送信バッファが空 
    {
        SPI2BUF = 0x55;        // 次の送信データを書く
                               // 書くと送信開始される

        IFS1bits.SPI2TXIF = 0; // TX割り込みフラグクリア
    }
    */

    /* ---- エラー割り込み ---- */
    if (IFS1bits.SPI2EIF)
    {
        SPI2STATbits.SPIROV = 0;   // 受信オーバーフロー解除
        IFS1bits.SPI2EIF = 0;      // エラーフラグクリア
    }
}

// UMEHOSHI ITA のRAMプログラムの設定希望プログラム(0x80005000番地より起動)
__attribute__((address( 0x80005000 ))) void start (void);
void start()
{
	_clear_beep_code(); // _debug_hex? 関数利用の初期化
	_UM_PTR_GOTO_BEEP = NULL; // debug_hex?の出力をループしない設定

	_HANDLES[_IDX_SPI2_FUNC] = (void *)SPI2_Slave_func;// SPI割り込み関数の登録

	((void (**)(void) )_PTR_HANDLERS)[_IDX_INIT_SPI2_SLAVE]();// SPI2の初期化
	SPI2BUF = spi_out_v;// 最初の送信データのセット(重要)

	_RB5 = 0; // D1 LED の消灯
	_debug_hex4(0,0x0f,1);//  _debug_hex? の動作確認用
	_send_string("start END\r\n");//起動関数終了の表示
}


Raspberry Pi 3 Model A+側はモータPWM情報をSPIでマスターで送り、受信した「UMEHOSHI ITA」でモータを制御する。

RaspberryPi3側からモータ制御用のPIC32MXにSPI通信でPWMデータが送信できる状態になった。
しかしSPIは「相手に正しく伝達できたか(相手が正しく読んだか)」をハードウェアレベルで知る術が規格自体には存在しない。
(SPIは「クロックに合わせて機械的にビットを押し出し、同時に吸い込む」だけの非常にシンプルな構造だからです。)

一つPWMデータは符号あり16ビットと考えおり上位バイトと下位バイトで同期がズレが起きない制御が必要である。
また、ビッグエンディアン送信とします。
(Raspberry Pi側で使うPythonのspidevでは、リスト形式でバイトごとに送っています。 そして多くのデバイスは「上位バイト(MSB) → 下位バイト(LSB)」の順(ビッグエンディアン)でデータを期待するため、それに合わせました。)
上位バイトと下位バイトで同期がズレが起きない工夫として次のようなSPIの送受信制御にします。
まとまった送信データを、「右モータのPWMデータ(符号あり16bit)と右モータのPWMデータ(符号あり16bit)」の計32ビットとする。
(ここで符号ありとしたのは、正ならモータを正転とし負ならモータを逆転させる情報にするためです。)
こうすると、送る情報に識別番号が付けられるので、それを次のように決めます。
SPI送信データの意味右モータのPWMの
データMSByte
右モータのPWMの
データLSByte
左モータのPWMの
データMSByte
左モータのPWMの
データLSByte
上のデータを識別番号0123
そして、識別番号を利用いて次のように制御します。
  1. スレーブ側は、次に受信を希望する識別番号をslv_req_idxに記憶して、それを送信データとしてセットしておきます。
    (最初のslv_req_idxは、1を記憶しておくことになります。)
  2. マスターは、最初にmas_req_idxに識別番号の0を記憶しておきます。
  3. 送受信の繰り返し
    1. マスターは、mas_req_idxに記憶される識別番号が示す送信データを送信します。同時にスレーブからの受信要求番号を、mas_req_idxに記憶します。
      (マスター側は同じ要求番号を連続して受信した場合、スレーブ側が取り損なったと判断して、チョット待ってからmas_req_idxに識別番号の0にして次の繰り返しに進む。)
    2. スレーブは、sla_req_idxに記憶される識別番号から前に要求した識別を得てそれが示す受信データとして格納します。
      格納データが左モータのPWMデータのMSByteであれば、「右モータのPWMデータ(符号あり16bit)と右モータのPWMデータ(符号あり16bit)」の計32ビットが更新されたととしてPWM制御行います。
      格納データの次に要求すべき識別番号をreq_idxに記憶して、それを送信データとしてセットしておきます。
      sla_req_idxに記憶される識別番号を次の要求番号へ変更する順番は 1→2→3→0→1→2→3→0→・・・と続く
      (スレーブ側で受信側の読み取りが間に合わないエラー割り込みがあった場合、更新されていない判断しslv_req_idxに1記憶して、それを送信データとしてセットしておきます。)
下記は、上記の
32ビットを正確に送るためのSPI送信用関数を定義し、その確認用のpythonコード(spimaslist.py)です。
(32ビットに限らず、スレーブ側の処理に応じて複数バイトの送信を確実に行えるように考慮したコードです。)
import spidev
import time

def spi_send(spi: spidev.SpiDev, datas: list):
    # spiで、listのbyte列を送る。 (送信成功で、Trueを返す)
    mas_req_idx:int=0 # 要求されたデータの添え字
    last_idx = len(datas)-1
    fail_count=0
    while True:
        send_data = datas[mas_req_idx]
        rec_data = spi.xfer2([send_data])[0] # 1byte送受信
        print(f"send[{mas_req_idx}]:{send_data},receive: {rec_data}")
        if rec_data > last_idx: return False # スレーブから想定外の受信
        if rec_data == mas_req_idx: # 受信エラーと判断し、先頭から送り直す。
            mas_req_idx = 0
            fail_count+=1 # 失敗数カウント
            if fail_count > 5: return False # 送信失敗
            time.sleep(0.001) # チョット待つ
            continue
        if mas_req_idx == last_idx: return True # 送信成功終了
        mas_req_idx = rec_data # 次のデータの添え字に更新
    #
#

spi = spidev.SpiDev() # SPI操作オブジェクト生成
spi.open(0, 0)        # bus=0 device=0 (CE0) でSPIオープン

spi.max_speed_hz = 7812500 # 7.8 MHzSPIクロック
spi.mode = 0 # クロック信号(SCLK)はLow待機、LowからHighに立ち上がる瞬間でデータを読み取り

value=4847 # 0x12ef
#value=20479 # 0x4fff
data_list = list(value.to_bytes(2, byteorder='big' , signed=True))
value=-479 # 0xfe21
data_list += list(value.to_bytes(2, byteorder='big', signed=True))

print(data_list)
print(spi_send(spi, data_list)) # 送信、確認

while True:
    value = int(input("PWM data>>"))
    data_list = list(value.to_bytes(2, byteorder='big', signed=True))  # 右 PWM
    print(f"{data_list[0]:X}, {data_list[1]:X}")
    data_list += list(value.to_bytes(2, byteorder='big', signed=True)) # 左 PWM
    print(spi_send(spi, data_list)) # 送信、確認
#

上記では、右用の20479と左用の-479の32ビットを送った後、-32768〜32767の範囲でキー入力した値を、右と左に同じ値のPWMをまとめた32ビットをSPIで送信するプログラムです。

上記の
Parspberry PIのSPIマスター用コードからのPWMの情報を受け取るUMEHOSHI ITA基板側のSPIスレーブコードを以下に示します。

//UMEHOSHI ITA基板側のSPIスレーブコード(pwm_pi3ma_spi.c)  
#include <xc.h> 
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include <proc/p32mx270f256b.h>
#include <sys/attribs.h>//割込み関係の定義

int reset_on_zero_sequence(int val); // ゼロが連続したときにモータなどを初期化する
void set_PWM(int16_t right_pwm, int16_t left_pwm ); // 右と左のPWMデータをセット

#define LEN 4
int slv_req_idx=1; // 次に要求する識別番号
uint8_t datas[LEN]={0,0,0,0};// 受信データ記憶域

void SPI2_Slave_func(void)// スレーブ用割り込み処理から呼ばれる
{
	/* ---- 受信割り込み ---- */ 
	if (IFS1bits.SPI2RXIF)// SPI2RXIF = 受信バッファにデータが入った 
	{
		//_send_hex_low(TMR2); 		// 確認用
		//_send_hex_low(PR2); 		// 確認用
		//_send_hex_low(IEC0bits.T2IE ); // 確認用
		//_send_hex_low(T2CONbits.ON); // 確認用
		//_send_string("\r\n"); 	// 確認用

		uint8_t val;
		val = SPI2BUF; // SPI受信データを読む(読まないとSPIROVのオーバーフローが発生)

		// 前に要求した識別番号を得てそれが示す受信データとして格納
		if( slv_req_idx == 0) {
			datas[LEN -1] = val; // バッファの最後に要求した受信データを格納
		} else {
			datas[slv_req_idx -1] = val;// 要求した受信データを格納
		}
		// 次に要求するデータを記憶する添え字に更新
		slv_req_idx ++;
		if( slv_req_idx == LEN ){
			 slv_req_idx = 0;
		}

		if( reset_on_zero_sequence(val) ) {// ゼロが連続したときの初期化
			slv_req_idx = 1;// 初期化要求なら次の要求番号を1
			SPI2BUF = slv_req_idx;// 次の送信データをセット
			IFS1bits.SPI2RXIF = 0;   // 受信割り込みフラグクリア
			return;
		}

		while(SPI2STATbits.SPITBE == 0); // 送信バッファ空待ち
		SPI2BUF = slv_req_idx;// 次の送信データをセット

		// バッファに溜まった情報をデコードしてPWMデータに戻す。
		if (slv_req_idx == 1){// PIC32はリトルエンディアンに合わせてデコード
			int16_t right_pwm= ((uint16_t)datas[0] << 8) | datas[1]; 
			int16_t left_pwm = ((uint16_t)datas[2] << 8) | datas[3]; 
			set_PWM(right_pwm, left_pwm );// 右と左のPWMデータをセット
		}
	      IFS1bits.SPI2RXIF = 0;   // 受信割り込みフラグクリア
	}

	/* ---- エラー割り込み ---- */
	if (IFS1bits.SPI2EIF)
	{
		SPI2STATbits.SPIROV = 0;   // 受信オーバーフロー解除
		slv_req_idx = 1;
		SPI2BUF = slv_req_idx;// 次の送信データ先頭用にセット

		IFS1bits.SPI2EIF = 0;      // エラーフラグクリア
		_debug_hex16(9, 0x0ffff  , 1); 
	}
}

int reset_on_zero_sequence(int val) // ゼロが連続したときにモータなどを初期化する
{
	static int zero_count=0;// 初期化判定用 0のbyteを連続5個受け取りで初期化
	if( val == 0){ // zero の連続5個受信で、次の受信を先頭データと初期化
		zero_count++;
		if( zero_count == 5){
			zero_count = 0;

			// モータ全停止
			OC4RS = 0;	// CN7[3-4]制御(正転情報)(RB13)
			OC3RS = 0;	// CN7[1-2]制御(逆転情報)(RB14)
			OC5RS = 0;	// CN6[1-2]制御(正転情報)(RB2)
			OC1RS = 0;	// CN6[3-4]制御(逆転情報)(RB3)
			_debug_hex8(8, 0, 1); // 16ビット受信データ確認
			return 1;
		}
	} else zero_count = 0;
	return 0;
}

void set_PWM(int16_t right_pwm, int16_t left_pwm )// 右と左のPWMデータをセット
{
	_send_decimal(right_pwm,10);// _send_hex_low(right_pwm);	// 確認用
	_send_string(": right_pwm , ");					// 確認用
	_send_decimal(left_pwm,10);	// _send_hex_low(left_pwm);	// 確認用
	_send_string(": left_pwm \r\n");				// 確認用
	//_clear_beep_code(); //デバック用ビープ領域初期化
	//_debug_hex16(0, right_pwm , 1); // 16ビット受信データ確認
	//_debug_hex16(1, left_pwm , 1); // 16ビット受信データ確認
	//OC4RS = 0;//0x3fff; 実験用コード 

	if(right_pwm >= 0){
		OC4RS = right_pwm;	// CN7[3-4]制御(RB13)
		OC3RS = 0x0;		// CN7[1-2]制御(RB14)
	} else {
		OC4RS = 0x0;		// CN7[3-4]制御(RB13)
		OC3RS = -right_pwm;;	// CN7[1-2]制御(RB14)
	}
	if(left_pwm  >= 0){
		OC5RS = left_pwm;	// CN6[1-2]制御(RB2)
		OC1RS = 0x0;	// CN6[3-4]制御(RB3)
	} else {
		OC5RS = 0x0;	// CN6[1-2]制御(RB2)
		OC1RS = -left_pwm;	// CN6[3-4]制御(RB3)
	}
}

// UMEHOSHI ITA のRAMプログラムの設定用プログラム(0x80005000番地より起動)
__attribute__((address( 0x80005000 ))) void start (void);
void start()
{
	//__builtin_disable_interrupts();// PIC32MX270F256B-50I/SPの割り込みをまとめて一括禁止(Disable)
	// これは、コンパイラ(XC32)の組み込み関数(Built-in Function) 「asm volatile ("di");」でも良い

	_clear_beep_code(); // _debug_hex? 関数利用の初期化
	_UM_PTR_GOTO_BEEP = NULL; // debug_hex?の出力をループしない設定

	_set_pwd_mode(1); // PWM モードへ変更( SPI2の初期化の前に行う。そうしないとPWMが動かない:理由不明)

	_HANDLES[_IDX_SPI2_FUNC] = (void *)SPI2_Slave_func;// 上記のSPI割り込み関数の登録
	((void (**)(void) )_PTR_HANDLERS)[_IDX_INIT_SPI2_SLAVE]();// SPI2の初期化

	slv_req_idx=1;	// 最初設定
	SPI2BUF = slv_req_idx;// 最初の送信データのセット(重要)

	_RB5 = 0; // D1 LED の消灯

	T2CONbits.ON = 1; //Timer2の機能を有効(PWM利用ではこの有効設定が必要)
	PR2=0x09c3F;	// Timer2の周期設定
	//IEC0bits.T2IE = 0; // Timer2 Interrupt Enable を 0 (禁止) にする。(割り込み必要無し)

	//OC4RS = 0x1fff;  //CN7のPWMの値設定確認用

	//_debug_hex4(0,slv_req_idx,1);//  _debug_hex? の動作確認用
	//_send_string("start END\r\n");//起動関数終了の表示

	//__builtin_enable_interrupts();// PIC32MX270F256B-50I/SPの割り込みをまとめて一括許可(Enable)
	// 上記は、組み込み関数(asm volatile ("ei");でも良い 
}

以下は上記SPI送受信の実行計です。左に合わせて右は改行や説明などを追加しています。
Raspberry PI側のPythonのSPIマスター実行例PIC32MXの実行例(「umehoshiEdit」ツールでの表示)
suzuki@raspberrypi:/usr/local/apps $ sudo python  spimaslist.py
[18, 239, 254, 33]  ← 送信予定のリスト
send[0]:18,receive: 1 ←18が1byte送信で、1の受信が要求番号
send[1]:239,receive: 2 ←1の受信で、send[1]の239の1byteを送信
send[2]:254,receive: 3
send[3]:33,receive: 0
True
PWM data>>0 ← 以降はキー入力データを送信(0のPWMはモータ停止)
0, 0
send[0]:0,receive: 1
send[1]:0,receive: 2
send[2]:0,receive: 3
send[3]:0,receive: 0
True
PWM data>>-32768  ← このキー入力値はモータの正転の最大値
80, 0
send[0]:128,receive: 1
send[1]:0,receive: 2
send[2]:128,receive: 3
send[3]:0,receive: 0
True
PWM data>>32767
7F, FF
send[0]:127,receive: 1
send[1]:255,receive: 2
send[2]:127,receive: 3
send[3]:255,receive: 0
True
PWM data>>0
0, 0
send[0]:0,receive: 1
send[1]:0,receive: 2
send[2]:0,receive: 3
send[3]:0,receive: 0
True
PWM data>>
START:80005000		← 起動の表示(80005000番地より実行)





     +4847: right_pwm ,       -479: left_pwm ←32bit受信でモータが回る 





         0: right_pwm ,          0: left_pwm ←0の受信でモータが停止






    -32768: right_pwm ,     -32768: left_pwm ←この受信値はモータ正転最大






    +32767: right_pwm ,     +32767: left_pwm ←この受信値はモータ逆転最大






         0: right_pwm ,          0: left_pwm ←0の受信でモータが停止

以上のように実行できました。
PWMの情報は、-32768(0x8000)〜32767(0x7FFF)の値で送られます。この値はPIC32MXのPWMデューティ比を決めるOCxRSレジスタに直接送られます。
そして、PWMの周期は「PR2=0x9c3F」と設定(以前の実験と同じ設定)しています。 よってデューティ比最大にはならない設定で使います。
PR2=0x9c3Fによって、TMR2のタイマー2のカウントアップは0x9c3Fまで進んだ所で0に戻りモータがONします。
OCxRSに正転最大値(0x7FFF)が設定された場合、TMR2の値が0x7FFFになるとモータがOFFします。
よって、0x7FFF(10進は32767)までカウントアップ期間がONとなり、残り0x9c3F-0x7FFF=7232の個数のカウントアップ期間はOFFになります。
(つまり、このデューティ比の最大は、 (ONの時間 ÷ 全体の周期) × 100 [%] =0x7fff/0x9c3F =約0.82%です。)



RasPiよりPWM情報を上記SPIで送信して、ロボットの倒立振子の検討を行う。

前述のpwm_pi3ma_spi.cの確認用黄色部分の出力コードをコメントにし、 そのビルドデータでそれを初期実行させるpwm_pi3ma_spi.c.umhを作成しておきます。
これをumeusbモジュール使って、例えば次のように読み込み初期化させます。
umeusb.send_cmdfile("/usr/local/apps/pwm_pi3ma_spi.c.umh") # SPIでPWM送信するモジュールを初期化

さて前回の ロボットの倒立振子の検討のコードを再検討し、モジュール分割する。
その一つとして、SSD1306チップを使った128×64ドット有機ELディスプレイをI2Cで操作するためのモジュールを、 次のように作成した。
これは、複数のモージュールからディスプレイに描画するためのdraw_text関数を定義したモジュールです。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#  ssd1306.py のファイル名で、ssd1306を使った128×64ドット有機ELディスプレイをI2Cで操作するdraw_text関数の定義
#  利用する場合、『from SSD1306 import i2c, draw_text』 を記述して使うとよい。

import board
import busio
from adafruit_ssd1306 import SSD1306_I2C # SSD1306ディスプレイ用

from PIL import Image, ImageDraw, ImageFont
import time

i2c = busio.I2C(board.SCL, board.SDA)# --- I2C初期化 ---

# --- SSD1306ディスプレイ初期化 (128x64の場合) -----------
oled = SSD1306_I2C(128, 64, i2c)
oled.contrast(128) # 0?255

oled.fill(0)   # --- クリア
oled.show() # ---表示

# --- Pillowで描画領域を作成 ---
image = Image.new("1", (oled.width, oled.height))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()# --- フォント設定 ---

def draw_text(txt: str, row=0, showFlag = True, newImageFlag = False, font=font, fill=255):
   '''txtの文字列を、row行目に設定する。
   showFlagをFalseにすると表示はしないで、次の表示に使うイメージに内部を更新する。
   newImageFlagをTrueにすると、以前の描画イメージをクリアして、新しい文字列として更新する。
   '''
   global image,draw
   if newImageFlag: 
      image = Image.new("1", (oled.width, oled.height)) # イメージ作り直し(全体クリア)
      draw = ImageDraw.Draw(image)
   # --- テキスト描画 (0=黒、255=白)上記設定で、横21文字---
   draw.text((0, row*15), txt , font=font, fill=255)
   # --- 画面に表示 ---
   oled.image(image)
   if showFlag: oled.show()

if __name__ == '__main__':
   draw_text(f"UMEHOSHI ITA",0,showFlag=False,newImageFlag=True)
   draw_text(f"123456789ABCDEFGHIJKLMN",1,showFlag=False)
   draw_text(f"SSD1306 Display",2)