電子工作プログラミングにおいて、あれば便利な計測機器としてロジックアナライザがあります。波形などアナログのデータを取得できるオシロスコープとは違い、ロジックアナライザは基本的にオン・オフの信号しか読み取りませんが、規格化されている高速デジタル通信を可視化することには秀でています。

そんなロジックアナライザをArduinoで作るというオープンソースプロジェクトがあったのでご紹介したいと思います。

必要なもの
  1. Arduino本体
    USBシリアル通信に対応しているのであれば、基本的にArduino NanoやArduino互換機でも可。STM32にも対応しています。
  2. Processing
    Javaベースの開発環境です。クライアントソフトがこの言語で作られています。

ロジックアナライザの作り方
GitHubプロジェクトページでソースコード一式をダウンロードし、Arduino IDEを開いたら、「UNO.ino」をスケッチへコピーして、Arduinoへ書き込みます。これでハードウェアの完成です。標準ではArduinoのD8~D13ピンに接続したピンが解析対象となります。
クライアントソフトの作り方
Processingを起動したら、先ほどのソースコードに含まれていた「processing.pde」の内容を読み込むか、コピー&ペーストします。次に、「String LA_port = "COM10";」の数値をロジックアナライザプログラムを書き込んでいるArduinoが所有するポート番号(Arduino IDEの「ツール→シリアルポート」で確認できます)に置き換えます。
プログラムを修正したら、「実行」ボタンを押してプログラムを開始すると、クライアントソフトが動作します。
「Start」ボタンを押すとしばらくの後、信号の解析結果が表示されます。なお、信号の変化が全く見つからなかった場合は黒い画面のままとなります。
「Start」ボタンの右隣の「milliseconds/microseconds」で時間軸をミリ秒かマイクロ秒単位かに切り替えます。次の数値は倍率を示しており、ボタンのクリックではなく、マウスホイールで変更します。「Save」ボタンを押すと、プロジェクトファイルのあるフォルダーにtiff形式でスクリーンショットが保存されます。
 | 2018年10月10日

通信方式としてSPIとセットにして解説されるI2C。Arduinoでは必要なハードウェア設定は代わりにやってくれるので、すぐにプログラムが組めますが、PICマイコンではその箇所もきっちりとプログラムしないといけないので、まあまあやっかいです。

個人的な意見ですが、サンプルの制作に当たっては、Arduinoのプログラムコードを参考にする場合に気をつけなければいけない点として、これらが挙げられます。



ではマイクロチップ社のI2C方式I/Oエキスパンダー「MCP23017」のLチカを通して大まかなプログラムの流れを組んでみましょう。
MCP22017の左下にあるGNDの3本線はスレーブアドレスの定義用です。すべてが0だとアドレスは「0x20(0b10000)」ですが、3本すべてに電圧をかけている状態だと「0x27(0b10111)」になります。 その右隣にある電源から引っ張ってきた線はオフになるとリセットされます。プログラムを書き込んだ直後で正しく動かないようであれば、この線を挿し直してみましょう。

PIC32MX220F032Bでは、I2Cの信号線の位置は固定です。この例ではI2C2に当たるRB2とRB3を接続しています。

400kHz以上の高速度でデータをやりとりする場合は「I2C2CONbits.DISSLW = 0」のままでかまいません。この周波数を指示するレジスタが「I2CxBRG」ですが、計算式はメーカーによると「(I2Cクロック) = (周辺クロック)/(2*(I2CxBRG + 2))」とのことなので、以下の数値が目安になります。

周辺クロックI2C周波数I2CxBRGの値
80MHz400kHz0x062
80MHz100kHz0x18E
40MHz400kHz0x030
40MHz100kHz0x0C6
20MHz400kHz0x017
20MHz100kHz0x062
#include <stdio.h>
#include <stdlib.h>
#include <p32xxxx.h>

typedef unsigned char byte;

#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

// レジスタに登録するアドレスは7bitの数値を1bit左にずらしたものになる
#define MCP23017_ADDR (0x20 << 1)

void IdleI2C(void)
{
    // スタート、待機、受信、ACK、送信レジスタがクリアされるまで待機する
    while(I2C2CONbits.SEN || I2C2CONbits.PEN || I2C2CONbits.RSEN || I2C2CONbits.RCEN || I2C2CONbits.ACKEN || I2C2STATbits.TRSTAT);
}

unsigned int WriteI2C(unsigned char data)
{
    I2C2TRN = data;
    
    if(I2C2STATbits.IWCOL) {
        // 通信の衝突が発生したなら0を返す
        return 0;  
    }else{
         while(I2C2STATbits.TBF);      // 送信バッファが空になるのを待機
        IdleI2C();
        return 1;
    }
}

void send(byte reg, byte val)
{
    // 開始フラグ
    I2C2CONbits.SEN = 1;
      
    IdleI2C();    
    
    WriteI2C(MCP23017_ADDR);  
    WriteI2C(reg);    
    WriteI2C(val);
    
    // 停止状態にして送信を完了
    I2C2CONbits.PEN = 1; 
    
    IdleI2C();

    
    /*
    // データ受信のプログラム例(※汎用例です)
    I2C2CONbits.SEN = 1;
    IdleI2C();
    WriteI2C(SLAVE_ADDR);
    WriteI2C(reg);    
    WriteI2C(val);

    // 信号を途切れさせず、連続してデータを送りたいときの例
    I2C2CONbits.RSEN = 1; // リピートスタート
    IdleI2C();
    WriteI2C(SLAVE_ADDR | 0x1);  // 0x1はスレーブから受信を促すために必要
    WriteI2C(val);
    
    // データ読み出し
    I2C2CONbits.RCEN = 1;     // 受信モードに移行
    while(I2C2CONbits.RCEN) ; // 受信が完了すると0になるのでそれまで待機
    I2C2STATbits.I2COV = 0;   // バッファあふれのエラーフラグをリセット
    res = I2C2RCV;            // 受信したデータを格納
     
    I2C2CONbits.PEN = 1; 
    IdleI2C1();
    */
}

