前回、3線式SPIのIPを使い、DE0-Nanoに実装されている3軸加速度センサーADXL345にNiosからアクセスしてみました。
今回は、同じくDE0-Nanoに実装されている、EEPROM 24LC02Bを使ってみたいと思います。
アクセスはI2CのIPを利用し、Niosでリード・ライトを行いたいと思います。
![]() | 【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門[Intel FPGA編] 新品価格 |

なお、この記事ではNiosをプライマリ、EEPROMをレプリカと呼ぶことにします。
FPGAとEEPROMの回路構成
DE0-Nanoボードですが、FPGAとEEPROMの回路構成は下図のようになっています。(下図は、DE0-NanoのUser Manualからの抜粋です。)

加速度センサーADXL345の時はSPIまたはI2Cが利用できましたが、EEPROM 24LC02BはI2Cのみです。
DE0-Nanoに実装されているEEPROMは、もともとSPIには対応しておらず、I2Cのみ利用可能なようです。
また、SCLKとSDATはIC外部でプルアップされているため、FPGAの内部でWeak Pull Upの設定などをする必要はなさそうです。
Platform Designerを使ってハードウェア設計
Platform Designer
まず、QuartusのPlatform Designerを使って必要なハードウェアを追加していきます。
以下の基本機能を追加します。
- Nios II Processor
- On-Chip Memory (RAM or ROM) Intel FPGA IP
- JTAG UART Intel FPGA IP
QuartusのPlatform Designerの使い方や設定値は、以下の記事を参考にしてください。
次に、EEPROMにアクセスするためのI2CのIPを追加します。
IP Catalogから、Interface Protocols -> Serial -> Avalon I2C (Master) Intel FPGA IPを追加します。

設定は、Interface for transfer command Fifo and receive data Fifo accessesをAvalon-MM Slaveにします。
Depth of Fifoは、お好みで。たぶん、今回作成したCのソースコードではFIFOは使っていないと思います(←FIFOの使い方が理解できていません)(FIFOは使用していました)。最小値が4なので、とりあえず4に設定しています。

すべてのIPの追加が終わったら、以下のように接続します。(各IPの名前はわかりやすいように変更しています)
“i2c_serial”のExportをダブルクリックするのを忘れずに。

接続が終わったら、メニューのSystem -> Assign Base Addressesを実行し、Generate HDLを実行します。
詳細は”第2回 Nios IIで遊ぼう Quartus Platform Designer編“の記事を参照してください。
HDLの生成
Generate HDLを実行すると、xxx_inst.vhdというファイルが生成されると思います。
※xxxは、Platform Designerで保存したときの名前
※拡張子vhdは、VHDLファイルを生成したため
そのファイルの中身がこちら。
component i2c_qsys is port ( clk_clk : in std_logic := 'X'; -- clk i2c_0_i2c_serial_sda_in : in std_logic := 'X'; -- sda_in i2c_0_i2c_serial_scl_in : in std_logic := 'X'; -- scl_in i2c_0_i2c_serial_sda_oe : out std_logic; -- sda_oe i2c_0_i2c_serial_scl_oe : out std_logic -- scl_oe ); end component i2c_qsys; u0 : component i2c_qsys port map ( clk_clk => CONNECTED_TO_clk_clk, -- clk.clk i2c_0_i2c_serial_sda_in => CONNECTED_TO_i2c_0_i2c_serial_sda_in, -- i2c_0_i2c_serial.sda_in i2c_0_i2c_serial_scl_in => CONNECTED_TO_i2c_0_i2c_serial_scl_in, -- .scl_in i2c_0_i2c_serial_sda_oe => CONNECTED_TO_i2c_0_i2c_serial_sda_oe, -- .sda_oe i2c_0_i2c_serial_scl_oe => CONNECTED_TO_i2c_0_i2c_serial_scl_oe -- .scl_oe );
I2C関連の信号は、以下の4本です。
- i2c_0_i2c_serial_sda_in — 入力信号
- i2c_0_i2c_serial_scl_in — 入力信号
- i2c_0_i2c_serial_sda_oe — 出力信号
- i2c_0_i2c_serial_scl_oe — 出力信号
I2Cの信号線は、プライマリが出力するクロックと、プライマリが入出力するデータの2本なので、なぜ4本の信号線???と最初は謎でした。(今も完全には理解できていませんが)
インテルが公開しているEmbedded Peripherals IP User Guideでは、以下のように説明しています。

