PIC32MX+Harmony3:I2CとPWM

LEDをふわりと光らせるために、PWM(パルス幅変調)という方法を使います。作品「ステラノーヴァ」ではスピーカーから音が鳴ると同時に、星のまたたきのようなイメージでLEDも光るようになっています。マスラックスでは、いつもPICマイコンを使って、PWMのプログラムをしているのですが、今まで8bitや16bitが一般的だったマイコンが、より高性能な32bitへと移ってきているので、作品の回路をアップデートしました。PICの開発で何度もつまづくので、メモ代わりに書いておきます。こちらの基板のなかのプログラムです。


1. 今回つくるプログラム

全体構成としては、RaspberryPiをメインに使い、PICの回路でLEDを制御します。光の明るさを変える数値はRaspberryPiの中で「Pd(PureData)」を動作させ、OSC通信(OpenSoundControl)で数値を送ります。この数値は、これもまたRaspberryPiの中で動作する「NODE-RED」で受け、そのNODE-REDからPICが指示をもらってLEDの明るさを変えます。この指示は「I2C(アイスクエアードシー)」という通信ルールのもと、0〜8000までの数値を扱い、PWMでLEDの明るさとして反映させます。

memo

PdとPICを直接通信させることもできますが、PICとPdの起動順や、不意に接続が途切れたときのエラー処理や自動接続など、Pdだけで処理するには少し不安な要素が出てきます。そこで、Pdが得意なネットワーク通信で、RaspberryPiのNODE-REDとOSC通信し、NODE-REDがPICとI2C通信することにしています。少しまわりくどい方法かもしれませんが、安定して動作します。


2. I2Cの設定

「MPLAB Harmony3 Configurator(MHC)」の設定画面です。PIC32MX220F032BのI2Cは、ピン割り当てを変更できますが、自由に割り当てできるわけではないので、あらかじめ確認しておきます。今回は、RB9にSDA1(データ)、RB8にSCL1(クロック)を割り当て、内部プルアップを有効にしています。

SLAVEモードにし、アドレスは「0x54」になっています。アドレスは10進数にすると「84」です。


3. PWMの設定

PWMは「OCMP」を使います。OCMP2とOCMP5の2つを設定しましたが、基板の確認用LEDと本番用の外部につなぐLEDのためです。OCPM2とOCPM5の設定は同じでOKです。
「PWM mode on OCx Fault pin disabled」モードにし、「Timer2」をソースに設定しました。Compare ValueがPWMの解像度になりますが、「8000」としています。この数値はこのあとTMR2で設定するものと合わせる必要があります。


4. TMR2の設定

MCCではPWMするときに使うタイマーは自動的に割り当ててくれたので、MHCでもそうかと思ったら違いました。手作業で割り当てます。ポイントは「割り込みナシ」にすることと、「Timer Period」をOCPMの設定と合わせることです。Timer Periodを「0.2」とすると「Period Register」が「7,999」となりました。Period Registerを直接変更できると楽なのですが変更できないので、Timer Periodを少しずつ変更しました。0〜7,999までカウントするので、全部で8000カウントになります。この「8000」カウントと、OCPMのCompare Valueの「8000」を合わせることで、きれいにPWMすることができます。


5. コード生成と記述

MHCの「Genatrate」>「Genarate Code」を実行すると、コード一式が自動的に生成されます。今回はそのなかの「main.c」を変更していきます。

I2C通信で擬似的にEEPROMのように扱う、公式のサンプルを少し改変しています。擬似EEPROMの0番目と1番目に、NODE-REDから送られる「0〜8000」の値を2バイトで受信し、その2バイトをPICで0〜8000に戻して、PWMの値としています。擬似EEPROMは256バイト確保してあってほとんど使ってないので、もう少しコンパクトにできそうです。

/*******************************************************************************
  Main Source File

  Company:
    Microchip Technology Inc.

  File Name:
    main.c

  Summary:
    This file contains the "main" function for a project.

  Description:
    This file contains the "main" function for a project.  The
    "main" function calls the "SYS_Initialize" function to initialize the state
    machines of all modules in the system
 *******************************************************************************/

// *****************************************************************************
// *****************************************************************************
// Section: Included Files
// *****************************************************************************
// *****************************************************************************

