# UART-SPI READER 實現 組員: A1105107 莊庭勛 指導老師:林宏益 # 一、作業內容 ![image](https://hackmd.io/_uploads/BJ3jMF3wkl.png) 透過UART從電腦傳輸訊號到FPGA板子後,輸入特定的指令讀出NOR-Flash地址裡面的數值 # 二、實驗材料 - NOR-flash - FPGA板 - 麵包版 - 杜邦線 - RS232 - 七段顯示器 # 三、實驗步驟 需要以下的module - UART - receiver - SPI controller ## UART - receiver ### A. UART - receiver 原始(接收1個byte) 首先要透過AccessPort傳送訊號到FPGA,需要用verilog設計一個UART接收器, ![image](https://hackmd.io/_uploads/HyNXBMMFp.png) 要利用UART通訊協議來實現資料傳輸 以下是UART receiver的top module: ```verilog= //TOP MODULE `timescale 1ns / 1ps module rx_top( input clk, input rst, input rx_pin_in, output [7:0] seg7 ); wire [7:0] rx_data; wire rx_band_sig; wire clk_bps; rx_band_gen rx_band_gen( .clk( clk ), .rst( rst ), .band_sig( rx_band_sig ), .clk_bps( clk_bps ) ); H2L_detect rx_in_detect( .clk( clk ), .rst( rst ), .pin_in( rx_pin_in ), .sig_H2L( rx_pin_H2L ) ); rx_ctl rx_ctl( .clk( clk ), .rst( rst ), .rx_pin_in( rx_pin_in ), .rx_band_sig( rx_band_sig ), .rx_clk_bps( clk_bps ), .rx_data( rx_data ), .rx_done_sig( rx_done_sig ) ); ASCII27 ASCII27( .rst( rx_done_sig ), .ascii( rx_data ), //rx_data訊號->ASCII .seg7(seg7) ); endmodule ``` UART receiver主要分成幾個結構: #### 1. rx_band_gen 因為在FPGA板子的工作系統頻率是125M Hz,但是我們使用的Baud rate是**9600bps**去接收資料,所以要做除頻的動作 ```verilog= module rx_band_gen( input clk, input rst, input band_sig, output reg clk_bps ); parameter SYS_RATE = 125000000;//FPGA 之系統頻率 parameter BAND_RATE = 9600; parameter CNT_BAND = SYS_RATE / BAND_RATE; parameter HALF_CNT_BAND = CNT_BAND / 2; reg [13:0]cnt_bps; always @( posedge clk or posedge rst ) if( rst ) begin cnt_bps <= HALF_CNT_BAND; clk_bps <= 1'b0; end else if( !band_sig ) begin cnt_bps <= HALF_CNT_BAND; clk_bps <= 1'b0; end else if( cnt_bps == CNT_BAND ) begin cnt_bps <= 14'd0; clk_bps <= 1'b1; end else begin cnt_bps <= cnt_bps + 1'b1; clk_bps <= 1'b0; end endmodule ``` #### 2. H2L_detect 此模組的目的是為了要去接收start的資料,當接收到start的訊號,rx_ctl的狀態機會進行切換,告訴系統要做接收的動作 ```verilog= module H2L_detect( input clk, input rst, input pin_in, output sig_H2L ); reg pin_pre; assign sig_H2L = !pin_in & pin_pre; always @( posedge clk or posedge rst ) if( rst ) pin_pre <= 1'b0; else pin_pre <= pin_in; endmodule ``` H2L的原理如下圖所示 ![image](https://hackmd.io/_uploads/S1VsoXGY6.png) 先把`pin_in`延後一個週期得到`pin_pre`,則可以發現 再將`!pin_in` 和 `pin_pre`做and即可得到`sig_H2L` #### 3. UART控制器及狀態機 控制器要去控制整個系統的dataflow和狀態切換 這邊的接收器直接使用張家凱同學的控制器: ```verilog= module rx_ctl( input clk, input rst, input rx_pin_in, input rx_pin_H2L, output reg rx_band_sig, input rx_clk_bps, output reg [7:0] rx_data, output reg rx_done_sig ); localparam [4:0] IDLE = 5'b00001, BEGIN = 5'b00010, DATA = 5'b00100, END = 5'b01000, BFREE = 5'b10000; reg [4:0] pos; reg [2:0] data_bit_count; // 計數器用於追蹤數據位 always @(posedge clk or posedge rst) begin if (rst) begin rx_band_sig <= 1'b0; rx_data <= 8'd0; pos <= IDLE; rx_done_sig <= 1'b0; data_bit_count <= 3'd0; end else begin case (pos) // 使用one-hot狀態機 IDLE: begin if (rx_pin_H2L) begin rx_band_sig <= 1'b1; pos <= BEGIN; rx_data <= 8'd0; data_bit_count <= 3'd0; end end BEGIN: begin if (rx_clk_bps) begin if (rx_pin_in == 1'b0) begin pos <= DATA; end else begin rx_band_sig <= 1'b0; pos <= IDLE; end end end DATA: begin if (rx_clk_bps) begin rx_data[data_bit_count] <= rx_pin_in; if (data_bit_count == 3'd7) begin pos <= END; end else begin data_bit_count <= data_bit_count + 1'b1; end end end END: begin if (rx_clk_bps) begin rx_done_sig <= 1'b1; pos <= BFREE; rx_band_sig <= 1'b0; end end BFREE: begin if (rx_clk_bps) begin rx_done_sig <= 1'b0; pos <= IDLE; end end endcase end end endmodule ``` 當在`IDLE`時候: **系統等待H2L_detect模組偵測start(`rx_pin_H2L`)訊號** 當偵測到start後,啟動`rx_band_sig`,系統開始進行接收訊號進入`BEGIN` 當在`BEGIN`時候: **要去檢測起始位是否正確** 在rx_clk_bps到達時 - 若rx_pin_in 是 0,表示起始位有效,進入`DATA` - 若rx_pin_in 是 1,表示起始位無效,回到`IDLE` 當在`DATA`時候: **會按照Baud rate(`rx_clk_bps`)逐漸接收數據** 當`rx_clk_bps`抵達時,將`rx_pin_in`的資料存入`rx_data`,而是`rx_data`的第幾位是由`data_bit_count`決定,當`data_bit_count`接受8個數據接收完畢時,進入`END`狀態。 否則`data_bit_count`逐漸加一接收下一位元 當在`END`時候: **要去結束數據接收並且發送接收完成的訊號** `rx_clk_bps`時,將傳送結束訊號(`rx_done_sig`)、停止接收過程(`rx_band_sig`),清除`rx_band_sig`,並且將狀態切換到`BFREE` 當在`BFREE`時: **確保`rx_done_sig`只維持一個週期** 將`rx_done_sig`清零,並回到`IDLE` #### 4. ASCII轉換seg7 將接受到的訊號`rx_data`轉換成seg7,方可在FPGA板子上面顯示 ```verilog= module ASCII2seg7 ( rst, ASCII, seg7_out ); input rst; input [7:0] ASCII; reg [7:0] seg7; output [7:0] seg7_out; always@( posedge rst ) begin case (ASCII) 8'b0011_0000: seg7<=8'b00000011; 8'b0011_0001: seg7<=8'b10011111; 8'b0011_0010: seg7<=8'b00100101; 8'b0011_0011: seg7<=8'b00001101; 8'b0011_0100: seg7<=8'b10011001; 8'b0011_0101: seg7<=8'b01001001; 8'b0011_0110: seg7<=8'b01000001; 8'b0011_0111: seg7<=8'b00011111; 8'b0011_1000: seg7<=8'b00000001; 8'b0011_1001: seg7=8'b00001001; 8'b0100_0001: seg7=8'b00010001; 8'b0100_0010: seg7=8'b11000001; 8'b0100_0011: seg7=8'b11100101; 8'b0100_0100: seg7=8'b10000101; 8'b0100_0101: seg7=8'b01100001; 8'b0100_0110: seg7=8'b01110001; default: seg7=8'b01100001; endcase end assign seg7_out=seg7; endmodule ``` ![image](https://hackmd.io/_uploads/rJJt0XzFa.png) ### B.UART - receiver (接收2個byte) 因為在使用上我們需要輸入R0~RF 2個Byte的資訊,所以我們需要在UART - receiver做一些修改。 ```verilog = `timescale 1ns / 1ps module rx_ctl ( input clk, input rst, input rx_pin_in, input rx_pin_H2L, output reg rx_band_sig, input rx_clk_bps, output reg [15:0] out_data, output reg rx_done_sig, output seg ); localparam [5:0] IDLE = 6'b000001, BEGIN = 6'b000010, DATA= 6'b000100, END = 6'b001000, BFREE = 6'b010000, FINISH = 6'b100000; reg [5:0] pos; reg [7:0] rx_data; reg [2:0] data_bit_count; reg [1:0] count; reg [15:0] out_data1; always @(posedge clk or posedge rst) if (rst) begin rx_band_sig <= 1'b0; rx_data <= 8'd0; pos <= IDLE; data_bit_count<=3'd0; rx_done_sig <= 1'b0; count <= 2'd0; end else begin case (pos) IDLE: if (rx_pin_H2L) begin rx_done_sig <= 1'b0; rx_band_sig <= 1'b1; pos <= BEGIN; rx_data <= 8'd0; data_bit_count<=3'd0; end BEGIN: if (rx_clk_bps) begin if (rx_pin_in == 1'b0) begin pos <= DATA; end else begin rx_band_sig <= 1'b0; pos <= IDLE; end end DATA: begin if (rx_clk_bps) begin rx_data[data_bit_count] <= rx_pin_in; if (data_bit_count == 3'd7) begin pos <= END; end else begin data_bit_count <= data_bit_count + 1'b1; end end end END: if (rx_clk_bps) begin rx_band_sig <= 1'b0; pos <= BFREE; if (count == 2'd0) begin out_data1 <= {rx_data,out_data1[7:0]}; end else if (count == 2'd1) begin out_data1 <= {out_data1[15:8],rx_data}; end end BFREE: begin if (out_data1[15:8] != 8'h52 && count == 2'd0 ) begin rx_done_sig <= 1'b0; pos <= IDLE; count <= 2'd0; end else begin rx_done_sig <= 1'b0; pos <= FINISH; count <= count + 2'd1; end end FINISH: begin if (count == 2'd2) begin out_data <= out_data1; count <= 2'd0; rx_done_sig <= 1'b1; out_data1 <= 16'd0; end pos <= IDLE; end endcase end endmodule ``` 在原始的`rx_data`是8bit,要將其改成16 bit(因為要接收R0~RF的訊號) 主要的更改在`END`、`BFREE`和多加入`FINISH`狀態 :::info 在原本的`END`是要負責**去結束數據接收並且發送接收完成的訊號** info rx_clk_bps時,將傳送結束訊號(rx_done_sig)、停止接收過程(rx_band_sig),清除rx_band_sig,並且將狀態切換到BFREE ::: 此時的`END`狀態要多負責將讀取後的資料將其放到`out_data`的register裡面,也多了一個`count`的register,此register要去紀錄目前輸入的是第幾個byte - 當`count`為0,代表準備接受第一個byte,則將剛剛的`rx_data`接受到的值放到`out_data1`的前面8個bit,此時接收到的值要會是**R** - 當`count`為1,代表準備接受第二個byte,則將剛剛的`rx_data`接受到的值放到`out_data1`的後面8個bit,此時接收到的值要會是**0~f** :::info 在原本的`BFREE`是要負責**確保rx_done_sig只維持一個週期** 將rx_done_sig清零,並回到IDLE ::: 此時的`BFREE`狀態要檢查`out_data1`的第一個byte是不是為**R** 若不是則代表有錯誤,將`rx_done_sig`和`count`清零並且回到`IDLE` 反之若第一個byte是**R**,則代表使用者輸入的第一個byte是正確的,將其count加一並將狀態切到`FINISH` `FINISH`要去檢查`count`是否為2,若是將`out_data1`的值做輸出,`rx_done_sig`和`count`清零表示完畢 --- 之後再將剛剛的out_data送Cmd_Flash_Logic這一個module 將out_data轉換成address,方便給NOR_Flash做讀取 ```verilog `timescale 1ns / 1ps module Cmd_Flash_Logic( input wire sys_clk, input wire sys_rst_n, input wire rx_done_sig, input wire [15:0] rx_data, output reg start_read, output reg [23:0] addr_offset ); reg [7:0] cmd_byte_1, cmd_byte_2; reg [1:0] byte_count; reg [23:0]addr; always @(*) begin case (cmd_byte_2[7:0]) 8'b0011_0000: addr=24'h000000; 8'b0011_0001: addr=24'h000001; 8'b0011_0010: addr=24'h000002; 8'b0011_0011: addr=24'h000003; 8'b0011_0100: addr=24'h000004; 8'b0011_0101: addr=24'h000005; 8'b0011_0110: addr=24'h000006; 8'b0011_0111: addr=24'h000007; 8'b0011_1000: addr=24'h000008; 8'b0011_1001: addr=24'h000009; 8'b0100_0001: addr=24'h00000A; 8'b0100_0010: addr=24'h00000B; 8'b0100_0011: addr=24'h00000C; 8'b0100_0100: addr=24'h00000D; 8'b0100_0101: addr=24'h00000E; 8'b0100_0110: addr=24'h00000F; default: addr= 24'h00000E; endcase end parameter [1:0]IDLE = 2'd0; parameter [1:0]READ_ADDRESS = 2'd1; parameter [1:0]FINISH = 2'd2; always @(posedge sys_clk or posedge sys_rst_n) begin if(sys_rst_n) begin byte_count <= 2'd0; cmd_byte_1 <= 8'd0; cmd_byte_2 <= 8'd0; start_read <= 1'd0; end else begin if(rx_done_sig) begin case(byte_count) IDLE: begin start_read <= 1'd0; cmd_byte_1 <= rx_data[15:8]; byte_count <= 2'd1; end READ_ADDRESS: begin cmd_byte_2 <= rx_data[7:0]; byte_count <= 2'd2; if(cmd_byte_1 == 8'h52) begin // 0x52='R' if(cmd_byte_2 >= 8'h30 && cmd_byte_2 <= 8'h39) begin addr_offset <= addr ; end else if(cmd_byte_2 >= 8'h41 && cmd_byte_2 <= 8'h46) begin addr_offset <= addr ; end else begin addr_offset <= 24'h00000E; end end end FINISH: begin start_read <= 1'd1; byte_count <= IDLE; end default: byte_count <= IDLE; endcase end end end endmodule ``` 將剛剛control中的`out_data`(在此模組叫做`rx_data`)用`cmd_byte_1`和`cmd_byte_2`去分割出**R**和**數字**的部分 在`READ_ADDRESS`的階段時, 先確認第一個byte是否為**R**,若不是則`addr_offset`顯示為24'h00000E,表示error 若是R,則確認第二個byte是否在0~f的值,如果是在將其正確的值做輸出,放到`addr_offset` 將全部做完後,在`FINISH`將state回到`IDLE`還有將`start_read`設定為1,告訴後續的SPI controller可以進行讀取的動作。 ## SPI Controller ![image](https://hackmd.io/_uploads/H1QCt32Dye.png) SPI要將剛剛讀取的資料`rx_data`透過`MOSI`傳到NOR Flash做溝通 再透過`MISO`將NOR Flash的資料回傳到FPGA板,再將其顯示到七段顯示器 ### SPI_clock除頻 首先,因為SPI的工作頻率是在1M Hz,而FPGA的工作頻率是125M Hz,所以如同UART的部分,要對其做除頻。如下: ``` verilog module SPI_clock ( input clk, input rst, output reg spclk ); parameter SYS_RATE = 125000000; parameter BAND_RATE = 1000000; parameter CNT_BAND = 125; parameter HALF_CNT_BAND = 62; reg [31:0] cnt_bps; always @(posedge clk or posedge rst) begin if (rst) begin cnt_bps <= 32'd0; spclk <= 1'b0; end else if (cnt_bps == HALF_CNT_BAND - 1) begin cnt_bps <= 32'd0; spclk <= ~spclk; end else begin cnt_bps <= cnt_bps + 1'b1; end end endmodule ``` --- ### SPI control ![image](https://hackmd.io/_uploads/rkK1Sa3Dyl.png) 首先在NOR_Flash的指令要發送一個0X03h的CMD,NOR Flash才知道要做Read 所以下面有定義一個`READ_CMD` = 8'h03的指令,因為`MOSI`是要傳入訊號給到NOR flash,所以在`CMD`的state時 ``` CMD: MOSI <= READ_CMD[7 - bit_cnt]; CMD2: MOSI <= READ_CMD[0]; ``` 此2行的目的就是為了將8'h03的CMD傳入NOR Flash 而接下來就是傳入address,也是透過`MOSI`傳入 ``` ADDR: MOSI <= addr_offset[23 - bit_cnt]; ADDR2: MOSI <= addr_offset[23]; ``` 而當`MOSI`全部傳送完畢後,state切到`READ` 從NOR Flash讀取資料,並透過`MISO`傳輸,並且將其紀錄在`read_reg`的register之中 ``` else if(current_state==READ) begin // bit_cnt=0 => 先收最高位(7) read_reg[7 - bit_cnt] <= MISO; end ``` 傳送完畢後,state切到`DONE` 並將`read_reg`的資訊傳送到`data_out`做輸出 而`CS_n`則是按照規格去做制定,當`IDLE`以及`DONE`時,`CS_n`才會為1 規格如下圖所示 ![image](https://hackmd.io/_uploads/SkmlYphwJl.png) SPI control完整的code如下所示 ```verilog //==================================================================== //SPI_control_read_only // 0x03 (CMD + ADDR + 1 Byte DATA) //==================================================================== module SPI_control_read_only( input wire sys_clk, input wire sys_rst_n, input wire start_read, input wire [23:0] addr_offset, input wire spclk, input wire MISO, output reg MOSI, output wire SPI_CLK, output reg CS_n, output reg [7:0] data_out ); localparam IDLE = 3'd0; localparam CMD = 3'd1; // 發送 0x03 localparam CMD2 = 3'd2; localparam ADDR = 3'd3; // 發送 24-bit addr localparam ADDR2 = 3'd4; localparam READ = 3'd5; // 讀 1 Byte localparam DONE = 3'd6; reg [2:0] current_state, next_state; reg [5:0] bit_cnt;// reg [7:0] read_reg; localparam [7:0] READ_CMD = 8'h03; // 狀態轉移 always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) current_state <= IDLE; else current_state <= next_state; end always @(*) begin next_state = current_state; case(current_state) IDLE: if(start_read) next_state = CMD; else next_state = IDLE; CMD : if(bit_cnt == 6'd7) next_state = CMD2; else next_state = CMD; CMD2 : if(bit_cnt == 6'd0) next_state = ADDR; else next_state = CMD2; ADDR: if(bit_cnt == 6'd23) next_state = ADDR2; else next_state = ADDR; ADDR2: if(bit_cnt == 6'd0) next_state = READ; else next_state = ADDR2; READ: if(bit_cnt == 6'd7) next_state = DONE; else next_state = READ DONE: next_state = IDLE; endcase end // bit_cnt always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) bit_cnt <= 6'd0; else begin case(current_state) CMD: if(bit_cnt==6'd7) bit_cnt<=0; else bit_cnt<= bit_cnt+1'b1; ADDR: if(bit_cnt==6'd23) bit_cnt<=0; else bit_cnt<= bit_cnt+1'b1; READ: if(bit_cnt==6'd7) bit_cnt<=0; else bit_cnt<= bit_cnt+1'b1; default: bit_cnt <= 0; endcase end end // MOSI always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) begin MOSI <= 1'b0; end else begin case(next_state) CMD: MOSI <= READ_CMD[7 - bit_cnt]; CMD2: MOSI <= READ_CMD[0]; ADDR: MOSI <= addr_offset[23 - bit_cnt]; ADDR2: MOSI <= addr_offset[23]; default: MOSI <= 1'b0; endcase end end // 從 MISO 讀資料 always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) begin read_reg <= 8'd0; end else if(current_state==READ) begin // bit_cnt=0 => 先收最高位(7) read_reg[7 - bit_cnt] <= MISO; end end // CS_n 控制 always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) CS_n <= 1'b1; else begin case(next_state) CMD : CS_n <= 1'b0; CMD2 : CS_n <= 1'b0; ADDR : CS_n <= 1'b0; ADDR2 : CS_n <= 1'b0; READ : CS_n <= 1'b0; default: CS_n <= 1'b1; endcase end end // data_ready & data_out always @(posedge spclk or posedge sys_rst_n) begin if(sys_rst_n) begin data_out <= 8'd0; end else begin if(current_state==DONE) begin data_out <= read_reg; end end end assign SPI_CLK = spclk; endmodule ``` # 四、實驗數據 ## UART - receiver(接收8bit) ::: success 影片連結: https://studio.youtube.com/video/8nWxnUkTYok/edit ::: ## UART - receiver (接收16bit) ![image](https://hackmd.io/_uploads/BJV3ca3w1g.png) ::: success 影片連結: https://studio.youtube.com/video/kvPTIJ1mfKo/edit ::: ## SPI - controller ![image](https://hackmd.io/_uploads/HJQyj6nvyl.png) ## UART-SPI READER ::: success 影片連結: https://studio.youtube.com/video/sVkDUDID5e8/edit ::: # 五、心得 在這次的期末作業學習到UART和SPI的通訊協定,為了將傳送2 byte的資料,在前面UART的部分就花了許多時間下去改,最後還是透過和學長討論及參考才將UART的部分先行完成,再來就是處理SPI的部分,這個部分在剛開始寫的時候也是毫無頭緒,這個訊號什麼要升起來,什麼時候要掉下去,光是在處理狀態機的部分想了很久。 中間也需要看NOR flash的手冊才知道部分的指令是什麼,但是最難的是,知道這邊指令要用哪一個但是要如何將其實現並且這個指令出現在正確的時間才是最困難的。 這個部分也是有看過助教的simulation才慢慢推敲出來,即便看到模擬正確,但是在接上板子後錯的機會還是很大,大部分的問題都是來自哪個訊號慢來或是早來的原因。 總的來說這次作業也是花了一些時間下來做 額外學到的東西更多像是HackMD的使用和指令的運用,這篇報告也是花了蠻多時間才將其整理出來 # 六、參考資料 :::warning - 學長HackMD - SPI_introduction - NOR Flash規格書 :::