また、以下の図も記載されていました。

さらに、以下のHDLの記載例。

理解が間違っていたら申し訳ないのですが、どうやら、プライマリが出力するクロックもデータも、トライステートバッファで制御するようです。
クロックもデータも、Lowを出力するときはトライステートバッファのoe(Output Enable)がHighとなります。
トライステートバッファの入力は、Low固定(たぶん”1’b0″はLow固定の意味)なので、トライステートバッファがHighを出力することはなさそうです。
クロックとデータのHigh出力は、トライステートバッファの出力をハイインピーダンスとし(oe=LOW)、外部のプルアップでHighレベルにしているようです。
ということで、2本の出力信号(i2c_0_i2c_serial_sda_oeとi2c_0_i2c_serial_scl_oe)は、トライステートバッファ出力の制御に使っていると理解しました。
つまり、oe信号はI2CのIPが制御してくれそうですが、トライステートバッファはIP内に含まれていないので、IPの外部で準備する必要がありそうです。
次に、2本の入力信号(i2c_0_i2c_serial_sda_inとi2c_0_i2c_serial_scl_in)ですが、これらはトライステートバッファ出力をそのまま接続すれば良いようです。(ユーザに接続させないで、IP内で接続しておいてくれれば混乱しなかったのだが。。。)
あと、データの信号線は、プライマリもレプリカも出力するのでトライステートバッファを使うことはわかるのですが、なぜクロックの信号線もトライステートバッファにしたのだろうか???
私が知らないだけで、クロックの信号線もハイインピーダンス出力しなければならない時があるのかな???マルチマスタとか???
この理解が正しいかはわからないのですが、最終的に以下のVHDLを生成しました。
library ieee; use ieee.std_logic_1164.all; entity nios_i2c is port( CLK50M : in std_logic; SDA : inout std_logic; SCL : inout std_logic ); end nios_i2c; architecture rtl of nios_i2c is component i2c_qsys is port ( clk_clk : in std_logic := 'X'; -- clk i2c_0_i2c_serial_sda_in : in std_logic := 'X'; -- sda_in i2c_0_i2c_serial_scl_in : in std_logic := 'X'; -- scl_in i2c_0_i2c_serial_sda_oe : out std_logic; -- sda_oe i2c_0_i2c_serial_scl_oe : out std_logic -- scl_oe ); end component i2c_qsys; signal sda_in : std_logic; signal scl_in : std_logic; signal sda_oe_out : std_logic; signal scl_oe_out : std_logic; begin u0 : component i2c_qsys port map ( clk_clk => CLK50M, -- clk.clk i2c_0_i2c_serial_sda_in => sda_in, -- i2c_0_i2c_serial.sda_in i2c_0_i2c_serial_scl_in => scl_in, -- .scl_in i2c_0_i2c_serial_sda_oe => sda_oe_out, -- .sda_oe i2c_0_i2c_serial_scl_oe => scl_oe_out -- .scl_oe ); scl_in <= SCL; sda_in <= SDA; SCL <= '0' when scl_oe_out = '1' else 'Z'; SDA <= '0' when sda_oe_out = '1' else 'Z'; end rtl;
VHDLの作成が終わったらシンボル化しておきます。
私は設計階層のトップは回路図にするのが好きなので、シンボル化したVHDLを使って、階層トップを以下のようにしました。

ピン割付けとコンパイルを実施、問題なければ、FPGAに書き込んでおきます。
Nios II Software Build Tools for Eclipseを使ってソフトウェア設計
ソースコードと実行結果
FPGAへの書き込みが終わったら、次にソフトウェア設計を行います。
FPGAへの書き込み先がRAMの場合には、DE0-Nanoの電源を落とさないままソフトウェア設計を行います。電源を落とすと書き込みデータが消えてしまうので。
QuartusメニューのTools -> Nios II Software Build Tools for Eclipseを選択します。