#include <stddef.h>                     // Defines NULL
#include <stdbool.h>                    // Defines true
#include <stdlib.h>                     // Defines EXIT_FAILURE
#include <string.h>
#include "definitions.h"                // SYS function prototypes
#include "peripheral/i2c/slave/plib_i2c1_slave.h"

#define EEPROM_PAGE_SIZE_BYTES                  256
#define EEPROM_PAGE_SIZE_MASK                   0xFF
#define EEPROM_SIZE_BYTES                       256

typedef struct {
    /* currentAddrPtr - to allow for sequential read (from the current address) */
    uint16_t currentAddrPtr;
    /* addrIndex - used to copy 2 bytes of EEPROM memory address */
    uint8_t addrIndex;
} EEPROM_DATA;

EEPROM_DATA eepromData;

uint8_t EEPROM_EmulationBuffer[EEPROM_SIZE_BYTES] ={
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
    0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
    0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
    0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
    0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
    0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
    0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
    0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
    0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
    0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
    0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff

};

bool APP_I2C_SLAVE_Callback(I2C_SLAVE_TRANSFER_EVENT event, uintptr_t contextHandle) {
    bool isSuccess = true;

    switch (event) {
        case I2C_SLAVE_TRANSFER_EVENT_ADDR_MATCH:

            /* Reset the index. MSB address is sent first followed by LSB. */
            eepromData.addrIndex = 1;

            break;

        case I2C_SLAVE_TRANSFER_EVENT_RX_READY:
            /* Read the data sent by I2C Master */
            if (eepromData.addrIndex > 0) {
                ((uint8_t*) & eepromData.currentAddrPtr)[--eepromData.addrIndex] = I2C1_ReadByte();
            } else {
                EEPROM_EmulationBuffer[eepromData.currentAddrPtr++] = I2C1_ReadByte();

                /* If exceeding the page boundary, rollover to the start of the page */
                if ((eepromData.currentAddrPtr % EEPROM_PAGE_SIZE_BYTES) == 0) {
                    eepromData.currentAddrPtr -= EEPROM_PAGE_SIZE_BYTES;
                }
            }
            break;

        case I2C_SLAVE_TRANSFER_EVENT_TX_READY:

            /* Provide the EEPROM data requested by the I2C Master */
            I2C1_WriteByte(EEPROM_EmulationBuffer[eepromData.currentAddrPtr++]);
            if (eepromData.currentAddrPtr >= EEPROM_SIZE_BYTES) {
                eepromData.currentAddrPtr = 0;
            }
            break;

        default:
            break;
    }

    return isSuccess;
}

int main(void) {
    uint16_t led;

    /* Initialize all modules */
    SYS_Initialize(NULL);

    I2C1_CallbackRegister(APP_I2C_SLAVE_Callback, 0);

    OCMP2_CompareSecondaryValueSet(0);
    OCMP5_CompareSecondaryValueSet(0);
    OCMP2_Enable();
    OCMP5_Enable();
    TMR2_Start();

    while (true) {
        led = (EEPROM_EmulationBuffer[0] << 8) + EEPROM_EmulationBuffer[1];
        OCMP2_CompareSecondaryValueSet(led);
        OCMP5_CompareSecondaryValueSet(led);
    }

    /* Execution should not come here during normal operation */

    return ( EXIT_FAILURE);
}

/*******************************************************************************
 End of File
 */



6. NODE-REDからI2C通信

NODE-REDにはあらかじめ、OSC通信のノード「node-red-contrib-osc」と、I2C通信のノード「node-red-contrib-i2c」を追加しておきます。RaspberryPiは、raspi-configで「I2C」を使えるように設定しておきます。

また、ブラウザからGUIで操作できるよう「node-red-dashboard」も追加しました。

ポート8000から来る数値を、スライダーに入れます。その後の「convert to Signed char 2Bytes」ノードは、「0〜8000」まで変化する数値を、-128〜127までの2バイトのデータに変換する自作のスクリプトです。これは「i2c out」ノードが-128〜127までしか受け付けないためつくったものです。

