FPGA實現串列埠與iic控制器總結(1)
在剖析了《深入淺出玩轉FPGA》的串列埠程式碼和IIC控制器程式碼、xilinx官方的xilinx的iic控制器(參見書《FPGACPLD設計工具──Xilinx ISE使用詳解》)、《片上系統設計思想與原始碼分析》一書中帶有wishbone介面的iic控制器後,本文嘗試對以上做一些總結,並分析不同的iic控制器的實現區別。
1、串列埠
該章節程式碼來源於《深入淺出玩轉FPGA》深入淺出的相關章節。
改程式碼實現的例子是串列埠以9600的波特率接受從電腦傳來的一個數據,然後立馬發回電腦。根據頂層框圖可以發現rx的輸出資料介面是接在tx的輸入介面的。所以這並不是一個完整的全功能的串列埠,是一個閹割板的仿串列埠協議的收發,並且沒有用狀態機實現。
整體框圖:
4個檔案:
speed_selectspeed_rx(
.clk(clk),//波特率選擇模組
.rst_p(rst_p),
.bps_start(bps_start1),
.clk_bps(clk_bps1)
);
my_uart_rxmy_uart_rx(
.clk(clk),//接收資料模組
.rst_p(rst_p),
.rs232_rx(rs232_rx),
.rx_data(rx_data),
.rx_int(rx_int),
.clk_bps(clk_bps1),
.bps_start(bps_start1)
);
///////////////////////////////////////////
speed_selectspeed_tx(
.clk(clk) ,//波特率選擇模組
.rst_p(rst_p),
.bps_start(bps_start2),
.clk_bps(clk_bps2)
);
my_uart_txmy_uart_tx(
.clk(clk),//傳送資料模組
.rst_p(rst_p),
.rx_data(rx_data),
.rx_int(rx_int),
.rs232_tx(rs232_tx),
.clk_bps(clk_bps2),
.bps_start(bps_start2)
);
speed_select模組中:
一根輸入線受外部限制,在if判斷中作為是否有必要啟動計時器的標誌。定義了一個計時器,將在2603個計時週期處輸出改變clk_bps_r變數,作為取樣脈衝。同時已經計算好了在一個數據位的正中心取樣。我們考慮為9600bps,這樣算下來每個資料位的時間是多少,再結合自己板子的時鐘,決定應該算多少個週期。
speed模組例化了2次,在接受和傳送都可以設定選用不同的波特率。已經定義為變數,只需選擇不同的值即可。
my_uart_rx模組中:
input clk; // 50MHz主時鐘
input rst_p; //高電平復位訊號
input rs232_rx; // RS232接收資料訊號
input clk_bps; // clk_bps的高電平為接收或者傳送資料位的中間取樣點,spped模組就是專門產生這麼一個取樣脈衝的
output bps_start; //接收到資料後,波特率時鐘啟動訊號置位
output[7:0] rx_data; //接收資料暫存器,儲存直至下一個資料來到
output rx_int; //接收資料中斷訊號,接收到資料期間始終為高電平
clk_bps即為speed模組中傳來的取樣脈衝訊號,bps_start決定是否有必要啟動那個計時模組。
進來首先可以看到是對rs232_rx訊號的一個濾波:
//----------------------------------------------------------------
reg rs232_rx0,rs232_rx1,rs232_rx2,rs232_rx3; //接收資料暫存器,濾波用
wire neg_rs232_rx; //表示資料線接收到下降沿
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
rs232_rx0 <= 1'b0;
rs232_rx1 <= 1'b0;
rs232_rx2 <= 1'b0;
rs232_rx3 <= 1'b0;
end
else begin //打了4拍
rs232_rx0 <= rs232_rx;
rs232_rx1 <= rs232_rx0;
rs232_rx2 <= rs232_rx1;
rs232_rx3 <= rs232_rx2;
end
end
//因為串列埠的起始訊號是一個下降沿
//參考按鍵消抖,畫圖發現可以檢測到下降沿,如果正常的一個下降沿可以產生一個時長為20ns的瞬時高脈衝,
//但是如果是20ns-40ns的毛刺就無法產生這個了
//下面的下降沿檢測可以濾掉<20ns-40ns的毛刺(包括高脈衝和低脈衝毛刺),
//這裡就是用資源換穩定(前提是我們對時間要求不是那麼苛刻,因為輸入訊號打了好幾拍)
//(當然我們的有效低脈衝訊號肯定是遠遠大於40ns的)
assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; //接收到下降沿後neg_rs232_rx置高一個時鐘週期
可以看到實際上類似與按鍵去抖,可以自己去畫畫隨著clk演變的圖,分析一下對什麼樣的噪聲有效。這是一種常見的處理方式。
根據串列埠的協議,起始位是一個低電平,所以我們要檢測下降沿。
always @ (posedge clk or posedge rst_p)
if(rst_p) begin
bps_start_r <= 1'bz; //?
rx_int <= 1'b0;
end
else if(neg_rs232_rx) begin //接收到串列埠接收線rs232_rx的下降沿標誌訊號
bps_start_r <= 1'b1; //啟動串列埠準備資料接收
rx_int <= 1'b1; //接收資料中斷訊號使能
end
else if(num==4'd12) begin //接收完有用資料資訊
bps_start_r <= 1'b0; //資料接收完畢,釋放波特率啟動訊號
rx_int <= 1'b0; //接收資料中斷訊號關閉
end
assign bps_start = bps_start_r;
檢測到rx上有效的低電平,置位這2個訊號,啟動計數器模組開始準備技術取樣資料,同時表明正在接收資料。當一幀資料完了(12位),則釋放。
//----------------------------------------------------------------
reg[7:0] rx_data_r; //串列埠接收資料暫存器,儲存直至下一個資料來到
//----------------------------------------------------------------
reg[7:0] rx_temp_data; //當前接收資料暫存器
always @ (posedge clk or posedge rst_p)
if(rst_p) begin
rx_temp_data <= 8'd0;
num <= 4'd0;
rx_data_r <= 8'd0;
end
else if(rx_int) begin //接收資料處理 這個地方體現出對串列埠波特率的啟動
if(clk_bps) begin //這個地方顯然不能把clk_bps放到always的括號裡面,因為在speed模組中分析了其實
//讀取並儲存資料,接收資料為一個起始位,8bit資料,1或2個結束位
num <= num+1'b1;
case (num)
4'd1: rx_temp_data[0] <= rs232_rx; //鎖存第0bit
4'd2: rx_temp_data[1] <= rs232_rx; //鎖存第1bit
4'd3: rx_temp_data[2] <= rs232_rx; //鎖存第2bit
4'd4: rx_temp_data[3] <= rs232_rx; //鎖存第3bit
4'd5: rx_temp_data[4] <= rs232_rx; //鎖存第4bit
4'd6: rx_temp_data[5] <= rs232_rx; //鎖存第5bit
4'd7: rx_temp_data[6] <= rs232_rx; //鎖存第6bit
4'd8: rx_temp_data[7] <= rs232_rx; //鎖存第7bit
default: ;
endcase
end
else if(num == 4'd12) begin //我們的標準接收模式下只有1+8+1(2)=11bit的有效資料
num <= 4'd0; //接收到STOP位後結束,num清零
rx_data_r <= rx_temp_data; //把資料鎖存到資料暫存器rx_data中
end
end
assign rx_data = rx_data_r;
rx_int作為這個always中的if的啟動訊號,按照時鐘脈衝鎖存資料,當收滿一幀(包括奇偶校驗和停止位),num清0,傳出rx的值。
記住整個過程都是多個並行的always塊,是如何實現流程控制的?
即是通過if判斷中的是否滿足條件來實現的,如:
else if(neg_rs232_rx) begin //等到有效的下降沿啟動訊號
if(num==4’d12) begin //相當與for迴圈
if(clk_bps) begin //等待speed模組中的clk計數器到那一刻
tx模組中:
input clk; // 50MHz主時鐘
input rst_p; //高電平復位訊號
input clk_bps; // clk_bps_r高電平為接收資料位的中間取樣點,同時也作為傳送資料的資料改變點
input[7:0] rx_data; //接收資料暫存器
input rx_int; //接收資料中斷訊號,接收到資料期間始終為高電平,在該模組中利用它的下降沿來啟動串列埠傳送資料(下降沿表示資料接受完了)
output rs232_tx; // RS232傳送資料訊號
output bps_start; //接收或者要傳送資料,波特率時鐘啟動訊號置位
rx_int是在rx中定義的,還在接受資料則為1,接受完了即為0 ,這個地方即是通過這種訊號的傳遞來實現是先收滿一個數據後再發出來一個數據
還是先是一個濾波,可以分析下他是怎麼讓訊號繼續保持一個時鐘週期的
//---------------------------------------------------------
reg rx_int0,rx_int1,rx_int2; //rx_int訊號暫存器,捕捉下降沿濾波用
wire neg_rx_int; // rx_int下降沿標誌位
//同樣是一個消抖
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
rx_int0 <= 1'b0;
rx_int1 <= 1'b0;
rx_int2 <= 1'b0;
end
else begin
rx_int0 <= rx_int;
rx_int1 <= rx_int0;
rx_int2 <= rx_int1;
end
end
assign neg_rx_int = ~rx_int1 & rx_int2; //捕捉到下降沿後,neg_rx_int拉高保持一個主時鐘週期
接下來是把資料從rx中傳到tx中去
//---------------------------------------------------------
reg[7:0] tx_data; //待發送資料的暫存器
//---------------------------------------------------------
reg bps_start_r;
reg tx_en; //傳送資料使能訊號,高有效
reg[3:0] num;
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
bps_start_r <= 1'bz;
tx_en <= 1'b0;
tx_data <= 8'd0;
end
else if(neg_rx_int) begin //接收資料完畢,準備把接收到的資料發回去
bps_start_r <= 1'b1;
tx_data <= rx_data; //把接收到的資料存入傳送資料暫存器(整個暫存器一個週期就全部賦值過去了?)
tx_en <= 1'b1; //進入傳送資料狀態中
end
else if(num==4'd11) begin //資料傳送完成,復位
bps_start_r <= 1'b0;
tx_en <= 1'b0;
end
end
assign bps_start = bps_start_r;
思路與rx一致,同樣也定義了一個tx_en訊號來表徵是否發完了。注意這個時候num==11。
接下來吧資料發出去,思路與rx一致:
//---------------------------------------------------------
reg rs232_tx_r;
always @ (posedge clk or posedge rst_p) begin
if(rst_p) begin
num <= 4'd0;
rs232_tx_r <= 1'b1;
end
else if(tx_en) begin
if(clk_bps) begin
num <= num+1'b1;
case (num)
4'd0: rs232_tx_r <= 1'b0; //傳送起始位
4'd1: rs232_tx_r <= tx_data[0]; //傳送bit0
4'd2: rs232_tx_r <= tx_data[1]; //傳送bit1
4'd3: rs232_tx_r <= tx_data[2]; //傳送bit2
4'd4: rs232_tx_r <= tx_data[3]; //傳送bit3
4'd5: rs232_tx_r <= tx_data[4]; //傳送bit4
4'd6: rs232_tx_r <= tx_data[5]; //傳送bit5
4'd7: rs232_tx_r <= tx_data[6]; //傳送bit6
4'd8: rs232_tx_r <= tx_data[7]; //傳送bit7
4'd9: rs232_tx_r <= 1'b1; //傳送結束位
default: rs232_tx_r <= 1'b1;
endcase
end
else if(num==4'd11) num <= 4'd0; //復位
end
end
assign rs232_tx = rs232_tx_r;
其實完整梳理下來思路很清晰,沒有用狀態機依舊實現了串列埠的功能,後面可以試試如何改為分部的,連續接受後存到一個檔案中。
或者把別的部分怎麼加進來。
因為不像軟體程式碼的順序執行,這個是併發執行的,所以還是有差別的。
下面說說testbench,依舊是參見特權的例子:
關於模擬訊號很多,所以需要弄懂整個設計思路,再來分析訊號的變化。自己之前犯了個低階錯誤,ml605的板子是200MHZ的,但是自己的`timescale 1ns / 1ps在modelsim中根本產生不了200MHZ的時鐘,後面分析計數器的計數值是對的,但是算總的延時時間不對才定位到這裡。所以一定要小心,一點點排查,有耐心聚焦,不要一下子看到這麼多訊號懵逼了。
testbench的原理我就不講了,訊號線應該怎麼接,是reg還是wire型。
特權的程式碼裡面封裝了一些常見的task:
//-----------------------------------------
//常用資訊列印任務封裝
//-----------------------------------------
//警告資訊列印任務
task warning;
input[2*8:1] msg;
begin
$write("WARNING at %t : %s \n",$time,msg);
end
endtask
//錯誤資訊列印任務
task error;
input[20*8:1] msg;
begin
$write("ERROR at %t:%s \n",$time,msg);
end
endtask
//致命錯誤列印並停止模擬任務
task fatal;
input[20*8:1] msg;
begin
$write("FATAL at %t : %s",$time,msg);
$write("Simulation false\n");
$stop;
end
endtask
//完成模擬任務
task terminate;
begin
$write("Simulation Successful\n");
$stop;
end
endtask
呼叫系統命令,最後會在modelsim串列埠打出來。注意task的呼叫方法
這裡做了遍歷測試和隨機測試:
//遍歷測試
for(cnt=255;cnt>0;cnt=cnt-1) //順次傳送0-255
begin
tx_task(cnt); //傳送資料
@(negedge rx_flag); //表示等到這個訊號的下降沿再進行下一步。等待接收到的資料
//這個地方是要等串列埠接收完。185行是一個always塊一直在檢
//測是否收到資料,裡面的rx_flag表示是否收完
if(data_temp ==cnt)
$write("transmit:%d, receive:%d; ture\n",cnt,data_temp); //自收發資料正確
else begin
$write("transmit:%d, receive:%d; error\n",cnt,data_temp); //自收發資料錯誤
error("false");
end
end
#10_000; //10us延時
//隨機測試
for(cnt=1; cnt<255; cnt=cnt+1) //順次傳送0-255
begin
tx_data = {$random};
tx_task(tx_data); //傳送隨機資料
@(negedge rx_flag); //等待接收到的資料
if(data_temp ==tx_data)
$write("transmit:%d, receive:%d; ture\n",cnt,data_temp); //自收發資料正確
else begin
$write("transmit:%d, receive:%d; error\n",cnt,data_temp); //自收發資料錯誤
error("false");
end
end
terminate;
end
這裡呼叫了tx_task。注意@(negedge rx_flag);的用法,因為是模擬語句,並不需要可綜合。這表示在此處一致等等到rx_flag的下降沿,順序結構。
//串列埠傳送任務,是主動的,所以只需要一個task,我們主動去呼叫就可以了
task tx_task;
input[7:0] txdata; //傳送資料輸入
integer i;
begin
rs232_rx = 0; //起始位
#tx_bps;
for(i=0;i<8;i=i+1) //8位資料傳送
begin
rs232_rx = txdata[7-i];
#tx_bps;
end
rs232_rx = 1; //停止位
#tx_bps;
end
endtask
integer j;
//串列埠接收,是被動的,所以一直要用always塊去檢測是否有資料發上來
always @(negedge rs232_tx) //起始位檢測
begin
#(tx_bps/2);
if(rs232_tx == 0)
begin
rx_flag = 1;
#tx_bps;
for(j=0;j<8;j=j+1)
begin
data_temp[7-j] = rs232_tx;
#tx_bps;
end
rx_flag = 0;
end
end
其實整體思路還是比較清晰的。
與c語言差別很大,如何實現各種結構,需要積累經驗。然後就是比較verilog要實現的東西一定要想清楚怎麼去實現,劃分成那幾個功能模組,是否用狀態機,內部應定義那些訊號來實現各模組間的控制和資料傳遞。這需要多去聯絡來增強感覺。
關於iic留到一下講,更精彩!
lijiuyang
4-28夜於武昌藍巢逸品