上位モデルのPIC32MZを除き、PIC32シリーズにはフラッシュメモリーは搭載されていませんが、本来プログラムデータを書き込む領域にアクセスすることで、EEPROMのようにデータを記録したり読み出したりすることができます。

ただし、本来は動的にプログラムを更新するための機能なので、書き込める上限は1000回程度(一般的なフラッシュメモリは10万回以上)であったり、アクセスしている間はほかの処理ができないなど制限が多いので、頻繁に更新する必要があるのなら外部EEPROMやSPIによるSDカードアクセスを検討した方が良いでしょう。

プログラム領域に書き込めるということは、実行プログラムを容易に破壊できるということです。そのため、安易に更新できないようにするため、実行前にNVMKEYレジスタに特定の値を書き込むことでロックを解除する必要があります。

それさえわきまえれば、プログラムの実装は簡単です。
#define APP_WRITE_ADDRESS (uint32_t)(0x9D000000 + 0x3F00)

int WriteToProgramMemory()
{
    // 割り込み処理を停止して、以前の状態をieに格納
    int ie = __builtin_disable_interrupts();

    // 書き込みモードの指定
    // 0001 - 4バイト単位で書き込む
    // 0011 - 512バイト単位で書き込む(指定アドレスは512の倍数)
    // 0100 - 4KB単位で消去
    // 0101 - プログラム領域をすべて消去(ブート領域でのみ実行可)
    NVMCONbits.NVMOP = 0b0001; // WordWrite

    // 書き込みの許可
    NVMCONbits.WREN = 1;

    // 書き込み先の物理アドレス
    NVMADDR = KVA_TO_PA(APP_WRITE_ADDRESS);
    // 書き込みたい値
    NVMDATA = 0x12345678;
    
    // ロックを解除するためのキーを登録
    NVMKEY = 0xAA996655;
    NVMKEY = 0x556699AA;

    // 書き込みの実行。「NVMCONbits.WR = 1」にしただけでは失敗するので注意。
    NVMCONSET = 0x8000;
    // 書き込まれてフラグがリセットされるのを待つ
    while (NVMCONbits.WR);
    
    // 書き込みを禁止する
    NVMCONbits.WREN = 0;

    // 割り込み処理の状態を以前に戻す
    if (ie & 0x1) __builtin_enable_interrupts();

    // NVMERR、LVDERRのエラーフラグがあれば0以外を返す
    return (NVMCON & 0x3000);
}

uint32_t ReadWroteData()
{
    // 対象のアドレスに直接アクセスすれば、書き込んだ値を取得できる
    return *(uint32_t*)APP_WRITE_ADDRESS;
}
 | 2019年4月22日

ハードウェアを開発している本人であればなんら問題はありませんが、一般の人に「配布したハードウェアにバグがあったのでプログラムライターを購入してデータを書き換えてください」というわけにはいきません。将来的な不具合に対処するためには、そのハードウェア単体でファームウェアの更新を完結させる必要があります。

PIC32MXのフレームワーク「MPLAB Harmony」にはUSBブートローダープログラムが標準で用意されており、あらかじめこのブートローダーをマイコンに書き込んでおくことで、HID USB機器をソフトウェアレベルで制御できるパソコンを介してプログラムの更新に対応することができます。今回はHarmonyフレームワークを使ったブートローダーの活用例を紹介します。

まずは、ブートローダープログラムを作成してみましょう。統合開発環境のMPLAB X IDEを起動してHarmonyフレームワークの新規プロジェクト(hid_bootloader)を作成したら、MPLAB Harmony Configuratorにて「use Bootloader Library?」にチェックを入れます。「Bootloader Type」に「USB_DEVICE」を指定すると、USB対応のPIC32MX(ここではPIC32MX230F064Bを使用)だけでファームウェアの更新が実装できます。
つづいてHID USBデバイスの定義を指定します。USBの送信・受信と2つの領域が必要なので「Number of Endpoints Used」には「2」を入力し、「Function 1→Device Class」には「HID」を選択します。基本的にVendor ID/Product IDは任意の値でかまいませんが、画像ではマイクロチップのブートローダーユーティリティの初期値(0x04D8/0x003C)を書き込んできます。また、開発ボードは使用していないので「Use BSP?」のチェックは外しておきます。
ブートローダーにおけるクロックダイアグラムの設定は、後で追加・更新するプログラムにも適用されるので、ファームウェアの挙動を前提にした値を指定しましょう。
続いてブートローダー時におけるピンの設定を行います。この例ではRB4にタクトスイッチを、RB1にLEDを接続します。
ブートローダーコードを生成したら、プログラムに若干手を加えます。この状態でコンパイルするとタイマー関連のエラーが発生しますが、該当するコードを削除しても問題ありません。
APP_DATA appData;

int APP_Bootloader_ForceEvent(void)
{
    /* Check the switch press to trigger bootloader */
    if (BOOTStateGet()) return (1);

    /* Check the trigger memory location and return true/false. */
    if (*(uint32_t *)APP_RESET_ADDRESS == 0xFFFFFFFF) return (1);
    
    return (0);
}