Eclipseが立ち上がったら、以下の記事を参考に、プロジェクト生成や初期設定を行ってください。
準備ができたらソースコードです。
ここでは、EEPROMへのバイトライトをした後に、バイトリードをして、ライトした値がリードできるかを確認してみたいと思います。
I2C通信するために、まず必要な情報はレプリカ(EEPROM)のアドレスです。
DE0-NanoのUser Manualには以下の記載があります。
The I2C write and read address are 0xA0 and 0xA1, respectively.
ということで、ライト時のアドレスは0xA0、リード時のアドレスは0xA1です。
書き込み先のアドレスは、0x00。
書き込むデータは、0xA5。
書き込んだ後に、アドレス0x00からデータをリードして、その値をコンソールウインドウに表示させてみます。
そのソースコードがこちら。
#include "sys/alt_stdio.h" #include "system.h" #include <stdio.h> #include <unistd.h> #include <altera_avalon_i2c.h> #include <altera_avalon_i2c_regs.h> int main() { ALT_AVALON_I2C_DEV_t *i2c_dev; //pointer to instance structure alt_u32 data_u32; alt_u8 rdata; alt_u32 wait_time = 10000; alt_printf("Hello EEPROM !!\n"); //get a pointer to the avalon i2c instance i2c_dev = alt_avalon_i2c_open("/dev/i2c_0"); if (NULL == i2c_dev) { printf("Error: Cannot find /dev/i2c_0\n"); return 1; } //---------------------- //Control Register //---------------------- //bit1 : Bus speed -- 0: Standard mode (up to 100 kbits/s) //bit0 : EN -- 1: Core is enabled data_u32 = 0x01; IOWR_ALT_AVALON_I2C_CTRL(I2C_0_BASE,data_u32); //Check //data_u32 = 0; //data_u32 = IORD_ALT_AVALON_I2C_CTRL(I2C_0_BASE); //alt_printf("CTRL Reg = %x \n", data_u32); //---------------------- //EEPROM Write //---------------------- //Transfer Command FIFO //I2c write address //bit9 : STA -- 1: Requests a repeated START condition to be generated before current byte transfer //bit8 : STO -- 0: //bit7:1 -- I2c write address = 0xA0 //bit0 : RW_D -- 0: Specifies I2C write transfer request IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x2a0); //EEPROM address //bit9 : STA -- 0: //bit8 : STO -- 0: //bit7:1 -- EEPROM address = 0x00 //bit0 : RW_D -- 0: Specifies I2C write transfer request IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x000); //Data write //bit9 : STA -- 0: //bit8 : STO -- 1: Requests a STOP condition to be generated after current byte transfer //bit7:1 -- I2c write address = 0xA5 //bit0 : RW_D -- 0: Specifies I2C write transfer request IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x1A5); //---------------------- //EEPROM Read //---------------------- usleep(wait_time); IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x2a0); usleep(wait_time); IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x000); usleep(wait_time); //Transfer Command FIFO //I2c read address //bit9 : STA -- 1: Requests a repeated START condition to be generated before current byte transfer //bit8 : STO -- 0: //bit7:1 -- I2c read address = 0xA1 //bit0 : RW_D -- 0: Specifies I2C read transfer request IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x2a1); usleep(wait_time); //Data read //bit9 : STA -- 0: //bit8 : STO -- 1: Requests a STOP condition to be generated after current byte transfer //bit7:1 -- read data //bit0 : RW_D -- 1: Specifies I2C read transfer request IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x100); usleep(wait_time); //Receive Data rdata = 0; rdata = IORD_ALT_AVALON_I2C_RX_DATA(I2C_0_BASE); alt_printf("Read data = %x \n", rdata); return (0); }
これを実行し、コンソールウインドウに表示された結果です。

“Read data = a5″と表示されているので、EEPROMへの書き込みと読み込みには成功したようです。
検証
念のため、terasIC社のDE0_Nano_ControlPanelというツールを使って検証してみました。

- “Memory”を選択
- “Memory Type”で”EEPROM (80h WORDS, 256 Bytes)”を選択
- “Random Access”の”Address”を0にセット
- “Read”をクリック
結果、黄色枠のrDATAに00A5で表示されました。
少なくとも0xA5の書き込みには成功していそうです。
EEPROM Readの時のusleep()について
ソースコードを見ていただいた方は、リード時にusleep(wait);が大量に入っていることに気付かれたと思います。
なぜusleep()を入れたかというと、入れないと適切な値が読み込めなかったためです。
今のところ、原因はわかっていません。
全般的に、ライトは大丈夫そうなのですが、リードがちょっと不安定な感じがします。
ちなみに、リードは、24LC02Bのデータシートの7.2章 Random Readに従って行っているつもりです。
Random Readでは、まず読み込みたいアドレスを書き込んでいます。
ソースコードのこの部分で、アドレスを0に設定しています。
IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x2a0); IOWR_ALT_AVALON_I2C_TFR_CMD(I2C_0_BASE, 0x000);
このあたりが上手くいってないのかな???どこかのコマンドでACKが返ってないとか???
原因調査は次回の課題にしておきます。
追記 原因の一つは、Transmit readyのフラグが準備できていないない段階で、次の送信データを送っていたことでした。Cソースコードの修正版はこちらをご参照ください。
ADXL345と24LC02Bを同時に使用する時の注意点
前に3軸加速度センサーADXL345のアクセス方法を記事にしました。
この時は3線式SPIのIPを使って、Niosと通信させてみました。
そして、今回I2Cを使ってEEPROM 24LC02Bにアクセスしましたが、SPIとI2Cのクロックとデータ線は共通になっています。

