2018年1月18日木曜日

PIC16F1829をI2Cスレーブとして動かし、Arduinoからの信号でLチカする方法


背景

PIC16F1829とはmicrochip社が出している、PICマイコンと呼ばれることがある組み込み向けプロセッサの一つです。
以前、Lチカに成功しました。

ubuntuに開発環境を作り、PIC16F1829でLチカする方法

このPICマイコンをI2Cスレーブとして動作させるのに苦労したので、やり方を共有します。
説明の例として、ArduinoからI2Cで信号をPICマイコンに送り、その情報に応じてLチカさせてみます。

全体像

  1. 使ったもの
  2. 配線
  3. PICマイコンのプログラム
  4. ProMicro(Arduino)のプログラム
  5. 動作確認
  6. まとめ
  7. 参考

ArduinoからPICマイコンにLEDの制御信号を送信し、PICマイコンでLEDをON OFFする装置を作ります。

使ったもの

  • PC
    OSとしてubuntu17.10 (Linux) をインストールしたもの使いました。
  • Arduino IDE
    Arduinoのプログラム開発環境です。
    ProMicroにプログラムを書き込めれば良いので、PlatformIOでも良いです。
    参考: Ubuntuに入れたArduino IDEをバージョンアップする方法
  • Pro Micro
    ブレッドボードに差し込める、Arduino互換機です。
    スイッチサイエンスaliexpressで買えます。
  • PIC16F1829
    今回使うPICマイコンです。
  • PICkit3
    PICマイコン用のプログラム書き込み装置です。
  • ブレッドボード
    はんだ付けせずに電子部品を接続するのに利用します。
  • ジャンパワイヤ
    回路の配線に利用します。
  • LED 赤・緑・青
    PICマイコンで光らせます。
  • 1k抵抗
    LEDの破壊防止のため、回路に組み込みます。
  • 10k抵抗
    PICkit3の信号線のプルアップ(電圧を上げること)に利用します。

配線

このように配線しました。
PICマイコンのプログラムを期待通りに書き込めれば、PICkit3が無くても回路は動作します。

PICkit <-> PIC16F1829 <-> ProMicro(Arduino)



実際に組むとこうなりました。



PICマイコンのプログラム

0x10のアドレスでI2Cのスレーブデバイスとして動作し、受け取った情報に応じてLEDの光らせ方を変えるプログラムです。
長いですが、部分的に紹介すると必要な情報が漏れる可能性があるので、全部貼りつけます。
main.c
#include <xc.h>
#include <pic16f1829.h>
#include <stdint.h>
#include "i2c_slave.h"
#define I2C_ADDRESS 0x10
#define _XTAL_FREQ 16000000
#define LED_ON 1
#define LED_OFF 0
#pragma config FOSC=INTOSC, WDTE=OFF, PWRTE=OFF, MCLRE=OFF, CP=OFF, CPD=OFF, BOREN=ON, CLKOUTEN=OFF, IESO=OFF, FCMEN=OFF
#pragma config WRT=OFF, PLLEN=OFF, STVREN=OFF, LVP=OFF

unsigned char r = LED_OFF, g = LED_OFF, b = LED_OFF;

void ledR(int ledState) {
    if (ledState == LED_OFF) {
        r = LED_OFF;
        LATCbits.LATC5 = 0;
    } else {
        r = LED_ON;
        LATCbits.LATC5 = 1;
    }
}

void ledG(int ledState) {
    if (ledState == LED_OFF) {
        g = LED_OFF;
        LATCbits.LATC3 = 0;
    } else {
        g = LED_ON;
        LATCbits.LATC3 = 1;
    }
}

void ledB(int ledState) {
    if (ledState == LED_OFF) {
        b = LED_OFF;
        LATCbits.LATC6 = 0;
    } else {
        b = LED_ON;
        LATCbits.LATC6 = 1;
    }
}

void ledRGB(unsigned char data) {
    unsigned char portC = 0;
    // red
    if ((data & 0b0100) != 0) {
        portC = portC | 0b00100000;
        r = LED_ON;
    } else {
        r = LED_OFF;
    }
    // green
    if ((data & 0b0010) != 0) {
        portC = portC | 0b0001000;
        g = LED_ON;
    } else {
        g = LED_OFF;
    }
    // blue
    if ((data & 0b0001) != 0) {
        portC = portC | 0b01000000;
        b = LED_ON;
    } else {
        b = LED_OFF;
    }
    LATC = portC;
}