void APP_Initialize ( void )
{
    appData.state = APP_STATE_INIT;

    // Register the bootloader callbacks
    BOOTLOADER_ForceBootloadRegister(APP_Bootloader_ForceEvent);
}

void APP_Tasks ( void )
{
    switch ( appData.state ){
        case APP_STATE_INIT:
        {
            bool appInitialized = true;             
            if (appInitialized) appData.state = APP_STATE_SERVICE_TASKS;
            break;
        }

        case APP_STATE_SERVICE_TASKS:
        {
            static uint32_t cntr = 0;
            // Blink the LED
            if (cntr++ == 100000) {
                LEDToggle();
                cntr = 0;
            }       
            break;
        }
    }
}
プログラム実行時に「APP_Bootloader_ForceEvent」で登録した関数が呼び出され、この時点でタクトスイッチが押されている(BOOTStateGet() == true)場合は1を返すことでブートローダープログラムを実行し続けます。スイッチが押されておらず、かつファームウェアが見つかった場合は0を返して、ファームウェアプログラムに移行します。

ブートローダープログラムが実行中であれば、ここの「APP_Tasks」が呼び出されるため、RB0に接続したLEDが点滅します。

プロジェクトをビルドして「hid_bootloader.X.production.hex」が生成されたら、これをマイコンに書き込みます。開発環境に付属している「MPLAB X IPE」を起動したら、「Hex File」に先ほどのコンパイル済みバイナリファイルを指定し、プログラムライター(PicKit3など)経由で書き込みます。
正常に書き込まれれば、ブートローダープログラムが起動し、「D+/D-」ピンに接続したUSBケーブルとそれに繋がっているパソコン間でHID USBデバイスが認識されます。

ブートローダーを書き込んだマイコンは一度わきに置いておき、次はファームウェアを開発します。MPLAB X IDEで新しいプロジェクト(BootAppTest)を作成して、任意のプログラム(ここではRB1に繋げたLEDを単純に点灯させている)を作成します。

注意点として、ブートローダープログラムのシステム設定と競合するのを避ける点が挙げられます。そのため「#pragma config」は記述しない、もしくは、Harmonyプロジェクトであれば「system_init.c」にある「#pragma config」の宣言をコメントアウトしておくように心がけましょう。
では、これをビルドした「BootAppTest.X.production.hex」ファイルをマイクロチップ社のサイトで配布されているブートローダーユーティリティを経由して書き込んでみます。
このブートローダーに付属しているファイルはWindows専用ですが、自前のUIを用意したかったり、Linuxやmacでも動くツールを提供したいのであれば、「オープンソースのブートローダーツール」を参考にすると良いでしょう。

マイコンつないでいるタクトスイッチを押しながら電源を投入するとブートローダーモードに移行し、RB0のLEDが点滅します。この状態でユーティリティソフトの「Connect」ボタンを押すとマイコンとのUSB通信が開始されます。「Load Hex File」で「BootAppTest.X.production.hex」を指定し、「Erase-Program-Verify」ボタンを押すと、ファームウェアが更新されます。書き込みと整合性の検証が完了したら、「Run Application」ボタンを押してマイコンをリセットしてみましょう。


この映像ではファームウェアの更新により、2つのLEDが点滅するプログラムが1つのLEDのみが点滅するプログラムに入れ替わっています。
 | 2019年4月18日

Direct Memory AccessはCPUを介さずにメモリー間のデータを転送する方法です。メモリーにデータを蓄積している間でもルーチンプログラムを動かし続けることができるので、オーディオストリーミングやディスプレイ出力など、組み込み機器で滞りなくデータを送受信し続けたい場合には必須のテクニックです。PIC32シリーズのマイコンではチャンネル単位で処理を確保し、PIC32MXにおいては最大4つの転送(4チャネル)を同時に行えます。

DMAを使用するに当たって必要なメモリーに関連する情報は「転送元の物理アドレス」「転送元のサイズ」「転送先の物理アドレス」「転送先のサイズ」「転送するサイズの上限」「1回につき転送するサイズ(4バイト単位、最大256バイト)」で、チャネル1に割り当てる場合はそれぞれ「DCH1SSA」「DCH1SSIZ」「DCH1DSA」「DCH1DSIZ」「DCH1CSIZ」となります。周辺モジュールからだけではなく、SRAM同士での転送にも使用できます。ただし、指定するアドレスは、マクロなどを使って仮想アドレスから変換する必要があります。
#include <sys/kmem.h>

DCH1SSA = KVA_TO_PA((void*)&SPI1BUF); // DMA1転送元物理アドレス
DCH1SSIZ = 4;                         // DMA1転送元サイズ
DCH1DSA = KVA_TO_PA(active_frame);    // DMA1転送先物理アドレス
DCH1DSIZ = PCM_BUFFER_SIZE;           // DMA1転送先サイズ
DCH1CSIZ = 4;                         // 一度に転送するサイズ
転送を実行するタイミングは「CHSIRQ」に割り込みベクターの値を与えることで有効になります。例えば「DCH1ECONbits.CHSIRQ = _TIMER_1_IRQ」ならタイマーによる一定周期ごとに、「DCH1ECONbits.CHSIRQ = _SPI1_RX_IRQ;」なら、SPI1のデータ受信時に転送が実行されます。