int main()   
{   
    int i, toggle = 0xFF;
    ANSELB = 0;
    
    // B2,B3を入力モードにし、プルアップ抵抗を有効にする
    TRISBbits.TRISB2 = 1;
    TRISBbits.TRISB3 = 1;
    CNPUBbits.CNPUB2 = 1;
    CNPUBbits.CNPUB3 = 1;
    
    // 100kHzモード
    I2C2CONbits.DISSLW = 1;
    I2C2BRG = 0x0C5;
    
    // I2C2を有効にする
    I2C2CONbits.ON = 1;
    
    send(0x00, 0x00);  // PortAをすべて出力にする命令
   
    while(1){
        send(0x12, toggle);  // PortAのGPIOをすべてON/OFFにする命令
        toggle = ~toggle;
        for(i=0;i<1000000;i++);
    }
   
    return 0;   
}   
 | 2018年10月10日

PIC32のHarmonyフレームワークで一般的なSPIスレーブ機器にデータを送りたい場合は「DRV_SPI_BufferAddWriteRead」の関数を呼び出すだけで事足ります。もうそれしか言うことがない(とはいうものの、資料が少なく、ここまでたどり着くのは難しかったです……)ので、とっととフレームワークの作成について解説します。

前回紹介した「MCP23S17の制御プログラム」をHarmonyフレームワークに移植してみます。SPIドライバーは次のように設定します。タイマードライバーは前回の設定例と同じ値にしてください。
「Clock mode」はArduinoにおける「SPI Mode」に相当しており、標準設定ではMode0と同じ動作をします。

DRV_SPI_CLOCK_MODE_IDLE_LOW_EDGE_RISEMode0
DRV_SPI_CLOCK_MODE_IDLE_LOW_EDGE_FALLMode1
DRV_SPI_CLOCK_MODE_IDLE_HIGH_EDGE_FALLMode2
DRV_SPI_CLOCK_MODE_IDLE_HIGH_EDGE_RISEMode3


SPI2を各ピンに割り当てる例です。3バイト単位でスレーブ選択信号を切り替えたいので、RB0をSS2(OUT)ではなく、GPIO_OUTにしています。
サンプルコードを見ると、初期化設定がまるごと隠蔽されているため、とてもすっきりしたコードになっています。
#include "app.h"
#include <p32xxxx.h>

typedef unsigned char byte;

void send(byte addr, byte data)
{  
    SS2Off();  // RB0をオフにしてデータの送信を開始

    byte buffer[3], dummy[3];
    buffer[0] = 0x40;
    buffer[1] = addr;
    buffer[2] = data;
    // buffer - 送信するデータ
    // dummy  - 受信するデータ
    // 3      - 送受信するデータのサイズ(ビットモードに関わらず1バイト単位)
    DRV_SPI0_BufferAddWriteRead(buffer, dummy, 3);
    
    SS2On();   // RB0をオンにしてデータの送信を終了
}


void APP_Initialize ( void )

    SS2On();   // RB0をオンにして待機状態にさせる
    
    send(0x0A, 0x20); // IOCON[0x0A]、シーケンシャルモードを無効[SEQOP = 1]
    send(0x00, 0x00); // IODIRA[0x00]、ポートAのすべてのピンを出力モードに
    send(0x12, 0xFF); // GPIOA[0x12]、すべてのピンをオン(1)にする
    
    DRV_TMR0_Start(); // タイマーの始動。タイマードライバーは設定しましたか?
}

void APP_Tasks ( void )
{
}

int spistate = 0xFF, tcount = 0;

void OnTimer1()
{
    tcount++;
    if(tcount == 10){
        send(0x12, spistate);  // GPIOA[0x12]
        spistate = ~spistate;  // すべてのピンのオン(1)・オフ(0)を切り替える
        tcount = 0;
    }
}
, | 2018年10月8日

PIC32開発向けのフレームワークであるMPLAB Harmonyでは、ドライバーをフレームワーク設定から導入すれば、少しのコードで処理を実装できます。以前のレジスタ処理によるプログラミングでは個別の紹介をしましたが、Harmonyフレームワークによるプログラミングでは記事の内容が薄くなってしまうので、今回はタイマーとUSART(UARTの上位互換にあたる)の使い方を一度に紹介します。

まずはフレームワークオプションのタブより「Harmony Framework Configration→Drivers→Timer」より「タイマードライバを使用(Use Timer Driver)?」にチェックを入れて、値を設定します。「Driver Implementation」が「STATIC」であれば、タイマーを初期化するルーチンが自動でプログラムに組み込まれます。「DYNAMIC」の場合は、サンプルコードのようにタイマーを初期化するルーチンを自前で用意して、任意の場所で実行する必要があります。
DRV_HANDLE timer1 = NULL;
DRV_TMR_INIT    prop;
prop.moduleInit.value = SYS_MODULE_POWER_RUN_FULL;
prop.tmrId            = TMR_ID_1;
prop.clockSource      = DRV_TMR_CLKSOURCE_INTERNAL;
prop.prescale         = TMR_PRESCALE_VALUE_256;
prop.interruptSource  = INT_SOURCE_TIMER_1;
prop.mode             = DRV_TMR_OPERATION_MODE_16_BIT;
prop.asyncWriteEnable = false;

