最終更新日
記事公開日

PIC12F1822とEEPROM(24LC256/24LC512)のシリアル通信でハマったこと

PIC12F1822を使い、シリアル通信でEEPROM(24LC256)から音声データを取り出す回路を作成しました。

今回は、そのときにハマったことを書いておきます。

回路図

プログラム

/*
 * 
 *  PIC12F1822 × EEPROM(24LS256)
 * シリアル通信で外部シリアルROMへのデータ書き込み
 *                               - 2021/02/08 -
 * 
 */

//ヘッダファイルの読み込み
//--------------------------------------------------------------
#include <xc.h>


// PIC12F1822 Configuration Bit Settings
//--------------------------------------------------------------
// CONFIG1
#pragma config FOSC = INTOSC    // 内部クロックを使用する
#pragma config WDTE = OFF       // フリーズしたときに強制リセットするか?(OFF:リセットしない)
#pragma config PWRTE = OFF      // 起動時、電源が安定するまで待つか?(OFF:待たない)
#pragma config MCLRE = OFF      // 外部からのリセット信号を受け付けるか?(OFF:受けつけない)
#pragma config CP = OFF         // プログラムの外部読み取りを許可するか?(OFF:許可)
#pragma config CPD = OFF        // データの外部読み取りを許可するか?(OFF:許可)
#pragma config BOREN = ON       // 電源が不安定になったとき動作を停止するか?(ON:停止する)
#pragma config CLKOUTEN = OFF   // 外部クロックを使用する場合の3番Pinの設定(OFF:3番Pinを入出力Pinに設定)
#pragma config IESO = OFF       // 外部クロックが安定するまで内部クロックを使用するか?(OFF:使用しない)
#pragma config FCMEN = OFF      // 外部クロックが止まったとき内部クロックを使用するか?(OFF:使用しない)
// CONFIG2
#pragma config WRT = OFF        // データの書き込み禁止エリアを設定するか?(OFF:設定しない)
#pragma config PLLEN = OFF      // 内部クロックを32MHz(8MHz×4倍)で使用するか? (OFF:使用しない)
#pragma config STVREN = OFF     // プログラムが暴走したときにリセットを行うか?(OFF:行わない)
#pragma config BORV = LO        // 電圧不足と判断してリセットされるときの電圧を設定(LO:低い電圧に設定)
#pragma config LVP = OFF        // PICKIT3以外のライターで書き込む場合に低電圧で書き込み可能にする(OFF:低電圧書き込み無効)
 
 
// クロック周波数指定
//--------------------------------------------------------------
// __delay_ms()関数はこの数値を元にwaitする
#define _XTAL_FREQ 16000000


// 割り込み関数のプロトタイプ宣言
//--------------------------------------------------------------
// C言語では使用する関数をあらかじめ宣言しておく必要がある
// void:データ型なし、unsigned char:0~255の整数)
void I2C_write_set(unsigned char addr1 ,unsigned char addr2 ,unsigned char send_data);
void I2C_read_set(unsigned char addr1 ,unsigned char addr2);
void I2C_start(void);
void I2C_send(unsigned char send_data);
void I2C_receive(void);
void I2C_ack(void);
void I2C_nack(void);
void I2C_stop(void);


// ============================================================
//
//                  メインプログラム
//
// ============================================================

/*              12F1822
 *                   ------        
 *             VDD -| 1      8 |- VSS
 *      RA5 → PWM -| 2      7 |- AN0/RA0
 *  AN3/RA4 → A/D -| 3      6 |- AN1/RA1/SCL → ROM(6Pin)
 *      RA3 →  IN -| 4      5 |- AN2/RA2/SDA → ROM(5Pin)
 *                   ------      
 */

