[PIC18F27Q43] 16bits IOエキスパンダー MCP23017使い方と、I2C　"1バイト読込のコード" 解説

# *PIC18F27Q43*で、I2C16ビットIOエキスパンダーMCP23017を使うときの仕様と、I2C,1バイト読込のコードの要点をまとめました。
MCP23017のデータシートから、必要なところをピックアップしました。
![IMG_4605.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/251907dd-9bff-c7c6-7624-cb585b94040f.jpeg)

:::note info
・PCF8574( Texas Instruments)というI2Cデバイスの8ビットIOｴｷｽﾊﾟﾝﾀﾞｰが販売終了。
・代わりにこの１６ビットIOｴｷｽﾊﾟﾝﾀﾞｰMCP23017(I2C)が、入手しやすいのでその使い方をまとめておきます。
・Q43のI2Cモジュールの読み取りは、特殊です。MSSPモジュールだと、すべてのシーケンスをコード上で、把握できたのですが、**Q43の場合、バイトカウンタにあわせて、半自動化になったことが、** 難易度を高くしている感じがします。
・オシロ、または、ロジックアナライザーで波形を確認しながらの作業が、効率的だと思われます。
:::
まず、1バイト読込の時に必要なMCP23017の仕様をざっと確認しておきます。

# １．MCP23017 端子機能
:::note info 

![MCP23017ピン配.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/70974e1e-054a-bf83-bb09-a4bd2ae800fa.jpeg)


| ピン番号 | ピン名 | 機能 |
|-----------:|:------------:|:------------:|
|1～8| GPBx | GPIO Bポート |
|9|VDD|電源1.8v～5.5v|
|10|VSS|グランド|
|11|NC|未使用|
| 12 | SCK　| I2C クロック線 |
| 13 | SDA　| I2C データ線 |
|14|NC|未使用|
| 15 | A0　| I2C ｽﾚｰﾌﾞｱﾄﾞﾚｽ指定bit0 |
| 16 | A1　| I2C ｽﾚｰﾌﾞｱﾄﾞﾚｽ指定bit1 |
| 17 | A2　| I2C ｽﾚｰﾌﾞｱﾄﾞﾚｽ指定bit2 |
| 18 | /RESET　| リセット  |
| 19 | INTB　| 割込み出力  |
| 20 | INTB　| 割込み出力  |
|21～28| GPBx | GPIO Aポート |
:::

# 2．MCP23017　Client(Slave)addressの設定
:::note info
データシート DS20001952C-page 15より
![ControlByte.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/4997ca3d-62c8-7506-a543-fd2de574cf71.png)


**MCP23017のI2C Clientアドレスは、15,16,17ピン　A0,A1,A2をつかってユーザーが、定義できます。**

※今回は、ブレッドボード上で、A1,A2をプルアップし、A0だけをグランドにプルダウンしました。Clientアドレスは、R/Wビットを含めた、0x4C(0100 1100)として、設定しています。
:::

# ３．MCP23017 コントロールレジスタ

:::note info
データシートDS20001952C-page 16より
![コントロールレジスタ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/46864a51-fdff-c842-0618-93d11fefaa99.png)

MCP23017のレジスタアドレスは、２種類のアドレステーブルがあります。IOCONレジスタのBANKビットで選択します。

・IOCON.BANK=0 のとき、PORTAとPORTB関連レジスタは、交互に連続して配置されます。
・IOCON.BANK=1 のとき、0番から0Aまでが、PORTA関連レジスタに割り当てられて、0x10から0x1Aまでは、PORTB関連レジスタが割り当てられます。


・パワーオンリセット時、IOCON.BANKビットは０なので、レジスタは交互に並んでいます。

※今回は、MCP23017の初期化の際に、BANKを切り替えてBANK1のアドレステーブルで、ポートを操作しています。

:::