SYS_MODULE_OBJ obj = DRV_TMR_Initialize(DRV_TMR_INDEX_0, (SYS_MODULE_INIT*)&prop );
if(obj != SYS_MODULE_OBJ_INVALID){
  PLIB_TMR_Period16BitSet(TMR_ID_1, 7812);
  timer1 = DRV_TMR_Open(DRV_TMR_INDEX_1, (SYS_MODULE_INIT*)&prop);
}
Timer Periodの値はPRxのレジスターと同義なので、この数値の計算方法は「タイマー1によるLチカ」をご参照ください。複数のタイマーを使用したいのなら「Numer of Timer Driver Instances」の数を増やします。
続いてTimer項目の下にある「USART→USARTドライバを使用(Use USART Driver)?」にチェックを入れます。「Read\Write Model Support」にチェックを入れるとマルチバイトの読み書き(DRV_USART_Read/DRV_USART_Write)の関数が使えるようになります。
ピン設定でU1RX/U1TXの入出力先の指定もお忘れなく。
こちらはプログラム例です。この例では1秒ごと(割り込みは100ms間隔で発生する)にASCII文字列を送信しています。
#include "system/common/sys_common.h"
#include "app.h"
#include "system_definitions.h"

void __ISR(_TIMER_1_VECTOR, ipl4AUTO) IntHandlerDrvTmrInstance0(void)
{
    OnTimer1(); // app.hに「void OnTimer1();」の宣言も記述しておくこと
    
    // 割り込みフラグをリセット
    PLIB_INT_SourceFlagClear(INT_ID_0, INT_SOURCE_TIMER_1);
}
#include "app.h"
#include <stdio.h>

void APP_Initialize ( void )
{
    __XC_UART = 1; //stdioのUART出力先を1に指定するマクロ
    DRV_TMR0_Start(); // タイマーの開始
}

void APP_Tasks ( void )
{
}

int tmr1count = 0;

void OnTimer1()
{
    tmr1count++;
    if(tmr1count >= 10){
        DRV_USART_CLIENT_STATUS state = DRV_USART0_ClientStatus();
        if(state == DRV_USART_CLIENT_STATUS_READY) {
   // USARTが使用可能なら文字列(ASCII配列)を送る
           DRV_USART0_Write("Hello ", 6);
           printf("world!\n");
        }
        
        tmr1count = 0;
    }
}
, | 2018年10月7日

PICマイコンの開発環境であるMPLABには開発をサポートするライブラリーが別途提供されていましたが、従来の方式だとマイコンの型番が変わるとソースコードも変更しなければならないという組み込み開発特有の問題がありました。そこでマイクロチップ社はソースコードを動的に生成することで、開発者が記述するコードをできる限り少なくするためのフレームワーク「MPLAB Harmony」をこれまでのライブラリーの代替として公開しています。これを使うことで、GUIでの数値変更でハードウェア設定やタイマー処理などのPIC32にまつわる様々な構築を行うことができるようになっています。

ネットでの情報が乏しいことや、プログラムリソースを多く消費するのが欠点といえますが、GUIに慣れてしまえばものの5分でUSBキーボードも作れてしまうので、高度な処理が必要であれば利用する価値はあります(そもそもGPIOのオン・オフくらいの処理しかしないのならArduinoのほうが効率的ですし)。

ではMPLAB X IDEMPLAB Harmonyをインストールしたら、IDEを開いて「32-bit MPLAB Harmony Project」を任意のフォルダーに作成しましょう。ここでは自作の開発基盤を用いているので「Target Board」には「Custom」を指定しています。
プロジェクトが作成された直後や、ウィンドウメニューの「Tools→Embedded→MPLAB Harmony Configuler」を選ぶと、マイコンの環境設定画面が表示されます。まずは、マイコンのクロックを指定しましょう。「Clock Diagram」の「System PLL」にある「Auto-Calculate」ボタンを押すと、動作に最適な分周比などを自動で割り当ててくれます。あとは必要に応じて「POSCMOD」「FPBDIV」「UPLLIDIV/UPLLEN/UFRCEN」の値を赤文字の警告が出ない範囲で変更します。
設定が終わったら「Generate Code」を押してプログラムコードを作成させます。この作成作業は、環境設定を変えるごとに実行しなければプログラムに反映されないことや、設定ファイルを保存しないと環境設定をはじめからやり直す羽目になるので注意しましょう。
コードはこのように作成されます。
基本的にはArduinoのように「APP_Initialize()」で初期化のコードを書き、「APP_Task()」で繰り返し実行させたいプログラムを書きます。例えば、「app.c」のコードをこのように差し替えると、以前に紹介したLチカプログラムのHarmony版が完成します。
#include "app.h"

void APP_Initialize()
{
    TRISBbits.TRISB0 = 0;
    ANSELBbits.ANSB1 = 0;
    TRISBbits.TRISB1 = 1;    
}

void APP_Tasks()
{
    PORTBbits.RB0 = ~PORTBbits.RB1;
}
ではこのプログラムをHarmonyっぽく改良してみましょう。「MPLAB Harmony Configuler」を開いたら、まずは「Pin Settings」のタブを開き、「4/RB0」と「5/RB1」を画像のように、それぞれ「Led,GPIO_OUT,Digital」「Switch,GPIO_IN,Digital,Change Notifcation」に切り替えてピンの属性値を変更します。名前を指定すると、その名前を使用したポート制御命令が作成されるので、別のマイコンへの移植が容易になります。ちなみにこれは後述のオプション設定でも変更できます。
「Option」タブを選んでオプション設定を開いたら、「Port」の「変化検出を有効(Use Interrupt for Change Notification)?」にチェックを入れて、「INT_DISABLE_INTRRUPT」以外の優先度を選択します。
この状態で「Generate Code」ボタンを押すと、プロジェクトフォルダーの「Source Files/app/system_config/[name]/system_interrupt.c」に割り込み処理関数が生成されるので、ここに割り込み時の処理を記入します(コード生成時に「Automatically Overwrite User Changes」を選ぶと、ここに書いたコードは消えてしまうので注意!)。また、これにより「app.c」の処理は不要になるので、関数の中身は空にしておきましょう。
#include "system/common/sys_common.h"
#include "app.h"
#include "system_definitions.h"