void main(void) {
 
    //----------------------------------------------------------
    //               PICマイコン初期設定
    //----------------------------------------------------------
    
    // 内部クロック周波数
    // | 0 | x | x | x | x | 0 | 0 | 0 |
    //----------------------------------------------------------
    // OSCCON = 0b01011000;  // 1MHz
    // OSCCON = 0b01100000;  // 2MHz
    // OSCCON = 0b01101000;  // 4MHz
    OSCCON = 0b01110000;  // 8MHz
    //OSCCON = 0b01111000;     // 16MHzに設定
    
    // デジタル・アナログ設定(※RA3[4Pin]はデジタル固定)
    // | 0 | 0 | 0 | RA4 | RA3 | RA2 | RA1 | RA0 |
    //----------------------------------------------------------
    // ANSELA = 0b00000000; // 全てデジタルモード
    ANSELA = 0b00010000;    // RA4[3Pin]:アナログ、その他:デジタル
    
    // 入出力Pin設定(※RA3[4Pin]は入力固定)
    // | 0 | 0 | RA5 | RA4 | RA3 | RA2 | RA1 | RA0 |
    //----------------------------------------------------------
    TRISA  = 0b00011110;    // RA5:出力、RA4:入力、RA3:入力、RA2:入力、RA1:入力、RA0:出力
                            // I2Cを使う場合、RA1[6Pin]とRA2[5Pin]は必ず入力にする必要がある
    
    
    //----------------------------------------------------------
    //                   A/D変換設定
    //----------------------------------------------------------
    
    // アナログ入力チャネルの設定
    // | 0 | x | x | x | x | x | 0 | 1 |
    //----------------------------------------------------------
    // ADCON0 = 0b00000001; // RA0[7Pin]
    // ADCON0 = 0b00000101; // RA1[6Pin]
    // ADCON0 = 0b00001001; // RA2[5Pin]
    // ADCON0 = 0b00001101; // RA3{4Pin]
    ADCON0 = 0b00001101; // RA4[3Pin]
    
    // 出力フォーマット、クロック設定
    // | y | x | x | x | 0 | 0 | 0 | 0 |
    //----------------------------------------------------------
    // ADCON1 = 0b10100000; // 右詰め、Fosc/32
    ADCON1 = 0b01010000; // 左詰め、Fosc/16
    // ADCON1 = 0b00010000; // 左詰め、Fosc/8
    // ADCON1 = 0b01000000; // 左詰め、Fosc/4
    // ADCON1 = 0b00000000; // 左詰め、Fosc/2
    
    
    //----------------------------------------------------------
    //                    PWM設定
    //----------------------------------------------------------
    
    // PWA機能をRA5[2Pin]に設定
    //----------------------------------------------------------
    CCP1SEL = 1; 
    
    //PWM機能を有効
    //----------------------------------------------------------
    CCP1CON = 0b00001100;	
    
    // TMR2プリスケーラ値を設定
    // | 0 | 0 | 0 | 0 | 0 | 0 | x | x |
    //----------------------------------------------------------
    T2CON   = 0b00000000; // 1倍
    // T2CON   =    0b00011000; // 1倍
    // T2CON   = 0b00000001; // 4倍
    // T2CON   = 0b00000010; // 16倍
    // T2CON   = 0b00000011; // 64倍
    
    // PWMの周期を設定 
    //----------------------------------------------------------
    PR2 = 128;
    
    // PWMスタート
    //----------------------------------------------------------
    TMR2ON  = 1; 
    
    //----------------------------------------------------------
    //               シリアル通信(I2C)設定
    //----------------------------------------------------------
    
    // クロック信号速度
    // | x | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    //----------------------------------------------------------  
    SSP1STAT = 0b10000000;  // 標準モード(100kHz)
    //SSP1STAT = 0b00000000;  // 高速モード(400kHz)
    
    // マスター(制御する側) or スレーブ(制御される側)
    //----------------------------------------------------------
    SSP1CON1 = 0b00101000;  // マスターモードに設定
    
    // 通信速度設定
    //----------------------------------------------------------
    // クロック信号の速度(周波数) = PICマイコンの動作周波数 / ((SSP1ADDの値 + 1) x 4)
    // PICマイコンの動作周波数:16MHz、I2Cのクロック信号速度:400kHzに設定
    // 16000kHz / 400kHz x 4) - 1 = 16000/1600 - 1 = 10 - 1 = 9
    SSP1ADD = 6; // 声が低くなったので微調整
    //SSP1ADD = 19;
	 
    //----------------------------------------------------------
    //                 シリアル通信 動作テスト
    //----------------------------------------------------------
    
    /*
     *              24LC256
     *               ------        
     *   A0 → GND -| 1      8 |- VCC
     *   A1 → GND -| 2      7 |- 
     *   A2 → GND -| 3      6 |- SCL → PIC(6Pin)
     *  VSS → GND -| 4      5 |- SDA → PIC(5Pin)
     *               ------      
     */
    
    /* 
     * メモリ:256Kbit
     * 
     *      256000bits÷8bits = 32000Byte = 32kByte
     *      32×1024 = 32768bit 
     *      0~32767番地まで記憶可能
     * 
     *          ・ 10進数 → 最大「32767」
     *          ・ 16進数 → 最大「7FFF」
     *          ・ 2進数  → 最大「0111111111111111」 16桁
     */         

    // データの書き込みテスト
    //----------------------------------------------------------
    // 「1」番地に「5」というデータを書き込む
    // 2進数:0000000000000001 → 10進数:1 → 16進数:01
    // 2進数:00000101 → 10進数:5 → 16進数:05
    
    // 引数1:書き込む番地(上位8bit)
    // 引数2:書き込む番地(下位位8bit)
    // 引数3:書き込むデータ
    // I2C_write_set(0b00000000,0b00000001,0b00000101);    
      
    // 書き込み処理が終わるまで待機(もっと短くても大丈夫)
    //----------------------------------------------------------
    //__delay_ms(100);
    
    // データの読み込みテスト
    //----------------------------------------------------------
    // 引数1:読み込むm番地(上位8bit)
    // 引数2:読み込む番地(下位位8bit)
    // I2C_read_set(0b00000000,0b00000001);
    
    
    //----------------------------------------------------------
    //               CDS(光センサ)の反応を検知
    //----------------------------------------------------------
    
   // 永久ループ
   while(1) {
       
       // AD変換準備時間
        __delay_us(25); 
        
         // AD変換スタート 
        GO = 1;    
        
        // AD変換終了まで待機(終了するとGOが0になる)
        while(GO);      
        
        // CDS感度の調整(小さくすると感度アップ)
        if (ADRESH > 150) { 
            
            // EEPROMの読み込み(シリアル通信)
            I2C_read_set(0x00,0x00);
            
        }
        
   } 
    
}