そのため、3軸加速度センサーとEEPROMの両方を使ってDE0-Nanoを開発する場合には、I2Cを使うしかないようです。
失敗談
ソースコードを作成する際、インテルが公開しているEmbedded Peripherals IP User Guideの15.7.6章にあるサンプルプログラムを参考にさせていただきました。
以下のソースコードを作成し、実行してみました。
#include "sys/alt_stdio.h" #include "system.h" #include <stdio.h> #include <unistd.h> #include <altera_avalon_i2c.h> #include <altera_avalon_i2c_regs.h> int main() { ALT_AVALON_I2C_DEV_t *i2c_dev; //pointer to instance structure alt_u8 txbuffer[200]; ALT_AVALON_I2C_STATUS_CODE status; alt_printf("Hello EEPROM !!\n"); //get a pointer to the avalon i2c instance i2c_dev = alt_avalon_i2c_open("/dev/i2c_0"); if (NULL == i2c_dev) { printf("Error: Cannot find /dev/i2c_0\n"); return 1; } //Busy check status = alt_avalon_i2c_enable(i2c_dev); if (status == ALT_AVALON_I2C_BUSY){ alt_printf("the I2C controller is already enabled !!\n"); } else if (status == ALT_AVALON_I2C_SUCCESS){ alt_printf("the I2C controller has been successfully enabled !!\n"); } //set the address of the device using alt_avalon_i2c_master_target_set(i2c_dev, 0xA0); //write data to an eeprom at address 0x00 txbuffer[0] = 0x00; //The eeprom address which will be sent as first byte of data txbuffer[1] = 0x55; status = alt_avalon_i2c_master_tx(i2c_dev, txbuffer, 2, ALT_AVALON_I2C_NO_INTERRUPTS); if (status == ALT_AVALON_I2C_SUCCESS){ alt_printf("Successful !!\n"); } else if(status == ALT_AVALON_I2C_ARB_LOST_ERR){ alt_printf("ALT_AVALON_I2C_ARB_LOST_ERR !!\n"); } else if(status == ALT_AVALON_I2C_NACK_ERR){ alt_printf("ALT_AVALON_I2C_NACK_ERR !!\n"); } else if(status == ALT_AVALON_I2C_BUSY){ alt_printf("ALT_AVALON_I2C_BUSY !!\n"); } else{ alt_printf("Error !!\n"); } return (0); }
実行した結果、”ALT_AVALON_I2C_BUSY”となりました。

試しに、以下の部分をコメントアウトして実行すると、今度は”ALT_AVALON_I2C_NACK_ERR”となりました。
//Busy check status = alt_avalon_i2c_enable(i2c_dev); if (status == ALT_AVALON_I2C_BUSY){ alt_printf("the I2C controller is already enabled !!\n"); } else if (status == ALT_AVALON_I2C_SUCCESS){ alt_printf("the I2C controller has been successfully enabled !!\n"); }

何が起きているのかまったくわからず、挫折しました。。。このサンプルプログラムはどうやって使うんだろう???
![]() | FPGAチャレンジャー入門編:ALTERA-Cyclone-IV版 キット CD (キットで学ぶ! シリーズ) 新品価格 |

![]() | VHDLによるハードウェア設計入門?言語入力によるロジック回路設計手法を身につけよう (Design wave basic) 新品価格 |

まとめ
リードの動作がちょっと不安定ですが、I2CのIPを使ってEEPROMにアクセスできました。
ちょっとずつDE0-Nanoの機能を動かせるようになってきました。
追記 Cソースコードの修正版を作ってみました。