# ４．MCP23017 I2Cプロトコル
# write operation 書き込み
:::note info
![I2Cwrite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/b6acf68a-8f5c-ead5-2598-2881fdc21b48.png)
シーケンシャルにレジスタアクセスできるのですが、基本的に1バイト通信で事足りると思います。

**＜I2C１バイト書き込み＞**
**スタート→Clientアドレス+Wビット送信→Ack(Clinet)→レジスタアドレス送信→Ack(Clinet)→書き込みデータ送信→Ack(Clinet)→ストップ**

標準的なバイト書込みシーケンスです。
# read opreration 読み込み

![I2Cread.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/0a05c814-a604-e7fc-9daa-65b958184826.png)
今回は、設定を確認するだけなので、１バイト読込だけをコーディングしました。

**＜I2C　１バイト読込＞**
**スタート→Clientアドレス+Wビット送信→Ack(Clinet)→リスタート→Clientアドレス+Rビット送信→Ack(Clinet)→1バイトデータ受信→Nack(PICマイコン)→ストップ**

これも、標準的な1バイト読込のシーケンスです。
:::

MCP23017のI2Cは、標準的なプロトコルなので、たたき台にちょうどいいはずです。

# ５．PIC18F27Q43 I2C1モジュール　2バイト書込みと1バイト読込のコード
I2C1モジュール　2バイト書込みと、1バイト読込のコードです。MCP23017は、レジスタアドレスを指定して、書き込み、読込を行います。2バイト書込みと1バイト読込ができれば、MCP23017を制御できます。エラー処理は、全く考えていませんので、通信がずっこけたら、フリーズします。まずは、**正常系の動作が完成した後に、エラー処理を組み込んだほうが、理解が速いと思います。**
```C:Q_I2C1.h
#ifndef Q_I2C1_H
#define	Q_I2C1_H
#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include "Q_peripheral27Q43.h"

extern void I2C1_Init(void);
extern bool I2C1_b1Write(uint8_t _deviceAdd, uint8_t _data1);
extern uint8_t I2C1_b2Write(uint8_t _deviceAdd, uint8_t _data1, uint8_t _data2);
extern uint8_t I2C1_b1Read(uint8_t _deviceAdd,uint8_t _address);
#ifdef	__cplusplus
extern "C" {
#endif

#ifdef	__cplusplus
}
#endif

#endif	/* Q_I2C1_H */
```
```C:QI2C1.c
#include <pic18f27q43.h>

#include "Q_I2C1.h"

uint8_t i2c1Cnt;
//*************************************************//
//** I2C1初期化
//*************************************************//
void I2C1_Init(void)
{
     //Set Clear Buffer Flag
    I2C1STAT1bits.CLRBF = 1;//I2Cバッファ、RXBF、TXBEビットクリア
    I2C1CON0bits.MODE=0b100;//Host mode, 7bits address
    I2C1CON1bits.ACKCNT=1;  //最終ACK確認で返す値0:ACK 1:NACK
    I2C1CON2bits.ABD=0;     //I2C1ADB1をアドレスバッファとして使用
    I2C1CON2bits.BFRET=0b00;//8clocksスタートになる準備ﾊﾟﾙｽ数
    I2C1CLK=0b0011;         //MFINTOSC500kHz//I2C用クロックソース
    I2C1PIR=0x00;           //I2C1専用割り込みフラグのクリア
    I2C1CON0bits.EN=1;      //I2C1モジュール有効
}
//*************************************************//
//** I2C1 1バイト送信
//*************************************************//
bool I2C1_b1Write(uint8_t _deviceAdd, uint8_t _data1)
{
    I2C1ADB1 = _deviceAdd;       //ｱﾄﾞﾚｽﾊﾞｯﾌｧにClientアドレスをセット
    I2C1CNT=1;                   //送信データ数をセット
    I2C1TXB=_data1;              //送信データを1バイトだけ、前もってセット
    I2C1CON0bits.S=1;            //Startコンディション
    while(!I2C1STAT0bits.BFRE);  //I2Cバスが解放されているか？
    while(!I2C1STAT1bits.TXBE);  //送信バッファが空か？（セットしたデータの送信完了）
    while(I2C1PIRbits.PC1IF==0); //Stopコンディションになったか？
    I2C1PIRbits.PC1IF=0;         //Stopフラグクリア
    while(I2C1STAT0bits.MMA);    //Host mode active を見て終了確認 0:not active 1:active
    //Set Clear Buffer Flag
    I2C1STAT1bits.CLRBF = 1;     //I2Cバッファ、RXBF,TXBEのクリア
    return true;
}

//*************************************************//
//** I2C1 2バイト送信
//*************************************************//
uint8_t I2C1_b2Write(uint8_t _deviceAdd, uint8_t _data1, uint8_t _data2)
{
    I2C1ADB1 = _deviceAdd;  //Clientデバイスアドレスセット
    I2C1CNT=2;              //送信データ数
    I2C1TXB=_data1;         //送信バッファに最初のデータをセット
    I2C1CON0bits.S=1;       //スタートコンディションセット
    
    while(!I2C1STAT0bits.BFRE)      //I2Cバスが解放されているか？
    {};
    while(I2C1STAT1bits.TXBE!=1)
    {};                             //最初のデータ送信完了？
    //while(!I2C1CON1bits.ACKSTAT);
    I2C1TXB=_data2;                 //次の送信データをセット
    while(I2C1STAT1bits.TXBE!=1);   //送信完了？
    while(I2C1PIRbits.PC1IF==0);    //ストップコンディション確定？
    I2C1PIRbits.PC1IF=0;            //ストップフラグクリア
    while(I2C1STAT0bits.MMA);       //Host mode Active確認 0:not Active 1:active
    //Set Clear Buffer Flag         
    I2C1STAT1bits.CLRBF = 1;        //I2C1バッファ、TXBE,RXBFクリア
    return true;
}

//*************************************************//
//** I2C1 1バイト受信
//*************************************************//
uint8_t I2C1_b1Read(uint8_t _deviceAdd,uint8_t _address)
{
    uint8_t ret;
    I2C1ADB1 = _deviceAdd;          //Clientアドレスセット
    I2C1CNT=1;                      //送信バイト数(この場合は、引数の_address)
    I2C1TXB=_address;               //送信バイトセット
    I2C1CON0bits.S=1;               //Startコンディションセット
    while(!I2C1STAT0bits.BFRE);     //I2Cバス解放？確認
    while(I2C1STAT1bits.TXBE!=1);   //送信バッファ 0:full 1:empty 送信完了確認
    //リスタート(本来は、送信ストップコンディションのところでリスタート。)
    I2C1CON0bits.RSEN=1;            //リスタート開始(I2C1CNT==0の時)
    while(!I2C1CON0bits.MDR);       //RSENセットで１になる。SCLは、Hold状態
    I2C1ADB1 = _deviceAdd|0x01;     //Clientｱﾄﾞﾚｽ＋R/Wビット
    I2C1CON1bits.ACKCNT=1;          //I2C1CNT==0でNack返信
    I2C1CNT=1;                      //読み出し数
    I2C1CON0bits.S=1;               //Startコンディションセット
    while(!I2C1STAT1bits.RXBF);     //読み込み完了待ち
    ret=I2C1RXB;                    //バッファから読み出し
    I2C1CON0bits.RSEN=0;            //リスタート解除
    I2C1PIR=0x00;                   //PIRクリア
    while(I2C1STAT0bits.MMA);       //HostmodeActime 0:not Active 1:active
    //Set Clear Buffer Flag
    I2C1STAT1bits.CLRBF = 1;        //I2C1バッファクリア
    return ret;
}
```
# ６．PIC18F27Q43の1バイト読込シーケンス

