A/DコンバータをAvalonバスでCプログラムから読んで温度を計る

AlteraのFPGA開発環境には、Nios IIというFPGA組み込み用のCPUライブラリ(FPGAの世界ではIPというらしい)がついていて、このプロセッサをベースとしてCのプログラムが動かせるようになってる。
そこにSDRAMコントローラのIPやLCDコントローラのIPなどを組み合わせていくと、一台のコンピュータになる。


そうすると、ハードウェア制御はHDLで書いて、手続きはCで書いてという使い分けができるようになって、実際に動くモノを作るには非常に生産性がよい。
まあ、そうなると、Cの部分は単なる組み込み開発になってそんなに目新しいものでもなくなるのだけど、HDLのコンパイル(FPGAの世界では合成というらしい)に比べてCのコンパイルは格段に早いし、なにより手続きが書きやすいので、自分で作った回路の動作確認などもやりやすくなる。


ただ、そのためにはNios IIプロセッサとやりとりするためのAvalonバスに接続できる回路を書いてあげる必要があって、その作法を知っておかないといけない。
まあ、バスに接続するといっても、モジュールの入出力をあわせてあげるだけなのだけど。それが単独で動かすよりは結構大変。


そこで、ちょうど手元に2ch A/Dコンバータがあるので、練習に、これを制御しつつCプログラムから読み出せるようにコンポーネントを作ってみた。(A/DコンバータはSPIというプロトコルで制御するのだけど、SPIコアもすでに用意されてるので実際はそっちが使える)
そしたら、こんな感じでコンポーネントとして接続する。

なんか、一度登録した自作コンポーネントを認識してくれてないのだけど・・・


Cプログラムの開発はEclipseベースのツールで行う。

#include <stdio.h>
#include "io.h"
#include "system.h"
int main()
{
  printf("Hello from Nios II!Yes!!\n");

  int airtmp;
  int varreg;
  airtmp = IORD(ADC2CH_0_BASE, 1);
  varreg = IORD(ADC2CH_0_BASE, 0);
  double c = (airtmp - 0x14) / 4.;
  printf("var:%x tmp:%x=%.2f\n", varreg, airtmp, c);

  FILE* lcd = fopen("/dev/lcd_0", "w");
  fprintf(lcd, "hello from nios!\nvar:%x tmp:%.2f", varreg, c);
  fclose(lcd);

  FILE* uart = fopen("/dev/uart_0", "w");
  fprintf(uart, "hello!\n\rit is %.2fC\n\r", c);
  fclose(uart);
}

IORDが、Avalonバスから値を読み込むマクロ。
普通にprintfすると、Eclipseの出力ウィンドウにメッセージが表示される。LCDディスプレイやUARTシリアル通信のコントローラも組み込んでいるので、printfで出力できてすばらしい!
今回はch0に可変抵抗、ch1に温度センサをつけていて、その値が表示される。温度センサの値は、14hを引いて4で割ると摂氏温度になるらしい。

見にくいけど、LCDEclipseの出力とターミナルにメッセージが表示されている。19℃らしい。


最初リセット信号が、onでリセット、offでリセット解除となっていると気付かなくて、ハマりまくった。
A/Dコンバータのクロックが1KHzで、50MHzのネイティブ側からすれば5万クロックあるので、単純にwaitrequestを解除すると同じデータを出力するときに次の読み込み要求が来てしまって、アドレス指定を無視した結果が返ってしまっていた。ネイティブの1クロックだけwaitrequestを解除させて対処。
readyフラグみたいなのを用意して、Cプログラムでウェイトさせる必要もあるかも。


参考にしたのはこの本で、DE0でFPGAを始めるなら必ず目をとおしたほうがいいと思う。

FPGA ボードで学ぶ組込みシステム開発入門 ?Altera編?

FPGA ボードで学ぶ組込みシステム開発入門 ?Altera編?

Linuxを動かす解説もあるんだけど、ビルドが通らなかったのであきらめた。


Avalonバスの資料はこういうのがあった。日本語版はないのかな?
Avalon Interface Specifications」
http://www.altera.com/literature/manual/mnl_avalon_spec.pdf


あと、すでに用意されてるコンポーネントIPの説明はこんなのがあった。
「Nios II プロセッサ・リファレンス・ハンドブック セクションII. ペリフェラル・サポート」
http://www.altera.co.jp/literature/hb/nios2/n2cpu_nii5v1_02_j.pdf


とりあえずソースはこんな感じになった。
※2013/2/13 追記 MCP3002を想定していますが、最上位ビットがとれなかったり、タイミングの問題があるのでこのままでは使えません。データ取得をposedgeで行う必要があります。クロックも1MHzくらいで動くのでここまで遅くする必要はありません。

module adc2ch(
    input clk,
    input reset,
    input address,
    input read,
    output [15:0] readdata,
    output reg waitrequest,
    output sclk,
    output scs,
    output reg sdi,
    input sdo
);
    
    
    //1KHzをつくる
    reg [14:0] cnt;
    reg clk1k;
    always @(posedge clk or posedge reset) begin
        if(reset) begin
            cnt <= 15'd0;
        end else if(cnt == 15'd24999) begin
            if(!clk1k && scnt == 5'd16) begin
                waitrequest <= 1'b0;
            end
            clk1k <= clk1k + 1'b1;
            cnt <= 15'd0;
        end else begin
            cnt <= cnt + 15'd1;
            waitrequest <= 1'b1;
        end
    end
    
    //状態カウンタ
    reg [4:0] scnt;
    always @(posedge clk1k or posedge reset) begin
        if(reset) begin
            scnt <= 5'd0;
        end else if(scnt == 5'd16) begin
            scnt <= 5'd0;
        end else if(scnt == 5'd0) begin
            if(read)
                scnt <= 5'd1;
        end else begin
            scnt <= scnt + 5'd1;
        end
    end
    
    
    //SPI処理
    reg [9:0] tmpdata;
    reg [9:0] result;
    reg cs;
    assign scs = ~cs;
    assign sclk = clk1k & cs;
    assign readdata = {6'd0, result};
    always @(negedge clk1k) begin
        case(scnt)
        5'd0: begin
            cs <= 1'b0;
        end
        5'd1: begin //start
            cs <= 1'b1;
            sdi <= 1'b1;
        end
        5'd2: begin //single
            sdi <= 1'b1;
        end
        5'd3: begin //ch
            sdi <= address;
        end
        5'd4: begin //msbf
            sdi <= 1'b1;
        end
        5'd5: begin
            tmpdata <= 10'd0;
        end
        5'd16: begin
            cs <= 1'b0;
            result <= tmpdata;
        end
        default: begin
            tmpdata <= {tmpdata[8:0], sdo};
        end
        endcase
    end
endmodule