void __ISR(_CHANGE_NOTICE_VECTOR, ipl4AUTO) _IntHandlerChangeNotification(void)
{
    // スイッチの状態を取得(Pin Settingsで指定した名前+StateGetが関数名になっている)
    bool pushed = !SwitchStateGet();
    // LEDの状態を変更
    LedStateSet(pushed);

    /*
    // ピンの番号を直接制御する場合
    // ポートモジュールO(標準)、Bチャンネル、1番ピンの状態を取得
    bool pushed = !PLIB_PORTS_PinGet(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_1);
    // ポートモジュールO(標準)、Bチャンネル、0番ピンの状態を設置
    PLIB_PORTS_PinWrite(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_0, pushed);

    // (その他、主なピン制御にまつわる関数)
    // PLIB_PORTS_PinClear  - 指定のピンをオフにする
    // PLIB_PORTS_PinSet    - 指定のピンをオンにする
    // PLIB_PORTS_PinToggle - 指定のピンのオン・オフを切り替える
    */

    PLIB_INT_SourceFlagClear(INT_ID_0, INT_SOURCE_CHANGE_NOTICE_B);
}
#include "app.h"

void APP_Initialize()
{
}

void APP_Tasks()
{
}
, | 2018年10月6日

言うまでもなくUSB規格の情報はとても膨大で、USB仕様書から1つの機器を作り上げるのは素人にはまず無理です。USBをサポートしたPICマイコンでは、信号の解析まではハードウェアで行ってくれますが、それをレジスタからどう扱うかはプログラムでの処理となります。これを補うため、マイクロチップではUSBライブラリーを別途配布しているわけですが、ここで互換性の問題にぶち当たります。

PIC16FやPIC32MMシリーズでは最新の拡張ライブラリー(MLA)でもUSBはサポートされていますが、それ以外のPIC32シリーズはMicrochip Harmonyフレームワークに取って代わっていて、従来のライブラリーを応用する場合は大幅な修正が必要です。

そんななか、既存のライブラリーに極力頼らないようにしているため、XC32とPIC32MXでも使えるUSBライブラリー「m-stack」がGithubで公開されていましたので、これを組み込んだ開発を試みてみました。

開発環境のMPLAB X IDEを起動してスタンダードプロジェクトを作成したら、Githubからダウンロードしたソースコードから「usb」のフォルダーをまるごとプロジェクトフォルダーに、「apps/hid_mouse」フォルダーにある「usb_config.h」と「usb_descriptors.c」をプロジェクトフォルダーにコピーします。
プロジェクトをICアイコンを右クリックして表示される「Properties」で表示されるウィンドウから「XC32→xc32-gcc」を選び、ドロップダウンリストから「Processing and messages」を指定します。更新されたリストにある「Include directories」より、プロジェクトフォルダーにある「usb/include」フォルダーの場所を入力します。
つぎはメインプログラムの移植です。UPLLIDIVへは「外部発振器のクロック数/UPLLIDIV=4MHz」が成り立つ値を入力します。私の開発ボードでは8MHz水晶発振子を用いているので、1/2を指示しています。設定と環境の組み合わせによっては周波数が不安定になることがあるので、パソコンにつないだときにデバイスを認識しない警告が出るようであれば「UPLLEN」と「UFRCEN」の組み合わせを変えてみましょう。
// Original program was developed by Alan Ott, Signal 11 Software.

#include <p32xxxx.h>
#include <stdio.h>
#include "usb.h"

#pragma config FNOSC = PRIPLL   // 主外部発振器
#pragma config POSCMOD = XT     // 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2   //周辺モジュールクロック倍数 : x1/2

#pragma config UPLLEN = ON      // USBPLLを使用する(デバイスの認識がうまくいかない場合はこの値をOFFにしてみる)
#pragma config UPLLIDIV = DIV_2 // USBクロック倍数 : x1/2(4MHzになるように設定)

// デバッグ用。必要に応じて値を変える
#pragma config ICESEL = ICS_PGx3
#pragma config DEBUG  = ON

void InitHardware(void)
{
    IPC7bits.USBIP = 4; // USBの割り込み優先度
    // マルチベクタ割り込みを有効
    INTCONbits.MVEC = 1;
    __builtin_enable_interrupts();
}


#ifdef MULTI_CLASS_DEVICE
static uint8_t hid_interfaces[] = { 0 };
#endif

void InitSerial()
{     
    // USBシリアルモニターでの検証用
    __XC_UART = 1;            // stdioによる出力先をUART1にする
    U1RXRbits.U1RXR = 0b0100; // UART1/RXにB2を
    RPB3Rbits.RPB3R = 0b0001; // B3をUART1/TXに
    
    U1STAbits.UTXEN = 1;  // UART1の送信を有効
    U1STAbits.URXEN = 1;  // UART1の受信を有効
    U1BRG = 129;          // ボーレート9600 [20M/(16*9600)-1]
    
    U1MODEbits.ON = 1;   // UART1を有効
    
    printf("Hello!\n"); 
}

int main(void)
{
    InitHardware();
    
    InitSerial();

#ifdef MULTI_CLASS_DEVICE
    hid_set_interface_list(hid_interfaces, sizeof(hid_interfaces));
#endif
    usb_init();

    uint8_t x_count = 100;
    uint8_t delay = 7;
    int8_t x_direc = 1;

    while (1) {
        if (usb_is_configured() &&
            !usb_in_endpoint_halted(1) &&
            !usb_in_endpoint_busy(1)) {

            // 送信バッファの更新
            unsigned char *buf = usb_get_in_buffer(1);
            buf[0] = 0x0;
            buf[1] = (--delay)? 0: x_direc;
            buf[2] = 0;
            usb_send_in_buffer(1, 3);

            if (delay == 0) {
                if (--x_count == 0) {
                    x_count = 100;
                    x_direc *= -1;
                }
                delay = 7;
            }
        }

        #ifndef USB_USE_INTERRUPTS
        usb_service();
        #endif
    }

    return 0;
}
サンプルのUSBマウスの定義(usb_descriptors.c)より、送信する命令のバイト配列がわかります。「Report Size」はビット数、「Report Count」はそれらの数なので、「ボタンに3bitが1つ(ビット演算による組み合わせで3つのボタンを同時に押せる)、割り当てられていない5bitが1つ、XY移動に8bitが2つ」と考えることができ、計3バイトの情報を送信することになります。