void onI2CReceiveCallback(unsigned char address, unsigned char data) {
    switch (address) {
        case 0x00:
            ledRGB(data);
            break;
        case 0x01:
            ledR(data);
            break;
        case 0x02:
            ledG(data);
            break;
        case 0x03:
            ledB(data);
            break;
        default:
            break;
    }
}

void setI2CWriteCharCallback(unsigned char address) {
    switch (address) {
        case 0x00:
        {
            unsigned char data = 0;
            if (r != LED_OFF) data |= 0b0100;
            if (g != LED_OFF) data |= 0b0010;
            if (b != LED_OFF) data |= 0b0001;
            I2CWriteChar = data;
            return;
        }
        case 0x01:
            I2CWriteChar = r;
            return;
        case 0x02:
            I2CWriteChar = g;
            return;
        case 0x03:
            I2CWriteChar = b;
            return;
        default:
            I2CWriteChar = 0;
            return;
    }
}

void main(void) {
    OSCCONbits.IRCF = 0b1111; // 16MHz
    OSCCONbits.SCS = 0b11; // Use internal oscillator as system clock
    TRISC = 0x00;
    LATC = 0x00;

    // Start as I2C slave
    setupI2CSlave(I2C_ADDRESS);

    while(1) {}
}


i2c_slave.h
#ifndef XC_HEADER_TEMPLATE_H
#define XC_HEADER_TEMPLATE_H

#include <xc.h> // include processor files - each processor file is guarded.

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#define RXBUFFER_SIZE 255

void setupI2CSlave(short address);
void __interrupt() I2Cinterrupt();

void onI2CReceiveCallback(unsigned char address, unsigned char data);
void setI2CWriteCharCallback(unsigned char address);
unsigned char I2CWriteChar;

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* XC_HEADER_TEMPLATE_H */


i2c_slave.c
#include "i2c_slave.h"

void setupI2CSlave(short address) {
// start I2C(1) setup
    SSP1CON2 = 0b00000001;  // SEN is set to enable clock stretching
    SSP1CON3 = 0x00;  // remove this line if use for PIC18F27J53
    SSP1ADD = address << 1; // 7-bit address is stored in the 7 MSB's of the SSP1ADD register**********
    SSP1STAT = 0x00;
    SSP1CON1 = 0b00110110;
//      B7: WCOL = 0;           // Clear write collisions
//      B6: SSPOV = 0;          // Clear receive overflow indicator
//      B5: SSPEN = 1;          // Enable SSP and configures SDA & SCL pins
//      B4: CKP = 1;            // Releases clock stretching
//      B<3:0>: SSPM = 0b0110;  // 7-bit addressing slave mode (Start and Stop bit interrupts disnabled)
                                // 000...0101   SPI mode
                                // 0110         I2C 7bit address
                                // 0111         I2C 10bit address
                                // 1000         I2C master
    /* Enable interrupts */
    SSP1IF  = 0; // Clear MSSP interrupt flag
    SSP1IE  = 1; // I2C interrupt enable
//end I2C(1) setup----------------------------------------------------------------

// Common Interrupt
    PEIE    = 1; // Enable Peripheral interrupts
    GIE     = 1; // Enable global interrupts
}

void I2CWrite(unsigned char data) {
    while(SSP1STATbits.BF);      // Wait while buffer is full
    do {
        SSP1CON1bits.WCOL = 0;   // Clear write collision flag
        SSP1BUF = data;
    } while (SSP1CON1bits.WCOL); // Do until write collision flag is clear
    if (SSP1CON2bits.SEN)
        SSP1CON1bits.CKP = 1;    // Release the SCL line
}

volatile unsigned char RXBufferIndex = 0;

#define STATE1 0b00001001 // 0x09 master write last was address
#define STATE2 0b00101001 // 0x29 master write last was data
#define STATE3 0b00001101 // 0x0d master read last was address
#define STATE4 0b00101100 // 0x2c master write last was data
#define STATE5 0b00101000 // 0x28

void countUpRXBufferIndex() {
    RXBufferIndex ++;
    if (RXBufferIndex >= RXBUFFER_SIZE)
        RXBufferIndex = 0;
}