// ============================================================
//
//                  I2C 書き込み設定
//
// ============================================================
void I2C_write_set(unsigned char addr1 ,unsigned char addr2 ,unsigned char send_data) {
      
    // 通信開始の合図を送る
	I2C_start();
    
    /*
     * スレーブアドレスとは?
     * スレーブアドレスは、複数のデバイスが回路上にあるときの見分けを付けるためのもの。
     * 
     * 24LC256のスレーブアドレスは、1010[A2][A1][A0] となっている。
     * 24LC256のA2[3Pin]・A1[2Pin]・A0[1Pin]をGNDに繋いだら「0」、VCCに繋いだら「1」
     * 例えば、A2・A1・A0をすべてGNDに繋いだら、スレーブアドレスは「1010000」となる。
     * 例えば、A2・A1・A0をすべてVCCに繋いだら、スレーブアドレスは「1010111」となる。
     * 
     * ↑の7ビットの最後に、ROMの読み書き設定を1ビット追加する。
     * 書き込むときは「1」、読み込むときは「0」
     * もし1個しかROMを付けないなら、「0b10100000」(書き込みの場合)とすればいい。
     */
    
    // スレーブアドレスの送信(書き込みモード)
    I2C_send(0b10100000);

    // 書き込む番地(上位8bit)の送信
	I2C_send(addr1);

    // 書き込む番地(下位8bit)の送信
	I2C_send(addr2);

    //  書き込むデータの送信    
    I2C_send(send_data);

    // 通信終了の合図を送る
    I2C_stop();

	return;

}