このことより、main.cでは一定時間ごとに「1バイト目のみを更新している=マウスのX値を更新している」ことがわかります。実際にプログラムを書き込んだマイコンをUSBでパソコンにつなぐと、マウスカーソルが左右にゆらゆらと動きます。

拙作の開発基盤を使用する場合は、写真のようにD+とD-をビニルワイヤーで結線しないと動作しないのでご注意ください。
今回はプロジェクトファイル一式を用意しています。ダウンロードはこちらから。
 | 2018年10月6日

例えばテレビリモコンのように、電池はつなぎっぱなしにするけれど、ボタンを押したときくらいしかCPUを動かす必要のない機器の作成では、普段は省電力モードにしておくことでバッテリーをより長く保つことができます。

PIC32では主な省電力モードに「CPUは止めるが周辺モジュールは維持する」アイドルモードと「CPUと、I/Oピンの入出力とWDT/RTCC/ADC/UART以外のモジュールを止める」スリープモードがあります(PIC32MZなどの上位モデルにはDMAモジュールを動かすときだけスリープからアイドルモードに移行するドリームモードなんてのもある)。もちろん、アイドルよりもスリープの方が低電力ですが、それだけ制限もあるので、使いたい機能は何であるかを念頭に置いて選択しましょう。

待機モードに移行するにはアセンブラ命令で「asm volatile ("wait");」を実行するだけです。この命令が実行されると、割り込み処理(割り込み優先度がCPU優先度より低いと何も起きないが、CPU優先度の初期状態は0なので特別気にする必要はない)によって復帰するまでこの命令にとどまり、復帰後はプログラムにおける次の行から再開されます。

特別な指定がなければ、待機命令でアイドルモードに移行します。以下のサンプルコードでは、初期設定の後にアイドル状態になり、タクトスイッチを押して割り込み処理を発生させると、先にタイマー1を有効にした上で、プログラムに復帰します。
#include <p32xxxx.h>
#include <sys/attribs.h>

// 外部の8MHz水晶発振器から40MHzのクロックを生成するための設定
#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

int count = 0, toggle = 0;
void __ISR(_TIMER_1_VECTOR, IPL4AUTO) OnTimer1(void) 
{
    IFS0bits.T1IF = 0; // 割り込みフラグをリセット

    count++;
    if(count >= 10){
        toggle = !toggle;
        LATBbits.LATB0 = toggle; // RB0のスイッチを切り替える      
        count = 0;
    }
}

void InitTimer1()
{
    T1CONbits.ON = 0;    // 設定のため、タイマー1を無効にする
    TMR1 = 0;            // タイマー1をリセット
     
    // 100ms周期で割り込み <- 500ns(1 / 20MHz) * 256 * 7812(0x1E84) = 100ms
    T1CONbits.TCKPS = 3; // プリスケーラ : x1/256
    PR1 = 0x1E84;        // カウンター
        
    IPC1bits.T1IS = 3;   // 副優先度(0-3)
    IPC1bits.T1IP = 4;   // 割り込み優先度(0-7)
    IFS0bits.T1IF = 0;   // 割り込みフラグをリセット
    IEC0bits.T1IE = 1;   // タイマー1の割り込みを許可
}

void __ISR(_CHANGE_NOTICE_VECTOR, IPL4AUTO) OnChanged(void) 
{
    IFS1bits.CNBIF = 0;
    T1CONbits.ON = 1;    // タイマー1有効
}

void InitB1Input()
{
    TRISBbits.TRISB1 = 1; // B1を入力にする
    CNCONBbits.ON = 1;    // ポートBの状態変化検出モジュールを使用
    CNENBbits.CNIEB1 = 1; // B1の検出を有効
    CNPUBbits.CNPUB1 = 1; // プルアップ抵抗を使用   
    
    IPC8bits.CNIP = 4;    // 割り込み優先度(0-7)
    IPC8bits.CNIS = 3;    // 副優先度(0-3)
    IFS1bits.CNBIF = 0;   // 割り込みフラグをリセット
    IEC1bits.CNBIE = 1;   // 状態変化Bの割り込みを許可
    
    PORTBbits.RB1 = 1;  // B1の初期状態を設定
}

int main(void)
{
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
    PORTB = 0;
    
    InitB1Input();
    InitTimer1();
        
    INTCONbits.MVEC = 1;            // マルチベクタ割り込みを有効にする      
    __builtin_enable_interrupts();  // マイコンにおける割り込みの有効  

    asm volatile ("wait");   // wait命令でアイドルモードに移行
        
    while(1) ;
}
先ほど述べたように、スリープモードでもI/O入力は受け付けるので、今度はスリープモードで動かしてみましょう。スリープモードにするには「OSCCONbits.SLPEN = 1;」と前置きしてからwait命令を実行するだけです。

スリープモードに移行する際に、あらかじめクロック発振源を内蔵発振器にすると、より低い電力の維持と素早い復帰を見込めます。ただし、これは誤動作の元になるため、通常はクロックの切り替えはできません。プログラム実行中でも任意に変更できるようにするには、「#pragma config FCKSM = CSECMD」で切り替えの許可を指示し、プログラム内で「OSCCONbits.CLKLOCK = 0」でロックを解除してから「OSCCONbits.NOSC」の値を変更します。
#include <p32xxxx.h>
#include <sys/attribs.h>

#pragma config FCKSM = CSECMD // クロックの切り替えを許可する