下のダイアグラムは、7bit Host Mode Receptionのタイミングチャートです。
リスタート後の、読取り動作のフラグのタイミングが、大体把握できます。ですが、このダイアグラムに載っていないビットフラグも使用しないと、動作しませんでした。
（データシートDS40002147E-page 670）
![7bitHostReceptionDiagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/60734d2d-c987-dd80-7e02-43390b9b7edb.png)
## 6.1 読込シーケンスはリスタートの設定が、重要です。
**リスタートの設定がキーポイントになってきます。** オシロやロジックアナライザーで波形を見ながら、デバッグコーディングして、コードの実行性を確かめながら、理解するのが一番です。自動化された部分の動きを理解するのに、データシートの説明だけでは、半自動化の動作を確認するのが難しくなります。

## 6.2 ここからは、自分が組んだI2C　1バイト読込の概略フロー解説です。
![7bitHostReadDiagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3693934/8c4b453b-b8a0-6900-18f5-5df8f940f3c4.png)
＜1バイト読取りコードの解説＞
0. アドレスバッファにClientアドレスをセット。バイトカウンタに送信データ数をセット。TX送信バッファに、MCP23017のレジスタアドレスをセットしておきます。

1. Sビットをセット。バスフリーなら、すぐにスタートコンディションから発行され、ClientアドレスとRWビットが送信されます。このときは、まだ書き込みモードなので、ACKは、Clientから返信されます。これで、ACKを受信したら、Clientアドレスの送信に成功です

