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する装置を作ります。

使ったもの


配線

このように配線しました。
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 WDTE = OFF
#pragma config PWRTE = OFF
#pragma config CP = OFF
#pragma config FOSC = INTOSC // Use internal oscillator

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

// Unstable
void ledR(int ledState) {
    if (ledState == LED_OFF) {
        r = LED_OFF;
        PORTCbits.RC5 = 0;
    } else {
        r = LED_ON;
        PORTCbits.RC5 = 1;
    }
}

// Unstable
void ledG(int ledState) {
    if (ledState == LED_OFF) {
        g = LED_OFF;
        PORTCbits.RC3 = 0;
    } else {
        g = LED_ON;
        PORTCbits.RC3 = 1;
    }
}

// Unstable
void ledB(int ledState) {
    if (ledState == LED_OFF) {
        b = LED_OFF;
        PORTCbits.RC6 = 0;
    } else {
        b = LED_ON;
        PORTCbits.RC6 = 1;
    }
}

// Stable: recommended to use this
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;
    }
    PORTC = portC;
}

void onI2CReceive(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 setI2CWriteChar(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;
    PORTC = 0x00;

    // Set callbacks
    onI2CReceiveCallback = onI2CReceive;
    setI2CWriteCharCallback = setI2CWriteChar;

    // 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();

int (*onI2CReceiveCallback)(unsigned char address, unsigned char data);
int (*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;
    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();
    }
}

このプログラムを設定するための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 onI2CReceive(unsigned char address, unsigned char data) {
    ..
}

void setI2CWriteChar(unsigned char address) {
    unsigned char data;
    ..
    I2CWriteChar = data;
}

void main(void) {
    onI2CReceiveCallback = onI2CReceive;
    setI2CWriteCharCallback = setI2CWriteChar;
}
コールバックとして設定した関数が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);
}

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

void read3bytesState() {
  Wire.beginTransmission(I2C_SLAVE_ADDRESS);
  Wire.write(R_ADDRESS);
  Wire.endTransmission();
  Wire.requestFrom(I2C_SLAVE_ADDRESS, 3);
  delay(10);
  Serial.print("read: ");
  while (Wire.available()) {
    Serial.print(Wire.read(), HEX);
    Serial.print(" ");
  }
  Serial.println("");
}

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 loop() {
  Serial.println("turn R on");
  writeByte(I2C_SLAVE_ADDRESS, R_ADDRESS, 1);
  read3bytesState();
  read1byteState();
  delay(1000);

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

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

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

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

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

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

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

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

PICマイコンのプログラムと同じように、リポジトリでも公開しています。

arduino_and_pic_i2c/arduino_i2c_master/arduino_i2c_master.ino

動作確認

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


ん?何かおかしい。

表にするとこうです。
信号実際の動作取得した内部状態
赤を点灯期待通り100: 期待通り
緑を点灯期待通り110: 期待通り
青を点灯青の点灯と共に、緑が消える111: 光り方とは合わないが、信号の通り
赤を消灯赤の消灯と共に青も消える011: 光り方とは合わないが、信号の通り
赤緑青を点灯期待通り111: 期待通り
赤を消灯、青緑を点灯期待通り011: 期待通り
赤緑を消灯、青を点灯期待通り001: 期待通り
赤緑青を消灯期待通り000: 期待通り

どうやら、「PORTCbits.RC3」など、一つのビットの情報を書き換える機能を使うと、上記のmain.cの書き方では、書き換え対象以外のGPIOが変化することがあるようです。
ループしても同じように変化するので規則性はあると思いますが、
unsigned char data;
PORTC = data;
のように、PORTのビットを全て同時に変更する方が、期待通りのふるまいをさせやすいと思いました。

まとめ

PICマイコンをスレーブとしてどうささせて、Arduinoからの信号によってLEDを変化させられました。
PICマイコンの「PORTCbits.RC3」などの特定のビットを書き換える機能は、他のビットの値も変えてしまうようなので、注意が必要そうです。

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

参考

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

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

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

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

0 件のコメント :

コメントを投稿