void checkStateAndManageI2c() {
    static char DAStatus = 0;
    unsigned char i2cStatus, value;
    i2cStatus = SSP1STAT;
    i2cStatus = (i2cStatus & 0b00101101); // Mask out unimportant bits
    // _, _, D/A, _, S, R/W, _, BF
    switch (i2cStatus) {
        case STATE1:
            value = SSP1BUF;   // Read buffer, clear BF
            RXBufferIndex = 0; // Clear index
            DAStatus = 1;      // Next call is address inside memory

            if (SSP1CON1bits.SSPOV)
                SSP1CON1bits.SSPOV = 0; // Clear receive overflow indicator
            if (SSP1CON2bits.SEN)
                SSP1CON1bits.CKP = 1;   // Release the SCL line
            break;

        case STATE2:
            value = SSP1BUF; // Read buffer, clear BF

            if (DAStatus == 1) {
                RXBufferIndex = value;
                if (RXBufferIndex >= RXBUFFER_SIZE)
                    RXBufferIndex = 0;
                DAStatus = 2;
            } else {
                onI2CReceiveCallback(RXBufferIndex, value);
                countUpRXBufferIndex();
            }

            if (SSP1CON2bits.SEN)
                SSP1CON1bits.CKP = 1; // Release the SCL line
            break;

        case STATE3:
            value = SSP1BUF; // Dummy read

            setI2CWriteCharCallback(RXBufferIndex);
            I2CWrite(I2CWriteChar);
            countUpRXBufferIndex();
            break;

        case STATE4:
            setI2CWriteCharCallback(RXBufferIndex);
            I2CWrite(I2CWriteChar);
            countUpRXBufferIndex();
            break;

        case STATE5:
            break;
        default:
            if (SSP1CON2bits.SEN)
                SSP1CON1bits.CKP = 1; // Release the SCL line
            break;
    } // End switch (i2cStatus)
}

void __interrupt() I2Cinterrupt() {
    if (SSP1IF) {
        SSP1IF = 0; // Clear interrupt flag
        checkStateAndManageI2c();
    }
}

上記のi2c_slaveをPIC18F27J53で利用する場合は「SP1CON3 = 0x00;」の行を削除してください。(その行があると、ビルドできるものの動きません。)

このプログラムを設定するためのMPLABのプロジェクトやファイルの作り方は、こちらを参考になると思います。

ubuntuに開発環境を作り、PIC16F1829でLチカする方法

ところどころ解説します。


#define _XTAL_FREQ 16000000
#pragma config FOSC = INTOSC // Use internal oscillator

void main(void) {
    OSCCONbits.IRCF = 0b1111; // 16MHz
    OSCCONbits.SCS = 0b11; // Use internal oscillator as system clock

}
PIC16F1829は何もしなければ500kHzで動作しますが、I2Cのプログラムを動かす場合は、500kHzではI2C通信で2byte目以後が取得できなかったため、16MHzにしました。

ledRGB(data);
ledR(data);
ledG(data);
ledB(data);
ビットごとにLEDを接続したGPIOを制御する関数と、ビットを束ねたバイト単位でLEDを制御する関数をつくりました。

void onI2CReceiveCallback(unsigned char address, unsigned char data) {
    ..
}

void setI2CWriteCharCallback(unsigned char address) {
    unsigned char data;
    ..
    I2CWriteChar = data;
}
コールバックとして設定した関数がi2cのreadとwriteでそれぞれ呼ばれます。
writeのコールバックでは、writeしたいbyteをI2CWriteCharに書き込んでいます。

picマイコンのプログラムは、リポジトリの下記のディレクトリにまとめています。

arduino_and_pic_i2c/pic_i2c_slave/

ProMicro(Arduino)のプログラム

PICマイコンとi2c通信をおこない、LEDの光らせ方を変えつつ、PICマイコンが保持するLEDの状態を取得するプログラムです。
arduino_i2c_master.ino
#include <Wire.h>

#define I2C_SLAVE_ADDRESS 0x10
#define RGB_ADDRESS 0x00
#define R_ADDRESS   0x01
#define G_ADDRESS   0x02
#define B_ADDRESS   0x03

void setup() {
  Wire.begin();
  Serial.begin(115200);
  delay(500);
}

uint8_t writeByte(char deviceAddress, char bufferAddress, char data) {
  Wire.beginTransmission(deviceAddress);
  Wire.write(bufferAddress);
  Wire.write(data);
  return Wire.endTransmission();
}

uint8_t read3bytesState() {
  Wire.beginTransmission(I2C_SLAVE_ADDRESS);
  Wire.write(R_ADDRESS);
  uint8_t result = Wire.endTransmission();
  if (result != 0) return result;
  Wire.requestFrom(I2C_SLAVE_ADDRESS, 3);
  delay(10);
  Serial.print("read:");
  while (Wire.available()) {
    Serial.print(" ");
    Serial.print(Wire.read(), HEX);
  }
  Serial.println("");
  return 0;
}