int main(void)
{
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
    PORTB = 0;
    
    InitB1Input();
    InitTimer1();
        
    INTCONbits.MVEC = 1;            // マルチベクタ割り込みを有効にする      
    __builtin_enable_interrupts();  // マイコンにおける割り込みの有効  
    
    OSCCONbits.CLKLOCK = 0;  // クロック切り替えのロックを解除
    OSCCONbits.NOSC = 0b101; //低電力内蔵発振器(約31KHzで動作)に切り替える
    OSCCONbits.SLPEN = 1;    // スリープモードを使用
    asm volatile ("wait");   // wait命令でスリープモードに移行
    
    OSCCONbits.NOSC = 0b011; // XTPLLモードに戻す
    OSCCONbits.CLKLOCK = 1;  // クロック切り替えのロックを有効
        
    while(1) ;
}
このサンプルコードは抜粋ですので、idle.cのコードに追加してください。

 | 2018年10月2日

特定の比率のオン・オフを周期的かつ正確に繰り返すパルス幅変調「PWM」は、サーボモーターの制御や、LEDの輝度調整やブザーの音階調整などの擬似的なアナログ出力に用いられます。PIC32マイコンでは、このPWM出力を出力コンペア機能の一つとして提供しています。

PWMを使用するには出力コンペア設定レジスタ(OCxCON)のOCMを0110にします。ちなみに、0111でフォルトを有効にすると、指定したフォルトピン(OCFA/OCFB)に0信号が流れたとき、PICはプログラムを介さず直接PWMを停止させます。

PIC32においては、タイマーで周期の長さをカウントし、タイマーカウントが特定の値(OCxR)になるとスイッチを切り、タイマーの1サイクルが終わると再びスイッチを入れることを繰り返しています。
タイマーは2もしくは3を使います。Timer2および出力コンペアをともに32bitモードにするとカウンターの上限が増えるので、極端に低い周波数の生成もできるようになります。

タイマークロックがそのまま1秒あたりの実行回となるので、このクロックを適用したい周波数で割ると、指定すべきタイマー値(PRx)になります。例えば、タイマーが20MHzクロックで動作している環境で1kHzの周期(1000カウントで1単位)を実装したいのなら、20000000/1000=2000が計算値となります。

PIC32MX1xx/2xxシリーズでは、PWMを出力できる主なピンはA0,B3,B4,B7,B15です。 PWMではスイッチのオン・オフの長さの比率がデューティ比になるので、30%にしたいときはPRxの1/3の計算結果をOCxRSの値にすると良いことになります。

このサンプルでは100msごとにタイマー割り込みを発生させ、呼ばれた回数に応じて「2kHz→1kHz→休止」の処理を繰り返します。
#include <p32xxxx.h>
#include <sys/attribs.h>

// 外部の8MHz水晶発振器から40MHzのクロックを生成するための設定
#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

#define BASE_CLK 20000000  // 周辺クロック(20MHz)

void updateDuty(int hz)
{
    // 1サイクルあたりの時間を計算
    int ticks = BASE_CLK / hz;
    PR2 = ticks;
    
    // PR2のタイマーカウントの半分で出力を切り替えるようにするとデューティ比が50%になる
    // 1/4ならデューティ比は25%になる。
    ticks /= 2;
    
    // OCxRSに値をセットするとバッファとして機能する。
    // これによりOCxRの値が急に切り替わることによって発生するノイズ(グリッチ)を抑止できる
    OC1RS = ticks;
    
    // 出力コンペアに割り当てているタイマーをリセットすることでPWMを再始動
    TMR2 = 0;
}

void InitTimer1()
{
    T1CONbits.ON = 0;    // 設定のため、タイマー1を無効にする
    TMR1 = 0;            // タイマー1をリセット
     
    // 100ms周期で割り込み <- 500ns(1 / 20MHz) * 256 * 7812(0x1E84) = 100ms
    T1CONbits.TCKPS = 3; // プリスケーラ : x1/256
    PR1 = 0x1E84;        // カウンター
        
    IPC1bits.T1IS = 3;   // 副優先度(0-3)
    IPC1bits.T1IP = 4;   // 割り込み優先度(0-7)
    IFS0bits.T1IF = 0;   // 割り込みフラグをリセット
    IEC0bits.T1IE = 1;   // タイマー1の割り込みを許可
    
    T1CONbits.ON = 1;    // タイマー1有効
}

int beep_mode = 1;

void __ISR(_TIMER_1_VECTOR, IPL4AUTO) OnTimer1()
{
    IFS0bits.T1IF = 0;

    if(beep_mode == 1){
        OC1CONbits.ON = 1;  // 出力コンペアを有効
        updateDuty(2000);
    }else if(beep_mode == 2){
        updateDuty(1000);
    }else if(beep_mode == 3){
        OC1CONbits.ON = 0;  // 出力コンペアを無効
    }else if(beep_mode == 16){
        beep_mode = 0;
    }
    beep_mode++;
}

int main(void)
{
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
    
    // タイマー2の設定(プリスケーラ1/1、内部クロックを使用)
    T2CONbits.ON = 0;
    T2CONbits.T32 = 1;       // タイマー3と併合して32bitモードで使用
    T2CONbits.TCKPS = 0b000; // 分周比 1:1
    T2CONbits.ON = 1;  
    TMR2 = 0;
        
    OC1CONbits.ON = 0;        // 設定のため出力コンパレータ(OC1)を無効に
    OC1CONbits.OCTSEL = 0;    // タイマー2を使用
    OC1CONbits.OCM = 0b0110;  // PWMで、失敗(fault)の検知を無効
    OC1CONbits.OC32 = 1;      // 32bitで計測
    RPB3Rbits.RPB3R = 0b0101; // B3にOC1を割り当てる
    OC1CONbits.ON = 1;
    
    InitTimer1();             // タイマー1の初期化
    
    INTCONbits.MVEC = 1;            // マルチベクタ割り込みを有効にする      
    __builtin_enable_interrupts();  // マイコンにおける割り込みの有効
    
    while(1) ;
}
PWMをデューティ比50%で出力すると一般的な矩形波になるので、圧電ブザーをB3とGND端子に接続すると、普遍的なブザー音が鳴り響きます。

 | 2018年10月1日