DCHxININレジスタのビットを有効にすることで、それらの値に対応したDMA割り込みが発生します。「DCH1INTbits.CHBCIE = 1」であれば指定したサイズ分のデータ転送が完了したときに割り込まれるので、このタイミングで転送先のアドレスを参照すれば、転送済みであるすべてのデータを取得することができます。

ここで紹介するサンプルはSPIモジュールを使った、デジタルオーディオ信号形式のひとつであるPCM(I2Sと違い、データ信号が途切れないのが特徴)の転送方法です。終了割り込みが発生するたびに、転送先のアドレスを差し替えており、これによりダブルバッファ処理を実装しています。

SPIxBUFのサイズは32bitなので、16bitオーディオを送信する場合でも、バッファには32bitごとにデータを収納する必要がある、つまり、各バッファの上位16bitは詰めないで0x0000のままにしなくてはいけません。
#include <p32xxxx.h>
#include <sys/kmem.h>
#include <sys/attribs.h>
#include <stdio.h>

#pragma config FNOSC = FRCPLL
#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 ICESEL = ICS_PGx3
#pragma config DEBUG  = ON
#pragma config FWDTEN = OFF

#define PCM_BUFFER_SIZE 32

char frame_buffer0[PCM_BUFFER_SIZE];
char frame_buffer1[PCM_BUFFER_SIZE];
char *active_frame = frame_buffer0;

void __ISR(_DMA1_VECTOR, IPL5AUTO) OnDma1(void)
{     
    if(DCH1INTbits.CHBCIE == 1){
        active_frame = (active_frame == frame_buffer0) ? frame_buffer1 : frame_buffer0;
        DCH1DSA = KVA_TO_PA(active_frame);

        DCH1CONbits.CHEN = 1;   // DMA1を有効にする
        
        DCH1INTbits.CHBCIF = 0; // ブロック終了割り込みフラグのクリア
    }
    IFS1bits.DMA1IF = 0;        // DMA1割り込みフラグのクリア
}

void InitSpi()
{
    SPI2CONbits.ON = 0;
  
    SDI2Rbits.SDI2R = 0b0011;  // B13をSDI2に割り当て
    SS2Rbits.SS2R = 0b0001;    // B14をSS2に割り当て
    
    SPI2CONbits.MSTEN = 0;     // SPI2スレーブモード
    SPI2CONbits.SPIFE = 1;     // フレーム同期モード
    SPI2CONbits.MODE32 = (PCM_BUFFER_SIZE == 32) ? 1 : 0; // 32bitモード
    SPI2CON2bits.IGNROV = 1;   // オーバーフローエラーを無視する

    SPI2CON2bits.AUDEN = 1;     // オーディオプロトコルを使用
    SPI2CON2bits.AUDMOD = 0b11; // PCM/DSPモード

    SPI2CONbits.ON = 1;         // SPI2を有効にする
}

void InitDma()
{
    IEC1bits.DMA1IE = 0;   // 設定のためDMA1を一時停止させる
   
    DCH1CON = 0;
    DCH1ECON = 0;
    DCH1INT = 0;

    DCH1CONbits.CHPRI = 3;              // DMAチャネルの優先度(3)
    DCH1ECONbits.CHSIRQ = _SPI2_RX_IRQ; // DMAを開始する割り込み番号
    DCH1ECONbits.SIRQEN = 1;            // 割り込みによる転送を有効にする
    DCH1INTbits.CHBCIE = 1;             // DMA割り込みを有効にする
    
    DCH1CONbits.CHEN = 1;               // DMA1を使用する
    
    DCH1SSA = KVA_TO_PA((void*)&SPI2BUF);
    DCH1SSIZ = 4;
    DCH1DSA = KVA_TO_PA(active_frame);
    DCH1DSIZ = PCM_BUFFER_SIZE;
    DCH1CSIZ = 4;
    
    IFS1bits.DMA1IF = 0;   // 割り込みフラグのクリア
    IPC10bits.DMA1IP = 5;  // 割り込み優先度
    IPC10bits.DMA1IS = 3;  // 副割り込み優先度
    IEC1bits.DMA1IE = 1;   // DMA1割り込みを有効にする
   
    DMACONbits.ON = 1;     // DMAモジュールを有効にする
}

int main(void)
{
    ANSELB = 0x00; 
    TRISB = 0xFFFFFFFF;
    
    InitSpi();
    InitDma();
      
    INTCONbits.MVEC = 1;
       
    __builtin_enable_interrupts();
     
    while(1) ;
}
割り込みフラグ解除はDMA割り込みだけでなく、DMAブロック終了割り込みなどの、関連する割り込み情報のフラグもリセットしないと次の割り込みが来ないので気をつけましょう。
 | 2018年12月9日

通信方式として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日