本帖最后由 小梅哥 于 2015-4-8 18:18 编辑
1.2Uart_Byte_TxUart_Byte_Tx为字节发送模块,该模块在波特率时钟的节拍下,依照UART通信协议发送一个完整的字节的数据。当一个字节发送完毕后,Tx_Done产生一个高脉冲信号,以告知其它模块或逻辑一个字节的数据已经传输完成,可以开始下一个字节的发送了。其发送一个字节数据的实现代码如下: [tr] [td=553] 33 /*计数波特率时钟,11个波特率时钟为一次完整的数据发送过程*/ 34 always@(posedge Clk or negedge Rst_n) 35 if(!Rst_n) 36 Bps_Clk_Cnt <= 4'b0; 37 else if(Bps_Clk_Cnt == 4'd11) 38 Bps_Clk_Cnt <= 4'b0; 39 else if(Bps_Clk) 40 Bps_Clk_Cnt <= Bps_Clk_Cnt + 1'b1; 41 else 42 Bps_Clk_Cnt <= Bps_Clk_Cnt; 43 44 /*生成数据发送完成标志信号*/ 45 always@(posedge Clk or negedge Rst_n) 46 if(!Rst_n) 47 Tx_Done <= 1'b0; 48 else if(Bps_Clk_Cnt == 4'd11) 49 Tx_Done <= 1'b1; 50 else 51 Tx_Done <= 1'b0; 52 53 /*在开始发送起始位的时候就读取并寄存Data_Byte,以免Data_Byte变化导致数据的丢失*/ 54 always@(posedge Clk or negedge Rst_n) 55 if(!Rst_n) 56 Data = 8'd0; 57 else if(Bps_Clk & Bps_Clk_Cnt == 4'd1) 58 Data <= Data_Byte; 59 else 60 Data <= Data; 61 62 /*发送数据序列机*/ 63 always@(posedge Clk or negedge Rst_n) 64 if(!Rst_n) 65 Rs232_Tx <= 1'b1; 66 else begin 67 case(Bps_Clk_Cnt) 68 4'd1: Rs232_Tx <= 1'b0; 69 4'd2: Rs232_Tx <= Data[0]; 70 4'd3: Rs232_Tx <= Data[1]; 71 4'd4: Rs232_Tx <= Data[2]; 72 4'd5: Rs232_Tx <= Data[3]; 73 4'd6: Rs232_Tx <= Data[4]; 74 4'd7: Rs232_Tx <= Data[5]; 75 4'd8: Rs232_Tx <= Data[6]; 76 4'd9: Rs232_Tx <= Data[7]; 77 4'd10: Rs232_Tx <= 1'b1; 78 default:Rs232_Tx <= 1'b1; 79 endcase 80 end [/td] [/tr][/table]在UART协议中,一个完整的字节包括一位起始位、8位数据位、一位停止位即总共十位数据,那么,要想完整的实现这十位数据的发送,就需要11个波特率时钟脉冲,如下所示:
BPS_CLK信号的第一个上升沿到来时,字节发送模块开始发送起始位,接下来的2到9个上升沿,发送8个数据位,第10个上升沿到第11个上升沿为停止位的发送。
单个串口接收模块中实现串口数据接收的主要代码如下所示:
025 always @ (posedge Clk or negedge Rst_n) 026 if(!Rst_n) begin 027 Rs232_Rx0 <= 1'b0; 028 Rs232_Rx1 <= 1'b0; 029 Rs232_Rx2 <= 1'b0; 030 Rs232_Rx3 <= 1'b0; 031 end 032 else begin 033 Rs232_Rx0 <= Rs232_Rx; 034 Rs232_Rx1 <= Rs232_Rx0; 035 Rs232_Rx2 <= Rs232_Rx1; 036 Rs232_Rx3 <= Rs232_Rx2; 037 end 038 039 wire neg_Rs232_Rx= Rs232_Rx3 & Rs232_Rx2 & ~Rs232_Rx1 & ~Rs232_Rx0; 040 041 assign Byte_En = neg_Rs232_Rx; 042 043 /*----------计数采样时钟--------------*/ 044 /*9倍波特率采样时钟,故一个完整的接收过程有90个波特率时钟*/ 045 reg[6:0]Sample_Clk_Cnt; 046 always @ (posedge Clk or negedge Rst_n) 047 if(!Rst_n) 048 Sample_Clk_Cnt <= 7'd0; 049 else if(Sample_Clk)begin 050 if(Sample_Clk_Cnt == 7'd89) 051 Sample_Clk_Cnt <= 7'd0; 052 else 053 Sample_Clk_Cnt <= Sample_Clk_Cnt + 1'b1; 054 end 055 else 056 Sample_Clk_Cnt <= Sample_Clk_Cnt; 057 058 reg [1:0]Start_Bit; /*起始位,这里虽然定义,但并未使用该位来判断接收数据的正确性,即默认接收都是成功的*/ 059 reg [1:0]Stop_Bit; /*停止位,这里虽然定义,但并未使用该位来判断接收数据的正确性,即默认接收都是成功的*/ 060 reg [1:0] Data_Tmp[7:0];/*此部分较为复杂,请参看说明文档中相关解释*/ 061 062 always @ (posedge Clk or negedge Rst_n) 063 if(!Rst_n)begin 064 Data_Tmp[0] <= 2'd0; 065 Data_Tmp[1] <= 2'd0; 066 Data_Tmp[2] <= 2'd0; 067 Data_Tmp[3] <= 2'd0; 068 Data_Tmp[4] <= 2'd0; 069 Data_Tmp[5] <= 2'd0; 070 Data_Tmp[6] <= 2'd0; 071 Data_Tmp[7] <= 2'd0; 072 Start_Bit <= 2'd0; 073 Stop_Bit <= 2'd0; 074 end 075 else if(Sample_Clk)begin 076 case(Sample_Clk_Cnt) 077 7'd0: 078 begin 079 Data_Tmp[0] <= 2'd0; 080 Data_Tmp[1] <= 2'd0; 081 Data_Tmp[2] <= 2'd0; 082 Data_Tmp[3] <= 2'd0; 083 Data_Tmp[4] <= 2'd0; 084 Data_Tmp[5] <= 2'd0; 085 Data_Tmp[6] <= 2'd0; 086 Data_Tmp[7] <= 2'd0; 087 Start_Bit <= 2'd0; 088 Stop_Bit <= 2'd0; 089 end 090 7'd3,7'd4,7'd5: Start_Bit <= Start_Bit + Rs232_Rx; 091 7'd12,7'd13,7'd14:Data_Tmp[0] <= Data_Tmp[0] + Rs232_Rx; 092 7'd21,7'd22,7'd23:Data_Tmp[1] <= Data_Tmp[1] + Rs232_Rx; 093 7'd30,7'd31,7'd32:Data_Tmp[2] <= Data_Tmp[2] + Rs232_Rx; 094 7'd39,7'd40,7'd41:Data_Tmp[3] <= Data_Tmp[3] + Rs232_Rx; 095 7'd48,7'd49,7'd50:Data_Tmp[4] <= Data_Tmp[4] + Rs232_Rx; 096 7'd57,7'd58,7'd59:Data_Tmp[5] <= Data_Tmp[5] + Rs232_Rx; 097 7'd66,7'd67,7'd68:Data_Tmp[6] <= Data_Tmp[6] + Rs232_Rx; 098 7'd75,7'd76,7'd77:Data_Tmp[7] <= Data_Tmp[7] + Rs232_Rx; 099 7'd84,7'd85,7'd86:Stop_Bit <= Stop_Bit + Rs232_Rx; 100 default:; 101 endcase 102 end 103 else ;
根据串口发送协议,一个字节的数据传输是以一个波特率周期的低电平作为起始位的,因此,成功接收UART串口数据的核心就是准确检测起始位。由于外部串口发送过来的数据与接收系统不在同一个时钟域,因此不能直接使用该信号的下降沿来作为检测标志,我们需要在fpga中,采用专用的边沿检测电路来实现,第25行至37行通过四个移位寄存器,存储连续四个时钟上升沿时外部发送数据线的状态,第39行通过比较前两个时钟时数据线的状态与后两个时钟时数据线的状态,来得到该数据线的准确下降沿,以此保证起始位的准确检测。 在简单的串口接收中,我们通常选取一位数据的中间时刻进行采样,因为此时数据最稳定,但是在工业环境中,存在着各种干扰,在干扰存在的情况下,如果采用传统的中间时刻采样一次的方式,采样结果就有可能受到干扰而出错。为了滤除这种干扰,这里采用多次采样求概率的方式。如下图,将一位数据平均分成9个时间段,对位于中间的三个时间段进行采样。然后对三个采样结果进行统计判断,如果某种电平状态在三次采样结果中占到了两次及以上,则可以判定此电平状态即为正确的数据电平。例如4、5、6时刻采样结果分别为1、1、0,那么就取此位解码结果为1,否则,若三次采样结果为0、1、0,则解码结果就为0。
因为采样一位需要9个时钟上升沿,因此,采样一个完整的数据需要10*9,即90个时钟上升沿,这里,采样时钟为波特率时钟的9倍。产生采样时钟的部分代码如下所示:
089 /*-------波特率时钟生成定时器--------------*/ 090 always@(posedge Clk or negedge Rst_n) 091 if(!Rst_n) 092 Count <= 10'd0; 093 else if(BPS_EN == 1'b0) 094 Count <= 10'd0; 095 else begin 096 if(Count == BPS_PARA) 097 Count <= 10'd0; 098 else 099 Count <= Count + 1'b1; 100 end 101 102 //===================================================== 103 /*输出数据接收采样时钟*/ 104 always @(posedge Clk or negedge Rst_n) 105 if(!Rst_n) 106 Sample_Clk <= 1'b0; 107 else if(Count== 1) 108 Sample_Clk <= 1'b1; 109 else 110 Sample_Clk <= 1'b0;
这里,BPS_PARA的计算原理和前面Tx_Bps_Gen模块中的BPS_PARA的计算原理一致,不过这里,因为采样时钟为波特率时钟的9倍,所以,BPS_PARA为Tx_Bps_Gen模块中的BPS_PARA的1/9。计算BPS_PARA的相关代码如下:
018 parameter system_clk = 50_000_000; /*输入时钟频率设定,默认50M*/ 019 020 /*根据输入时钟频率计算生成各波特率时分频计数器的计数最大值*/ 021 localparam bps9600 = system_clk/9600/9 - 1; 022 localparam bps19200 = system_clk/19200/9 - 1; 023 localparam bps38400 = system_clk/38400/9 - 1; 024 localparam bps57600 = system_clk/57600/9 - 1; 025 localparam bps115200 = system_clk/115200/9 - 1; 026 localparam bps230400 = system_clk/230400/9 - 1; 027 localparam bps460800 = system_clk/460800/9 - 1; 028 localparam bps921600 = system_clk/921600/9 - 1; 029 030 reg [31:0]BPS_PARA;/*波特率分频计数器的计数最大值*/ 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 BPS_PARA <= bps9600; /*复位时波特率默认为9600bps*/ 035 end 036 else begin 037 case(Baud_Set) /*根据波特率控制信号选择不同的波特率计数器计数最大值*/ 038 3'd0: BPS_PARA <= bps9600; 039 3'd1: BPS_PARA <= bps19200; 040 3'd2: BPS_PARA <= bps38400; 041 3'd3: BPS_PARA <= bps57600; 042 3'd4: BPS_PARA <= bps115200; 043 3'd5: BPS_PARA <= bps230400; 044 3'd6: BPS_PARA <= bps460800; 045 3'd7: BPS_PARA <= bps921600; 046 default: BPS_PARA <= bps9600;/*异常情况,恢复到9600的波特率*/ 047 endcase 048 end [table]
CMD
CMD模块为串口数据帧接收与解析模块,该模块负责对串口接收到的每一帧的数据进行解码判断,并从数据帧中提取出地址字节和数据字节。最后将地址字节和数据字节转换为类似于Avalon-MM形式的总线,以实现对其它模块的控制寄存器的读写,从而实现通过串口控制FPGA中各个模块工作的目的。 在工业应用中,串口指令大多以数据帧的格式出现,包含帧头、帧长、帧命令、帧内容、校验和以及帧尾,不会只是单纯的传输数据。在这个实验中,小梅哥也使用了数据帧的形式来通过上位机向FPGA发送命令,不过这里我使用的帧格式非常简单,帧格式以帧头、帧长、帧内容以及帧尾组成,忽略了校验部分内容,帧头、帧长以及帧尾内容都是固定的,不固定的只是帧内容,以下为小梅哥的设计中一帧数据的格式:
由于数据帧本身结构简单,因此数据帧的解析过程也相对简洁,以下为小梅哥的数据帧解析状态机设计,该状态机分为帧头解析、帧长解析、数据接收以及帧尾解析。默认时,状态机处于帧头解析状态,一旦出现帧头数据,则跳转到帧长接收状态,若下一个字节为帧长数据(这里严格意义上并不能算作帧长,因为长度固定,充其量只能算作帧头,读者不须过分纠结),则开始连续接收三个字节的数据,若非指定的帧长内容,则表明这是一次无关传输,状态机将返回到帧头解析状态继续等待新的数据帧到来。在帧尾解析状态,若解析到的数据并非指定的帧尾数据,则表明此次数据帧非有效帧,则将此帧已解析到的数据舍弃。若为帧尾数据,则解析成功,产生命令有效标志信号(CMD_Valid),Memory Mapped 总线进程在检测到此命令有效信号后,即产生写外设寄存器操作。
命令解析的状态机实现代码如下所示:
017 localparam 018 Header = 8'hAA, /*帧头*/ 019 Length = 8'd3, /*帧长*/ 020 Tail = 8'h88; /*帧尾*/ 021 022 /*----------状态定义-----------------*/ 023 localparam 024 CMD_HEADER = 6'b00_0001, 025 CMD_LENGTH = 6'b00_0010, 026 CMD_DATAA = 6'b00_0100, 027 CMD_DATAB = 6'b00_1000, 028 CMD_DATAC = 6'b01_0000, 029 CMD_TAIL = 6'b10_0000; 030 031 032 always@(posedge Clk or negedge Rst_n) 033 if(!Rst_n)begin 034 reg_CMD_DATA <= 24'd0; 035 CMD_Valid <= 1'b0; 036 state <= CMD_HEADER; 037 end 038 else if(Rx_Int)begin 039 case(state) 040 CMD_HEADER: /*解码帧头数据*/ 041 if(Rx_Byte == Header) 042 state <= CMD_LENGTH; 043 else 044 state <= CMD_HEADER; 045 046 CMD_LENGTH: /*解码帧长数据*/ 047 if(Rx_Byte == Length) 048 state <= CMD_DATAA; 049 else 050 state <= CMD_HEADER; 051 052 CMD_DATAA: /*解码数据A*/ 053 begin 054 reg_CMD_DATA[23:16] <= Rx_Byte; 055 state <= CMD_DATAB; 056 end 057 058 CMD_DATAB: /*解码数据B*/ 059 begin 060 reg_CMD_DATA[15:8] <= Rx_Byte; 061 state <= CMD_DATAC; 062 end 063 064 CMD_DATAC: /*解码数据C*/ 065 begin 066 reg_CMD_DATA[7:0] <= Rx_Byte; 067 state <= CMD_TAIL; 068 end 069 070 CMD_TAIL: /*解码帧尾数据*/ 071 if(Rx_Byte == Tail)begin 072 CMD_Valid <= 1'b1; /*解码成功,发送解码数据有效标志*/ 073 state <= CMD_HEADER; 074 end 075 else begin 076 CMD_Valid <= 1'b0; 077 state <= CMD_HEADER; 078 end 079 default:; 080 endcase 081 end 082 else begin 083 CMD_Valid <= 1'b0; 084 reg_CMD_DATA <= reg_CMD_DATA; 085 end |
第23行到第29行为状态机编码,这里采用独热码的编码方式。状态机的编码方式有很多种,包括二进制编码、独热码、格雷码等,二进制编码最接近我们的常规思维,但是在FPGA内部,其译码电路较为复杂,且容易出现竞争冒险,导致使用二进制编码的状态机最高运行速度相对较低。独热码的译码电路最简单,因此采用独热码方式编码的状态机运行速度较二进制编码方式高很多,但是编码会占用较多的数据位宽。格雷码以其独特的编码特性,能够非常完美的解决竞争冒险的问题,使状态机综合出来的电路能够运行在很高的时钟频率,但是格雷码编码较为复杂,尤其对于位宽超过4位的格雷码,编码实现较二进制编码和独热码编码要复杂的多。这里,详细的关于状态机的编码问题,小梅哥不做过多的讨论,更加细致的内容,请大家参看夏宇闻老师经典书籍《Verilog数字系统设计教程》中第12章相关内容。
Memory Mapped 总线进程根据命令有效标志信号产生写外设寄存器操作的相关代码如下所示:
087 /*------驱动总线写外设寄存器--------*/ 088 always@(posedge Clk or negedge Rst_n) 089 if(!Rst_n)begin 090 m_wr <= 1'b0; 091 m_addr <= 8'd0; 092 m_wrdata <= 16'd0; 093 end 094 else if(CMD_Valid)begin 095 m_wr <= 1'b1; 096 m_addr <= reg_CMD_DATA[23:16]; 097 m_wrdata <= reg_CMD_DATA[15:0]; 098 end 099 else begin 100 m_wr <= 1'b0; 101 m_addr <= m_addr; 102 m_wrdata <= m_wrdata; 103 end |
|