PIC32ではMSTEN=0をセットし、スレーブモードを有効にしてSPI通信を行うと、マスター側から受け取ったクロックのタイミングをハードウェアで認識してデータに変換してくれます。

スレーブモード固有の設定は入力タイミングに関連する指定が主のものとなります。SSENを1にするとSSxピンが閉じる(立ち下がる)を感知して、続いて送られてくるクロック信号とビット列の取得を開始します。SSEN=1に加えてFRMEN=1にすると、フレーム化SPI信号(クロック信号は常に発生していて、データの受信前に一瞬だけSSxピンのスイッチが切り替わる)を識別できます。ただ、私の知る限り、フレーム化SPIを送信するICはほとんど見かけません。

それではサンプルプログラムの紹介です。ここではArduinoで生成したSPI信号をPIC32で解析させます。
まずはマスター側であるArduinoのスケッチから。こちらではSSピンを手動でオフにしてから「hello」というASCIIコードを500msごとにSPI命令で送信しています。
#include <SPI.h>

#define SS 10

void setup() {
    pinMode(SS, OUTPUT);
    digitalWrite(SS, HIGH);

Serial.begin(9600);
    SPI.begin();
    delay(500);
}

void loop() {
    digitalWrite(SS, LOW);
    SPI.transfer('h');
    SPI.transfer('e');
    SPI.transfer('l');
    SPI.transfer('l');
    SPI.transfer('o');
    digitalWrite(SS, HIGH);

    delay(500);
}
つづいてPIC32でのプログラムコードです。SPI2の信号を各ピンに割り当てて(値とピンの関係についてはSPI送信の記事をご覧ください)、割り込み処理内のループで連続したビットを一度に取得しています。
#include <p32xxxx.h>
#include <sys/attribs.h>

// 外部の8MHz水晶発振器から40MHzのクロックを生成するための設定
#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

#pragma config ICESEL = ICS_PGx3
#pragma config DEBUG  = ON

void __ISR(_SPI2_VECTOR, IPL4AUTO) OnSpi2(void)
{
    int i, n[5];
     IFS1bits.SPI2RXIF = 0;     // 割り込みフラグのクリア
   
    for(i = 0; i < 5; i++){
        SPI2BUF = 0;
        while(!SPI2STATbits.SPIRBF);  // 受信バッファが使用可能になるのを待つ
        n[i] = SPI2BUF;
    }
}

int main(void)
{
    int i;
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
      
    SPI2CONbits.ON = 0;     // 設定のためSPI1を無効に
    
    SPI2CONbits.SSEN = 1;   // SSピンを有効にする
    SPI2CONbits.MSTEN = 0;  // スレーブモード
    SPI2CONbits.CKP = 0;    // ArduinoにおけるMode0
    SPI2CONbits.CKE = 1;    // ArduinoにおけるMode0
    
    SDI2Rbits.SDI2R = 0b0100; // B2にSDI2を割り当てる
    RPB1Rbits.RPB1R = 0b0100; // B1にSDO2を割り当てる
    SS2Rbits.SS2R = 0b0010;   // B0にSS2を割り当てる
    
    IPC9bits.SPI2IP = 4;      // SPI2割り込み優先度
    IPC9bits.SPI2IS = 3;      // SPI2副優先度
    
    IEC1bits.SPI2RXIE = 1;    // SPI2割り込みを有効にする
    IFS1bits.SPI2RXIF = 0;    // 割り込みフラグのクリア
    i = SPI2BUF;              // ダミーデータに格納することでバッファをクリア
    
    INTCONbits.MVEC = 1;      // マルチベクタ割り込みを有効にする
       
    __builtin_enable_interrupts();  // マイコンにおける割り込みの有効
    
    SPI2CONbits.ON = 1;       // SPI2を稼働

    while(1) ;
}
ここではデバッガでの確認でお茶を濁していますが、デバッガ情報の書き込み内容によっては、PIC内に余計な情報が挟まるためか、いつまでたっても正しい値を表示してくれないことがあります。そのときはクリーンビルドを試してみてください。
 | 2018年9月29日

SPIは通信相手(スレーブ)の選択やフレーム同期を行うSS/SSIと、ビットを知らせるSCK、データ入出力・SDO/SDIの4線から成り立つ通信方法で、PIC32MXの場合、最大で周辺クロックの1/2の速度(40MHzクロックなら20MHz)を出すことができます。

SCKのピンは固定(B14/B15)ですが、それ以外は複数のピンの中から選ぶことができます。

SDI1A1(0b00)B5(0b01)B1(0b10)B11(0b11)B8(0b100)
SDO1(0b11)A1B5B1B11
SS1A0(0b00)B3(0b01)B4(0b10)B15(0b11)B7(0b100)
SDI2A2(0b00)B6(0b01)A4(0b10)B13(0b11)B2(0b100)
SDO2(0b100)A1B5B1B11
SS2A3(0b00)B14(0b01)B0(0b10)B10(0b11)B9(0b100)
(26ピンモデルの場合。括弧内はレジスタに登録する数値)

SPIの方式は、同期信号が立ち上がったとき/立ち下がったとき、マスターかスレーブか、同期信号でビット列のグループを判断するかどうかなどで、信号の内容が少しずつ変わってくるため、SPIxCONレジスタで細かく指定する必要があります。

