VerilogでUART送信
前置き
先日UARTの受信を作成した。今回は送信。
方針
START信号を検出する。
外部データをBuffに入力。
スタートビットを出力。
クロックをカウントし、ボーレートのタイミングで信号を1bitずつ送信する。
信号送信回数をカウントする。
9回目のカウントで、出力を終えて初期化。
ソースコード
Verilog-HDL
module uart_tx( input[7:0] UARTDATA, input CLK, input RST, input START, output reg UARTTX ); reg[7:0] uartTxBuff = 8'b0; reg[8:0] clkCnt = 9'b0; reg[3:0] dataCnt = 4'b0; reg sendEn = 0; parameter UARTTIMING = 9'd434;//CLK/Baudrate ex)5oMHz/115200 always@(posedge CLK or negedge RST)begin if(!RST)begin clkCnt <= 9'b0; dataCnt <= 4'b0; end else if(sendEn)begin if(clkCnt != UARTTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCnt == UARTTIMING clkCnt <= 9'b0; dataCnt <= dataCnt + 4'b1; UARTTX <= uartTxBuff[7]; uartTxBuff[7:0] <= {uartTxBuff[6:0], 1'b1};//"1" means stop bit if(dataCnt == 4'b1001)begin sendEn <= 0; clkCnt <= 9'b0; dataCnt <= 4'b0; end end end else if(START)begin sendEn <= 1; uartTxBuff[7:0] <= UARTDATA[7:0];//Copy Data UARTTX <= 0;//start bit end end endmodule
テストベンチ
`timescale 1 ns/ 100 ps module uart_tx_vlg_tst(); // constants // general purpose registers reg eachvec; // test vector input registers reg CLK; reg RST; reg START; reg [7:0] UARTDATA; // wires wire UARTTX; // assign statements (if any) uart_tx i1 ( // port map - connection between master ports and signals/registers .CLK(CLK), .RST(RST), .START(START), .UARTDATA(UARTDATA), .UARTTX(UARTTX) ); always #10 CLK = ~CLK; initial begin CLK = 0; RST = 1; START = 0; UARTDATA = 8'b0101_1100; #200 RST = 0; #200 RST = 1; #200 START = 1; #200 START = 0; #100 UARTDATA = 8'b1100_1001; #90000 START = 1; #500 START = 0; $display("Running testbench"); end always begin @eachvec; end endmodule
気づき
verilog-HDLのソースを変更すると、Modelsimを再度立ち上げ直さないと反映されない。
なにか方法あるのかも?
VerilogでUART受信
前置き
Verilogと開発ツールの使い方の勉強のため、Verilogであれこれ作成・シミュレーションしようと思う。
UARTの受信
通信仕様は
- データ8bit
- Parityなし
Stop1bit
今までこれ以外のUART通信をみた事がないので、拡張性も持たせない。 ボーレートはparameterで可変とする。 今回は115200bpsで作成。
大方針
スタートビットの検出方法
一般的なスタートビットの検出方法として、
UARTのボーレート用の逓倍のクロックを生成し、数回連続でLだったらスタートビットとみなす方法と、
UARTデータを基板の元クロックでモニターし、数回連続でLだったらスタートビットとみなす方法がある。
今回は後者で検出する。後者のメリットは。ボーレートが多少ずれても許容幅が大きいこと。
手順
- スタートビットを立下りのエッジで検出する
- スタートビットを検出したら、クロックのカウントを開始する。
- ボーレートの半周期だけ待つ。
- ボーレートの1周期毎にデータを取得する。
- 上記を9回(データ8bit + stop bit)繰り返す。
- 9個目のデータ(ストップビット)がHの場合にバッファにデータを送る。
- レジスタを初期化する。
ソースコード
Verilog-HDL
module uart_rx( input wire CLK, input wire RST, input wire UARTRX, output reg[7:0] UARTRXBUFF ); parameter UARTTIMING = 9'd434;//CLK/Baudrate ex)50MHz/115200 parameter WAITTIMING = 9'd217;//CLK/Baudrate/2 ex)50MHz/115200/2 reg[8:0] clkCnt = 9'b0; reg[8:0] rxData = 9'b0;//8bit + stop bit reg[3:0] dataCnt = 4'b0; reg[3:0] startEdge = 4'b1111; parameter WAITSTATE = 2'b01, RDSTATE = 2'b10, IDLESTATE = 2'b00; reg[1:0] state = 2'b00; //UART CLK always@(posedge CLK or negedge RST)begin if(!RST)begin clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end else begin if(state == IDLESTATE)begin startEdge[3:0] <= {startEdge[2:0], UARTRX}; if(startEdge == 4'b1000)begin state <= WAITSTATE; end end else if(state == WAITSTATE)begin if(clkCnt != WAITTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCounter == WAITTIMING clkCnt <= 9'b0; state <= RDSTATE; end end else if(state == RDSTATE)begin if(clkCnt != UARTTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCounter == UARTTIMING clkCnt <= 9'b0; rxData[8:0] <= {rxData[7:0], UARTRX}; dataCnt <= dataCnt + 4'b1; end if(dataCnt == 4'b1001)begin if(rxData[0])begin UARTRXBUFF[7:0] <= rxData[8:1]; end clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end end else begin//state == OTHER clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end end end endmodule
テストベンチ
`timescale 1 ns/ 100 ps module uart_rx_vlg_tst(); // constants // general purpose registers reg eachvec; // test vector input registers reg CLK; reg RST; reg UARTRX; // wires wire [7:0] UARTRXBUFF; // assign statements (if any) uart_rx i1 ( // port map - connection between master ports and signals/registers .CLK(CLK), .RST(RST), .UARTRX(UARTRX), .UARTRXBUFF(UARTRXBUFF) ); always #10 CLK = ~CLK; initial begin CLK = 0; RST = 1; UARTRX = 1; #100 RST = 0; #100 RST = 1; #1000 UARTRX = 0;//start #8680 UARTRX = 1;//1bit #8680 UARTRX = 0;//2bit #8680 UARTRX = 1;//3bit #8680 UARTRX = 1;//4bit #8680 UARTRX = 0;//5bit #8680 UARTRX = 0;//6bit #8680 UARTRX = 1;//7bit #8680 UARTRX = 0;//8bit #8680 UARTRX = 1;//stop #20000 UARTRX = 0;//start #8680 UARTRX = 1;//1bit #8680 UARTRX = 1;//2bit #8680 UARTRX = 1;//3bit #8680 UARTRX = 0;//4bit #8680 UARTRX = 0;//5bit #8680 UARTRX = 0;//6bit #8680 UARTRX = 1;//7bit #8680 UARTRX = 0;//8bit #8680 UARTRX = 1;//stop // --> end $display("Running testbench"); end always begin @eachvec; end endmodule
気づき
4bitのレジスタに3bit と1bitのレジスタを連結させ代入する場合、わざわざ4bitであることを明記しないとデータが正しく代入できなかった。
手元にある書籍には明確な記載はないので、別に原因があったのかも?
//正しい startEdge[3:0] <= {startEdge[2:0], UARTRX}; //間違い startEdge <= {startEdge[2:0], UARTRX};
テストベンチで、不要と思っていた以下の記述を削除すると、波形が表示されなかった。
always begin @eachvec; end
身についたこと
- parameterの使い方
- テストベンチの書き方
- シミュレーションの内部信号の表示方法
FPGAで画像処理
DE10-Nanoボードで画像処理
IntelのFPGAボードとイメージセンサ2つで、深度カメラを構築しようと思います。
大まかな手順は以下を想定。
No | To Do | How |
---|---|---|
1 | イメージセンサとFPGA間の制御信号のIFを作成 | I2C, SCCB |
2 | FPGAとPC間の通信用IFを作成 | UART |
3 | イメージセンサの制御ソフトを作成 | ? |
4 | イメージセンサとFPGA間の画像データ信号のIFを作成 | ? |
5 | FPGAとPC or Display間の画像データ信号用IFを作成 | HDMI, VGA, USB |
6 | イメージセンサ2つのデータを処理するモジュールを作成 | ? |
部品の選定
FPGAボードはDE10-Nano-SoCを使用。理由は、現在手元にあるから。
MAX10の評価ボードもあるので、ひょっとしたらMAX10にするかも。
今回はverilogの勉強を兼ねているので、ARMもNIOSⅡも使わないで行けるところまで行く予定。
イメージセンサは何が適切か不明。なのでとりあえず秋月で安く売っていたOV7675http://akizukidenshi.com/catalog/g/gM-13201/を使用予定。
SCCB(I2C)IFの作成(作成中)
SCCBはオムニビジョン社のイメージセンサの制御によく用いられるIF。基本はI2Cのはず。
I2Cは昔ながらのIFなのでFPGAでも簡単に記述できるでしょう
という考えはちょっと甘く、I2Cはデバイスによって微妙に動作が異なる。
各デバイスに応じてverilogの記述を変える必要がある。
Intel提供のI2CのIPはコードが膨大。
QsysでI2C搭載してHDL生成したら、verilogファイルがわんさか出てきた・・・
UART IFの作成(作成中)
I2Cとは打って変わって、UARTは楽。
QsysでUARTのIP搭載してHDL生成しても、ソースコード1つしか出てこない。
920行もあるけど、それはハードウェア言語の宿命。
イメージセンサの制御ソフトを作成(作成中)
FPGAとPCが通信できたので、PCからFPGAのI2C IFを制御するためのソフトを作成。
主要なレジスタの設定だけできるようにする。
FPGA内部でも、PCから送られるUARTのデータとI2Cのデータを連携させる必要がある。
C言語なら簡単なのにと思いながらもverilogでせっせと作成。
イメージセンサとFPGA間の画像データ信号のIFを作成(作成中)
イメージセンサから送信される画像データは、D0-D8のデータをVSYC、HSYNCによって適切なレジスタに格納する。
難しいかと思ったが、他のIFより簡単かも。
FPGAとPC or Display間の画像データ信号用IFを作成(作成中)
HDMI、VGA、USBと方法は色々あれど、どのIFを使うにしても方法が謎。
ちょこちょこ勉強するしかない。
イメージセンサ2つのデータを処理するモジュールを作成(作成中)
トラ技かInterfaceにそれっぽい記事があったので、なんとかなると楽観視している。
Intel FPGAのSimulationの手順
Intel FPGAのSimulation
FPGAの設計は時間がかかる。
特にデバッグに時間がかかるので、コードの誤りは早い段階で検知しておきたい。
そのためにはSimulationを使いましょう。
ここではIntel FPGAのModelsim-Alteraの使い方を簡単にまとめる。
流れ
- プロジェクトの作成
- verilog fileの作成
- テストベンチのテンプレートの作成
- テストベンチの作成
- Modelsimの起動とSimulationの実行
プロジェクトの作成
Quartus PrimeのFile > New Project Wizardをクリック
適当に進めてProjectを作成する
この画面でModelsim-alteraを選択する
verilog fileの作成
- Quartus PrimeのFile > Newをクリック
Design Files > Verilog HDL Fileをクリック
Verilogで書く。
- Start Analysys & Synthesisをクリックし、エラーが出ないことを確認する。
テストベンチのテンプレート作成
Quartus PrimeのProcessing > Start > Start Test Bench Template Writerをクリックする。
プロジェクトフォルダ/simulation/modelsimフォルダ内に、"モジュール名".vtのファイルが生成される。
これがテストベンチのテンプレート。
入出力と内部信号が記載されている。便利。
テストベンチの作成
生成されたテストベンチテンプレートファイルを編集する。
ご丁寧にinitialとalwaysまで記載されているので、そこにinput信号の記載。プロジェクトを右クリックしてSettinsを選択。
EDA Tool SettingsのSimulationを選択。
NativeLink settingsのCompile test benchをチェックし。Test Benchesをクリック。
Newをクリック。
テストベンチ名を適当に入力し、Test bench and simulation filesのFile nameに作成したテストベンチを選択して入力してOKをクリック。
再びプロジェクトを右クリックしてSettingsを選択し、以下の画面のFilesをクリック。File名に先ほど作成したテストベンチを選択して追加する。
Modelsimの起動とSimulationの実行
ModelSimのPathを設定するため、Quartsu Primeの Tools > Optionをクリック。
GeneralのEDA Tool OptionsのMdelSim-AlteraにPathを選択して記入する。
自分の場合は"C:\intelFPGA_lite\17.1\modelsim_ase\win32aloem\"Modelsim-alteraを起動するため、Quartus Primeの Tools > Run Simulation Tool > RTL Simulationをクリック。
workの中にテストベンチがある事を確認する。
記述に誤りがありコンパイルができず表示されない。
その場合はテストベンチを修正し、Compile > Complieをクリックし、テストベンチをコンパイルする。
Simulationを実行するため作成したテストベンチのファイル名をダブルクリックする。
Waveタブをクリックし波形を表示する。
閲覧したいネットをドラッグしWaveにドロップする(まだ波形は表示されない)。
閲覧したい波形の最大時間を設定し、Runボタンをクリックする。
波形が表示される。
まとめ
手順が多いので地味に大変。基本は以下の3つ。
- テストベンチファイルを記述
- テストベンチの作成
- プロジェクトにテストベンチファイルを追加
2と3のいずれかをうっかり忘れてしまいがちなので
上手くいかない場合は、2か3をやり忘れていないか要確認。
I2C通信で温度を測定する
デジタル測温IC
温度を測定するICの種類は大きく2つ。
アナログ出力とデジタル出力。
アナログ出力タイプの測温IC
アナログ出力タイプのICは、温度に応じた電圧の信号を出力する。
例えばアナログデバイセズのTMP3xシリーズだと
ICに電源供給するだけで信号が出力されるので、ICの使い方自体は極めて簡単。
しかし、アナログ信号なので、システムに用いる場合はADCで受けてデジタル変換する必要がある。
デジタル出力タイプの測温IC
デジタル出力タイプのICは温度に応じたデジタル信号を出力する。
IFとしてI2C、SPIが一般的。
今回はI2CのMCP9808を使用。
MCP9808のデジタル信号出力は以下。
データとして2バイト出力される。
デジタル出力タイプのICは、電源供給するだけでデジタル信号は出力されない。
デジタル出力タイプのICを使用するには、
IFのプロトコル(今回はI2C)とICそのものについて
多少の理解が必要。
MCP9808の使い方
I2Cはマスターとスレーブがある。
スレーブは複数ある。
スレーブはそれぞれが異なるアドレスを有する。
マスターが全スレーブに対してアドレス付きのデータ送信し、
指定されたアドレスのスレーブがデータを返送することで
データの送受信が行われる。
MCP9808は常にSlaveとして扱われる。
MCP9808は3bitのSlaveアドレス用端子がある。
Slaveアドレスを8つから選択できる。
マスターがMCP9808へアドレスを書き込む場合は、末尾を0としたデータを送信する。
例えば、アドレスを指定する場合は、以下のデータを送信する。
A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W |
---|---|---|---|---|---|---|---|
0 | 0 | 1 | 1 | x | x | x | 0 |
MCP9808は温度データを出力するだけでなく
アラート信号を出力する温度の閾値や、
出力する温度の分解能なども読み書きできる。
MCP9808との通信
MCP9808から温度データを取得する場合の通信について述べる。
MCP9808のアドレスは0x18とする(A2:0、A1:0、A0:0)
全体の流れは
- マスターがスレーブにアドレスを送信する
- マスターがスレーブに読み出したいデータを指定する
- スレーブがマスターにデータを返送する
手順
1.マスターからスレーブに0x18の末尾に0を付加したデータを送信する。
A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W |
---|---|---|---|---|---|---|---|
0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
2.温度情報を読み出すため、マスターからスレーブへ0x05を送信する。
末尾に0を付加する必要はない。
3.スレーブからデータが2バイト返送される。
SoC FPGAでLチカ --DE10-Nanoを例に--
SoC FPGAとは
SoCFPGAとは、FPGAのチップにCPUの類のものが搭載されているもの。
やったこと
今回行ったのは以下
今回使用したボード
DE10-Nano(IntelのCyclone Vが実装されたもの)
Digikeyで15,000円くらい。カバーがついてて心なしか安心。
書き込みが専用ツールでなく、USBケーブルで出来るのもうれしい。
ハードウェアロジックでLEDを点滅
Quartus Primeを立ち上げて新規プロジェクトを作成する。
ボードは選択しないで、FPGA"5CSEBA6U23I7DK"を選択。
Verilogコードの作成
Verilogのコードは以下。
クロックが50MHzなので、LEDの点滅が目で追えないけど、
スイッチ押せば止まるので気にしない。
module LED_TEST( input I_CLK_50MHZ, input I_SW1, output reg O_LED1, output reg O_LED2 ); reg [15:0] CLKCNT; reg [15:0] HALFCNT; initial begin O_LED1 = 1'b0; O_LED2 = 1'b1; CLKCNT = 16'b0000_0000_0000_0000; HALFCNT = 16'b1000_0000_0000_0000; end always@( posedge I_CLK_50MHZ ) begin CLKCNT = (I_SW1)? CLKCNT + 1:CLKCNT; O_LED1 = (CLKCNT < HALFCNT)? 1:0; O_LED2 = ~O_LED1; end endmodule
ピンアサインの設定
次にVerilogコードのInput、OutputとFPGAのピンを紐づける。
DE10-NanoのUser Manualに記載がある。
とりあえず必要な箇所だけピックアップして以下に示す。
Quattus PrimeのAssignments > Pin Plannerをクリックし、VelirogのInput、outputと各ピンを接続する。
今回は以下のような感じ。
Node NameはVerilogに記載した名称を入力し、LocationにFPGAのピンアサインを入力する。
I/O Standardの箇所で電圧レベルや種類(シングル or 差動等)を選択する。
Directionはコンパイル時に自動で設定される。
コンパイル
Quartus PrimeのProcessing > Start Compilationをクリックし数分待てば書き込みデータが完成。
書き込み
Quartus PrimeのTools > Programmerをクリックすると、Programmerが起動する。
Auto Detectをクリックすると、基板上のメモリとFPGAが表示される。
FPGA上で右クリックし、edit > change fileを選択。
先ほど作成したコンパイルデータを選択する(projectディレクトリ直下のoutput_files内にあるはず)。
Program/Configureにチェックを入れて、Startをクリック。
無事書き込まれると、右上に100%(Successfuss)が表示される。
動作確認
LED0とLED1が点灯している(実際には高速で点滅している)。 Key0スイッチを押すと、一方のみが点灯する。
以上で、ハードウェアロジックでLEDを操作する説明終了。
LinuxからLEDを操作
次はボード上のLinuxからLEDを操作する。
準備
準備として、以下のDE10-Nanoの演習を一通りこなす。
service.macnica.co.jp
上記の5.演習 3は飛ばしてもよい。
ベアメタルアプリケーション開発はライセンス費用が掛かるので、
よほどのプロでない限りは使用しないはず。
Linuxアプリケーションの開発方法
6.演習4でLinuxアプリケーションの演習があるが、
Hello worldを表示できれば、
開発環境が構築されたことを意味する。
ただしHello Worldだけでは物足りないので、
LEDを点灯させてみる。
演習を完了している場合、LED PIOのベースアドレスは0x0001_0040
となっている。
実際にはブリッジのベースアドレスが0xFF20_0000
なので、
0xFF21_0040
である(演習のpdfに記載されている)。
0xFF21_0040
の各bitが各LEDの値に対応している。
レジスタの値を変えるにはmmap
を使用する。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> #include <time.h> #include <sys/time.h> #include <poll.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> #include <stdint.h> #include <errno.h> #define BASE_BRIGDE 0xff200000 #define BASE_LEDPIO 0xff210040 #define DEVNAME "/dev/mem" int main(int argc, char** argv) { int fd; uint32_t from; uint32_t num; uint32_t port; volatile uint8_t *iomap; printf("this is IOmemory test program\n"); fd = open(DEVNAME, (O_RDWR|O_SYNC)); if(fd <= 0){ perror(DEVNAME); exit(1); } from = BASE_BRIGDE; num = 0x20000; iomap = mmap(0, num, PROT_READ|PROT_WRITE, MAP_SHARED, fd, from); if (iomap == MAP_FAILED) { printf("Error : mmap failed. ERRO:%d\n", errno); exit(1); } //LEDを1つだけ点灯させる。プログラム。 port = BASE_LEDPIO; *(uint32_t*)(iomap + port - from) = ~0x01; munmap((void*)iomap, num); close(fd); return 0; }
LEDを右から左、左から右へと順番に消灯したい場合は以下のようなプログラムになる。
port = BASE_LEDPIO; uint8_t ledMask = 0x01; uint8_t ledDir = 0; int loopCount = 0; while(loopCount < 60){ *(uint32_t*)(iomap + port - from) = ~ledMask; usleep(100*1000); if(ledDir == 0){ ledMask <<= 1; if(ledMask == (0x01 << 6)){ ledDir = 1; } }else{ ledMask >>= 1; if(ledMask == 0x01){ ledDir = 0; loopCount++; } } }
RN4870のIOピンの使い方がよくわかんねーっす
RN4870のデータシートにはこんな記載がある。
上から三つ目、P0_2の機能としてAD2との記載がある。
一方、User's guideを見ると
ADコンバータの機能を有するピンにP0_2の記載がない・・・
どういうこと??
とりあえずMicrochipのForumで聞いてみるもなかなか返答がない。
自分の英語がクソすぎるからか?