// ============================================================
//
//                  I2C 読み取り設定
//
// ============================================================
void I2C_read_set(unsigned char addr1 ,unsigned char addr2) {

    // 通信開始の合図を送る
	I2C_start();
        
    // スレーブアドレスの送信(※書き込みモード)
    // 読み込みだけど、一旦書き込みモードでデータを送信する必要がある
    I2C_send(0b10100000);

    // 読み取り番地(上位8bit)の送信
	I2C_send(addr1);
    
    // 読み取り番地(下位8bit)の送信
	I2C_send(addr2);
    
    // 通信開始の合図を送る
	I2C_start();
    
     // スレーブアドレスの送信(※読み取りモード)
    I2C_send(0b10100001);

    // ROM内の全てのデータを受信する:512K(512000bits÷8bits = 64000Byte)
    for(unsigned short int i=1; i<64000; i++) {
            I2C_receive();
            I2C_ack();
    }
    
    // 読み取り終了の応答
    I2C_nack();

    // 通信終了の合図を送る
	I2C_stop();

	return;
    
}

// ============================================================
//
//                     I2C 通信開始
//
// ============================================================
void I2C_start() {
    
    // スタートコンディションを生成
    // SENをセットすることで、通信スタートの合図となる
	SEN = 1;
    
    // スタートコンディション発行終了まで待機
    // SENは発行終了後に0になる
	while (SEN) {};
  
	return;
}

// ============================================================
//
//                    I2C データ送信
//
// ============================================================
void I2C_send(unsigned char send_data) {
    
    // データ受信フラグをクリア(0:データが受信できる状態)
	SSP1IF = 0;
    
    // 送信モード有効
    // SSPIFが0のときにSSPBUFをセットすると、データが送信される
	SSP1BUF = send_data;
    
    // データ送信完了まで待機
    // 正常にデータが送信されると、SSPIFが1になる
	while (!SSP1IF) {}
    
	return;
}

// ============================================================
//
//                   I2C データ読み取り
//
// ============================================================
void I2C_receive() {
    
    // データ受信フラグをクリア(0:データが受信できる状態)
	SSP1IF = 0;
    
    // 受信モード有効
    // SSPIFが0のときにRCENをセットすると、データが受信される
	RCEN = 1;
    
    // データ受信完了まで待機
    // 正常にデータが受信されると、RCENが0になる
	while (RCEN) {}

    // 受信したデータは、SSP1BUFに入る
    // SSP1BUFを変数(reveive_data)に代入
    unsigned char receive_data;
    receive_data = SSP1BUF;
    
    // 受信データを音源に(パルス幅を変更)
    CCPR1L = receive_data;

    return;
}

// ============================================================
//
//                     I2C 継続応答
//
// ============================================================
void I2C_ack() {
    
    // 受信データの続きがあることを示す「ACK:0」を設定
	ACKDT = 0;
    
    // ACKDTを送信
	ACKEN = 1;
    
    // データ送信完了まで待機
    // 送信完了後、ACKENが0になる
	while (ACKEN) {}

	return;
}

// ============================================================
//
//                     I2C 終了応答
//
// ============================================================
void I2C_nack() {
	
    // 受信データの終わりを示す「NO-ACK:0」を設定
	ACKDT = 1;
    
    // ACKDTを送信
	ACKEN = 1;
    
    // データ送信完了まで待機
    // 送信完了後、ACKENが0になる
    while (ACKEN);

	return;
    
}

// ============================================================
//
//                     I2C 通信終了
//
// ============================================================
void I2C_stop() {
    
    // ストップコンディションの生成
    // PENをセットすることで、通信終了の合図となる
	PEN=1;
    
    // ストップコンディション発行終了まで待機
    // PENは発行終了後に0待つ
	while(PEN);
    
     // データ受信フラグをクリア(0:データが受信できる状態)
	SSP1IF = 0;
    
	return;
}