SRXISEL拡張バッファ使用時のSPI受信割り込みのタイミング
0-バッファの中身がすべて吐き出された
1-バッファが空でなくなった
2-バッファの半分以上が埋まった
3-バッファがすべて埋まった
STXISEL拡張バッファ使用時のSPI送信割り込みタイミング
0-送信が完了した
1-バッファが空になった
2-バッファが半分以上からになった
3-バッファに空きができた
DISSDISDIピンの使用(1-使用しない)
MSTENマスターモード(1-マスター:0-スレーブ)
CKP1でクロックがない状態をHighとみなす
SSEN1でスレーブ用にSSxピンを使用する
CKEデータの取得タイミング(0-クロックが開いている[Active]ときに取得:1-閉じている[Idle]ときに取得)
SMPマスターでのデータ受信タイミング(0-クロックの中央:1-クロックの終わり)
MODE1616bit単位で転送(0で8bit単位)
MODE3232bit単位で転送(0ならMODE16の値が適用される)
DISSDOSDOピンの使用(1-使用しない)
SIDLアイドル中の停止(1-停止する)
ONSPIの実行(1-実行する)
ENHBUF拡張バッファ(FIFOバッファ)の使用(1-使用する)
SPIFE同期エッジの位置(0-立ち下がり:1-立ち上がり)
MCLKSELマスタクロックを使用(0-周辺クロックを使用)
FRMCNTフレーム同期信号のタイミング(0-1データごと:1-2:2-4:3-8:4-16:5-32)
FRMSYPWフレーム同期信号の長さ(0-1クロック:1-MODE16/32で指定したビット数)
MSSENスレーブ選択の可否(1-スレーブを選択できるようにする)
FRMPOLフレーム同期信号の極性(0-Lowで開く:1-Highで開く)
FRMSYNCフレーム同期信号の向き(0-出力/マスター:1-入力/スレーブ)
FRMENフレームモードで使用(1-使用)

信号のタイミングは言葉では伝わりにくいので、データシートの内容も引用しておきます。
なおCKP/CKEの組み合わせとArduinoにおけるSPIモードの関係性は以下のようになります。
ModeCKPCKE
Mode001
Mode100
Mode211
Mode310
SPIの周波数は「PBCLK/(2*BRG+1)」で計算します。例えばPBCLKが40MHzの場合、BRG=0なら20MHz、BRG=15なら1.25MHzになります。

それではサンプルを作ってみましょう。今回はSPI通信でGPIOの制御ができるマイクロチップのI/Oエクステンダー「MCP23S17(日本語データシート/PDF)」を使用します。ピン数の少ない「MCP23S08」も基本的な操作方法は同じますが、MCP23S17の方がライブラリーなどの情報が比較的豊富です。

ArduinoではSIはMOSI(No.11)、SOはMISO(No.12)に接続しますが、PICの場合はSIはSDO、SOはSDIにつなぐ点に注意しましょう。またMCP23S17が受信できる信号速度は最大10MHzなので、送信時のクロックも適時落としておきます。I/Oの切り替えは、SSをオフにした間に、3バイトのデータから成る命令をSPI信号として送信することで実行されます。
#include <p32xxxx.h>
#include <sys/attribs.h>

// 外部の8MHz水晶発振器から40MHzのクロックを生成するための設定
#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

void send(int addr, int data)
{
    LATBbits.LATB0 = 0;
    int i, n;
    for(i = 0; i < 3; i++){
        switch(i){
        case 0: n = 0x40; break;
        case 1: n = addr; break;
        case 2: n = data; break;
        }
        SPI2BUF = n;
        while(SPI2STATbits.SPIBUSY) ; // 処理が完了するのを待つ
    }
    LATBbits.LATB0 = 1;
}

int main(void)
{
    int i, j = 0;
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
    
    LATBbits.LATB0 = 1;     // B0 = on   
    
    SPI2CONbits.ON = 0;     // 設定のためSPI1を無効に
    
    SPI2BRG = 15;           // SPIの周波数を2MHzに
    SPI2CONbits.MSTEN = 1;  // マスターモード
    SPI2CONbits.CKP = 0;    // ArduinoにおけるMode0
    SPI2CONbits.CKE = 1;    // ArduinoにおけるMode0
    
    SDI2Rbits.SDI2R = 0b0100; // B2にSDI2を割り当てる
    RPB1Rbits.RPB1R = 0b0100; // B1にSDO2を割り当てる
    SS2Rbits.SS2R = 0b0010;   // B0にSS2を割り当てる
    
    j = SPI2BUF;            // ダミーに出力してバッファをクリア
    
    SPI2CONbits.ON = 1;     // SPI2を稼働
    
    //  MCP23S17の設定
    // 1バイト目はDevice Opecode。0x40で書き込み命令、0x41で読み込み命令
    // 2バイト目は制御レジスタ
    // 3バイト目はパラメータ
    send(0x0A, 0x20);       // IOCON[0x0A]、シーケンシャルモードを無効[SEQOP = 1]
    send(0x00, 0x00);       // IODIRA[0x00]、ポートAのすべてのピンを出力モードに
    
    while(1){
        send(0x12, j);      // GPIOA[0x12]、すべてのピンのオン(1)・オフ(0)を切り替える
        j = ~j;             // 0x00<->0xFFに切り替え
        for(i = 0; i < 1000000; i++) ;
    }
}
SPI通信が成功したのを確認したら、LED(と抵抗)の数を増やし、jの値をビットシフトさせてキラキラさせてみましょう。


(おまけ)SPI信号のテストのために作ったArduinoスケッチ
#include<SPI.h>

#define SS 10

#define MCP_WRITE 0x40
#define MCP_READ 0x41

void sendBytes(int address, int data)
{
  digitalWrite(SS, LOW);
  SPI.transfer(MCP_WRITE);
  SPI.transfer(address);
  SPI.transfer(data);
  digitalWrite(SS, HIGH);
}

void setup()
{
  pinMode(SS, OUTPUT);
  digitalWrite(SS, HIGH);

  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2);

  sendBytes(0x0A, 0x20); // I/Oの初期設定[SEQOP:1(バイトモード)]
  sendBytes(0x00, 0x00); // ポートAをすべて出力にする
}

int p=0;
void loop() {
  sendBytes(0x12, 1<<p);
  p = (p == 0) ? 7 : 0;
  delay(200);
}
 | 2018年9月28日