void read1byteState() {
  Wire.beginTransmission(I2C_SLAVE_ADDRESS);
  Wire.write(RGB_ADDRESS);
  Wire.endTransmission();
  Wire.requestFrom(I2C_SLAVE_ADDRESS, 1);
  delay(10);
  Serial.print("read: ");
  while (Wire.available()) {
    Serial.print(Wire.read(), BIN);
  }
  Serial.println("");
}

void writeAndReadResults(char deviceAddress, char bufferAddress, char data) {
  if (writeByte(deviceAddress, bufferAddress, data) != 0) {
    Serial.println("Failed to write");
  } else {
    read3bytesState();
    read1byteState();
  }
}

void loop() {
  Serial.println("turn R on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, R_ADDRESS, 1);
  delay(1000);

  Serial.println("turn G on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, G_ADDRESS, 1);
  delay(1000);

  Serial.println("turn B on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, B_ADDRESS, 1);
  delay(1000);

  Serial.println("turn R off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, R_ADDRESS, 0);
  delay(1000);

  Serial.println("turn G off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, G_ADDRESS, 0);
  delay(1000);

  Serial.println("turn B off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, B_ADDRESS, 0);
  delay(1000);

  Serial.println("R:off,  G:off,  B:on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0b001);
  delay(1000);

  Serial.println("R:off, G:on,  B:on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0b011);
  delay(1000);

  Serial.println("R:on,  G:on,  B:on");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0b111);
  delay(1000);

  Serial.println("R:on, G:on,  B:off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0b110);
  delay(1000);

  Serial.println("R:on, G:off, B:off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0b100);
  delay(1000);

  Serial.println("R:off, G:off, B:off");
  writeAndReadResults(I2C_SLAVE_ADDRESS, RGB_ADDRESS, 0);
  delay(1000);
}


このような流れでLEDの制御信号をPICマイコンに送信するプログラムです。
  1. 赤を点灯
  2. 緑を点灯
  3. 青を点灯
  4. 赤を消灯
  5. 緑を消灯
  6. 青を消灯
  7. 青を点灯
  8. 緑を点灯
  9. 赤を点灯
  10. 青を消灯
  11. 緑を消灯
  12. 赤を消灯
  13. 先頭の信号に戻る

PICマイコンのプログラムと同じように、リポジトリでも公開しています。
リポジトリで管理するプログラムはPlatformIOようにcppファイルになっていますが、内容をコピペしてもらえばArduinoIDEでも動きます。

arduino_and_pic_i2c/arduino_i2c_master/src/main.cpp

動作確認

PICマイコンとArduinoにそれぞれプログラムを書き込み、動かしてみました。


期待通りに動きました。

まとめ

PICマイコンをスレーブとして動作させ、Arduinoからの信号によってLEDを変化させられました。

この情報が何かの役に立てば嬉しいです。

参考

今回の記事で作成したプログラムです。
asukiaaa/arduino_and_pic_i2c

PICマイコンのプログラムの参考にしました
PICをI2Cスレーブとする
PIC16LF1503 as I2C slave in XC8

記事には記述が出てきませんが、下記の動画を参考にしてMPLABのデバッガを利用しました。
プログラムがうまく動かず、何か情報が欲しい場合に役に立つと思います。
Microchip Debugging

今回利用したPICマイコンのデータシートです。
PIC16(L)F1825/9 Datasheet

今回利用したマイコンの製品ページです。
PIC16F1829

更新履歴

 2019.07.06
mplabxをv5.20、xc8をv2.05にすると下記の不具合があったので、それぞれ修正しました。

microchipのサンプルプログラム(製品ページのDocumentsタブのCode Examples)に合わせて、マイコンのpragma configの追加と、PORTCではなくLATCを利用するように変更を行いました。

以前は「PORTCbits.RC5」でビット操作すると周辺のビットがおかしくなることがありましたが、PORTCではなくLATCを使うようになったためか、xc8をv2にしたためか、原因を調査していませんが、おかしくならなくなったため注意書きを削除しました。

gifアニメを期待通りに動く様子に変更しました。

参考にマイコンの製品ページを追加しました。

2021.11.08
PIC18F27J53でi2c_slave.cを利用する場合は「SP1CON3 = 0x00;」を削除する注意書きを追加しました。

0 件のコメント :