2. MCP23017のレジスタアドレスが自動送信されます。読込先のレジスタアドレスです。書き込みモードなので、ClientからのAck返信を待ちます。ここでAckだったら、次は、リスタートして、指定データの読み込みを行います

3. 送信部分は完了したので、通常の送信だったらここでストップコンディションを発行して、トランザクションを終了するタイミングです。読取りデータをMCP23017から出力させたいので、このタイミングで、リスタートを開始します

4. コントロールレジスタ０のRSENビットをセットします。このとき、バスはまだ、リスタート状態になっていません。
    RSENビットをセットした時に、バイトカウンタI2C1CNTがゼロなので、**MDRビットがセットされます。**
     MDRビットがセットされると、Hostは、送信、受信バッファの処理が終わるまで、クロックラインをストレッチしたまま、つまりホールドしたまま、通信を中断しています。
    **MDRビットがセットされるまで、while文で待っています。これがないと、フリーズします。**
    データシートには、このタイミングで、MDRがセットされることは書かれていますが、このビットがセットするまで待つことは、明示されていない、とおもいます。この部分が、難解な感じがします

5. MDRビットがセットされて、通信が中断している間に、送信データ、ClientアドレスとRWビットをABD1レジスタにセット。バイトカウンタに読込数をセット。読込最終バイト時にHostは、Nackを返信したいので、ACKCNTビットに、Nackを返信するように指示します

6. 準備すべきデータセットが終わったら、スタートビットをセットします。ここで、MDRビットがクリアされて、クロックストレッチが、終了します

7. バスフリーなら、スタートコンディションを発行されます

8. Clientアドレスが送信され、AckがClientから返信されます

9. 次に８ビットは、先ほど指定したレジスタアドレスの格納データ値が、ClientからHostに送信されます。これも自動的に処理されて、RXBFビットがセットするまで、待つだけです

10. RXBFビットがセットされたら、受信完了なので、受信バッファからデータを取り出します

11. １バイト読込なので、ここで読込終了です。通常、読込シーケンスの最終バイトの後のAck確認では、HostからNackを返信するのがセオリです。これも、さきほど、前もって、ACKCNTビットで指定したNackが自動的に出力されます。

12. I2C1の割込みフラグを、クリアしておきます
13. MMAビットで、Hostモードがアクティブであるか、どうかチェックします
非アクティブなら、トランザクションは完了したので、I2C1の送受信バッファをクリアして、終了します。