voice.txt

配線図

1.シーケンシャルリードにおいて、2件目以降のデータが正常に取得できない

原因は、1件目のデータを送ったあとに、まだ続きがあることを示す「ACKDT = 0」を送信するコードが漏れていたとでした。

ACKDT = 0;

LCDモニターを持っていなかったので、データをLEDの点灯回数に見立ててテストしていたことが、仇になり、「2件目以降のデータを取得できてはいるのに、なぜか値がおかしい・・・」というハマり方をしてしまった。

しっかりとモニターを使い数値を見ていれば防げたミスでした。

2.一度入力がONになると、永遠にリピートされてしまう

今回の回路は、光センサーが検知してONになったら、音声データが一度だけ再生される仕組みでした。

しかし、24LC256では容量が小さすぎて音声データが収まりきらなくなりました。

そこで、ワンランク上の24LC512に変更。

すると、今まで正常に動いていたのに、24LC512に変えた途端、光センサが一度でもONになると、永遠に音声がループしてしまうようになりました。

原因は、シーケンシャルリードで繰り返し呼び出すときのデータ型の指定でした。

何も考えずに「int」を使ってしまっていたが、「unsigned short int」に変更することで、正常に動いた。

// ROM内の全てのデータを受信する:512K(512000bits÷8bits = 64000Byte)
for(unsigned short int i=1; i<64000; i++) {
    I2C_receive();
    I2C_ack();
}

何かしらが原因で、-符号がついて、番地の頭に戻ってしまいりリピートされていたとか?

正直、はっきりとした原因は分かりませんが、PICkitで書き込む際、その行に対して青文字エラーが表示されていたことがヒントになりました。

3.PICKIT3では「24LCシリーズ」のEEPROMに書き込めない?

「PICKIT3では、24LC256には対応していない(PICKIT3を改造する必要がある)」という記事を見てしまい、「何とかPICKIT3で書きこむ方法はないものか?」と探すことにかなりの時間を費やしてしまいました。

結果的に、PICKIT3で24LCシリーズの書き込みは可能でした。

改造も必要ありません。

ただし、「MPLAB X IDE」では書き込むことができません。

「PICkit 3 Programmer」という旧タイプの開発ソフトを使う必要があります。

詳しい方法は、長くなるので別の記事にまとめました↓

PICkit3 Programmerの使い方と注意点(EEPROM 24LC256 / 24LC512の書き込み)

4.音源データの作り方が分からない

意外と音声のバイナリデータそのものを作る方法は紹介されてないんですよね。

難しくはないのですが、情報がなくて苦労しました。

ざっくりとした手順は、以下の通り ↓

  1. おもちゃの音声をスマホで録音(.m4a形式)
  2. 「.m4a」→ 「.wav」にWEBサービスを使って変換
  3. バイナリ編集ソフト「Stirling」でwavファイルを開く
  4. 拡張子をバイナリデータ「.bin」として保存
  5. 「PICkit 3 Programmer」で.binファイルを開く
  6. EEPROMに書き込む

詳しい方法は、長くなるので別の記事にまとめました↓

PICで使うROM音源(音声データ)の作り方【.wav → バイナリへ】.hex / .bin

5.再生したときの声が何か違う・・・

PICを通して再生した音声は、少しイメージと違いました。

何だか早口だし、少し声も高い気がします。

内部クロック数とかシリアル通信のクロック数とかいじってみましたが、細かい数字の調整ができないので、うまくいかず。

最終的に、『シリアル通信速度』を微調整することでうまくいきました。

// 通信速度設定
SSP1ADD = 6;

値を小さくすると、ゆっくり喋るようになり、声も低くなります。

値を大きくすると、早口になり、声も高くなります。

関連情報

運営者プロフィール
コダマ

職業はIT系フリーランス。過去、電子配線業務の経験が10年ある為、はんだづけも得意です。宮崎県在住、30代・2児の父親。

プロが教える!イチからわかるハンダ付けのコツ(工学社)の著者です。

カテゴリー