DE0-Nanoで、NiosとI2CのIPを使ってEEPROMにアクセスする

FPGA

前回、3線式SPIのIPを使い、DE0-Nanoに実装されている3軸加速度センサーADXL345にNiosからアクセスしてみました。

今回は、同じくDE0-Nanoに実装されている、EEPROM 24LC02Bを使ってみたいと思います。

アクセスはI2CのIPを利用し、Niosでリード・ライトを行いたいと思います。

Terasic DE0-NANO開発ボード 【P0082】

新品価格
¥14,520から
(2020/10/1 16:59時点)

なお、この記事では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_oei2c_0_i2c_serial_scl_oe)は、トライステートバッファ出力の制御に使っていると理解しました。

つまり、oe信号はI2CのIPが制御してくれそうですが、トライステートバッファはIP内に含まれていないので、IPの外部で準備する必要がありそうです。

次に、2本の入力信号(i2c_0_i2c_serial_sda_ini2c_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というツールを使って検証してみました。

  1. “Memory”を選択
  2. “Memory Type”で”EEPROM (80h WORDS, 256 Bytes)”を選択
  3. “Random Access”の”Address”を0にセット
  4. “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");
	}

何が起きているのかまったくわからず、挫折しました。。。このサンプルプログラムはどうやって使うんだろう???

まとめ

リードの動作がちょっと不安定ですが、I2CのIPを使ってEEPROMにアクセスできました。

ちょっとずつDE0-Nanoの機能を動かせるようになってきました。

追記 Cソースコードの修正版を作ってみました。

タイトルとURLをコピーしました