[{“id”:”8cb02b4e.82a5f8″,”type”:”tab”,”label”:”フロー 1″,”disabled”:false,”info”:””},{“id”:”bde0d55f.5a5cb8″,”type”:”inject”,”z”:”8cb02b4e.82a5f8″,”name”:”scan”,”props”:[{“p”:”payload”}],”repeat”:””,”crontab”:””,”once”:false,”onceDelay”:0.1,”topic”:””,”payload”:”1″,”payloadType”:”num”,”x”:130,”y”:60,”wires”:[[“b1ec6e92.cff1f”]]},{“id”:”cbe51fad.8184a”,”type”:”debug”,”z”:”8cb02b4e.82a5f8″,”name”:””,”active”:false,”tosidebar”:true,”console”:false,”tostatus”:false,”complete”:”false”,”statusVal”:””,”statusType”:”auto”,”x”:630,”y”:60,”wires”:[]},{“id”:”101a2372.bb1eed”,”type”:”i2c out”,”z”:”8cb02b4e.82a5f8″,”name”:””,”busno”:”1″,”address”:”84″,”command”:”0″,”payload”:”payload”,”payloadType”:”msg”,”count”:”2″,”x”:390,”y”:240,”wires”:[[“267a3dd6.eb14b2”]]},{“id”:”b1ec6e92.cff1f”,”type”:”i2c scan”,”z”:”8cb02b4e.82a5f8″,”name”:””,”busno”:”1″,”x”:380,”y”:60,”wires”:[[“cbe51fad.8184a”],[]]},{“id”:”267a3dd6.eb14b2″,”type”:”debug”,”z”:”8cb02b4e.82a5f8″,”name”:””,”active”:false,”tosidebar”:true,”console”:false,”tostatus”:false,”complete”:”false”,”statusVal”:””,”statusType”:”auto”,”x”:630,”y”:240,”wires”:[]},{“id”:”91ce981d.460a18″,”type”:”osc”,”z”:”8cb02b4e.82a5f8″,”name”:”i2c”,”path”:”/i2c”,”metadata”:false,”x”:270,”y”:140,”wires”:[[“e361952.24e2968”]]},{“id”:”570de3a3.038dec”,”type”:”udp in”,”z”:”8cb02b4e.82a5f8″,”name”:””,”iface”:””,”port”:”8000″,”ipv”:”udp4″,”multicast”:”false”,”group”:””,”datatype”:”buffer”,”x”:120,”y”:140,”wires”:[[“91ce981d.460a18”]]},{“id”:”f1c57734.79a6b8″,”type”:”function”,”z”:”8cb02b4e.82a5f8″,”name”:”convert to Signed char 2Bytes”,”func”:”inlet = parseInt(msg.payload);\nmsb = (inlet >> 8) & 255;\nlsb = inlet & 255;\nbuff = [0,0];\n\nif(msb>=0 && msb<=127){\n buff[0] = msb & 127;\n}else if(msb>=128){\n buff[0] = msb – 256;\n}\nif(lsb>=0 && lsb<=127){\n buff[1] = lsb & 127;\n}else if(lsb>=128){\n buff[1] = lsb – 256;\n}\n\nmsg.payload = buff;\nreturn msg;”,”outputs”:1,”noerr”:0,”initialize”:””,”finalize”:””,”libs”:[],”x”:310,”y”:200,”wires”:[[“101a2372.bb1eed”]]},{“id”:”e361952.24e2968″,”type”:”ui_slider”,”z”:”8cb02b4e.82a5f8″,”name”:””,”label”:”LED”,”tooltip”:”0~8000″,”group”:”ccb11ff1.db2″,”order”:0,”width”:0,”height”:0,”passthru”:true,”outs”:”all”,”topic”:”topic”,”topicType”:”msg”,”min”:”0″,”max”:”8000″,”step”:1,”x”:390,”y”:140,”wires”:[[“f1c57734.79a6b8”]]},{“id”:”ccb11ff1.db2″,”type”:”ui_group”,”name”:”デフォルト”,”tab”:”2b753347.39755c”,”order”:1,”disp”:false,”width”:”6″,”collapse”:false},{“id”:”2b753347.39755c”,”type”:”ui_tab”,”name”:”STELLANOVA”,”icon”:”dashboard”,”disabled”:false,”hidden”:false}]