今天所要实现的功能是,编写串口程序从串口输入数据从而控制某管脚的PWM频率。
先给大家上RTL图理一下逻辑关系:
这个实例的难点是:
1. 从串口发送的数据,是通过ASCII码的形式发送到FPGA的,如发送“1”,其十六进制应该是0x01,但其ASCII码却是0x31。
2. 发送过程中难免会丢失数据,而我们在接收数据的时候却要保证其准确性。
3. 在写uart的时候,要理清时序关系。
下面上代码:
module uart_rx
#(
parameter CLK_FRE = 50, //clock frequency(Mhz)
parameter BAUD_RATE = 9600 //serial baud rate
)
(
input clk, //clock input
input rst_n, //asynchronous reset input, low active
output reg[7:0] rx_data, //received serial data
output reg rx_data_valid, //received serial data is valid
input rx_data_ready, //data receiver module ready
input rx_pin //serial data input
);
//calculates the clock cycle for baud rate
localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;
//state machine code
localparam S_IDLE = 1;
localparam S_START = 2; //start bit
localparam S_REC_BYTE = 3; //data bits
localparam S_STOP = 4; //stop bit
localparam S_DATA = 5;
reg[2:0] state;
reg[2:0] next_state;
reg rx_d0; //delay 1 clock for rx_pin
reg rx_d1; //delay 1 clock for rx_d0
wire rx_negedge; //negedge of rx_pin
reg[7:0] rx_bits; //temporary storage of received data
reg[15:0] cycle_cnt; //baud counter
reg[2:0] bit_cnt; //bit counter
assign rx_negedge = rx_d1 && ~rx_d0;
/****************触发器判断下降沿***********/
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
begin
rx_d0 <= 1'b0;
rx_d1 <= 1'b0;
end
else
begin
rx_d0 <= rx_pin;
rx_d1 <= rx_d0;
end
end
/******************************************/
/***************定义状态机************/
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
state <= S_IDLE;
else
state <= next_state;
end
/**********状态机*****************/
/*空闲状态->下降沿时->起始位,数据开始计数,计完一位进入下一位,直到计到八位为止,进入停止位,停止位计一半,进入*/
always@(*)
begin
case(state)
S_IDLE:
if(rx_negedge)
next_state <= S_START;
else
next_state <= S_IDLE;
S_START:
if(cycle_cnt == CYCLE - 1)//one data cycle
next_state <= S_REC_BYTE;
else
next_state <= S_START;
S_REC_BYTE:
if(cycle_cnt == CYCLE - 1 && bit_cnt == 3'd7) //receive 8bit data
next_state <= S_STOP;
else
next_state <= S_REC_BYTE;
S_STOP:
if(cycle_cnt == CYCLE/2 - 1)//half bit cycle,to avoid missing the next byte receiver
next_state <= S_DATA;
else
next_state <= S_STOP;
S_DATA:
if(rx_data_ready) //data receive complete
next_state <= S_IDLE;
else
next_state <= S_DATA;
default:
next_state <= S_IDLE;
endcase
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
rx_data_valid <= 1'b0;
else if(state == S_STOP && next_state != state)
rx_data_valid <= 1'b1;
else if(state == S_DATA && rx_data_ready)
rx_data_valid <= 1'b0;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
rx_data <= 8'd0;
else if(state == S_STOP && next_state != state)
rx_data <= rx_bits;//latch received data
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
begin
bit_cnt <= 3'd0;
end
else if(state == S_REC_BYTE)
if(cycle_cnt == CYCLE - 1)
bit_cnt <= bit_cnt + 3'd1;
else
bit_cnt <= bit_cnt;
else
bit_cnt <= 3'd0;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
cycle_cnt <= 16'd0;
else if((state == S_REC_BYTE && cycle_cnt == CYCLE - 1) || next_state != state)
cycle_cnt <= 16'd0;
else
cycle_cnt <= cycle_cnt + 16'd1;
end
//receive serial data bit data
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
rx_bits <= 8'd0;
else if(state == S_REC_BYTE && cycle_cnt == CYCLE/2 - 1)
rx_bits[bit_cnt] <= rx_pin;
else
rx_bits <= rx_bits;
end
endmodule
这段代码是copy黑金开发板的串口实例的,理解了就不想自己写了,直接copy省时间。
串口无非也就是几个关键点:判断下降沿、设置波特率、读取数据位和停止位。
为什么要判断下降沿?
因为当串口处于空闲状态时,一直保持着高电平的状态,当数据到来时,电平先被拉低,其次才是传送数据位。判断上升下降沿通过两个触发器,即判断前后两个状态即可。若前一个状态为高电平,后一个状态为低电平,则为下降沿。如代码段注释。
其次,则要定义状态机。状态机的过程主要是:首先,判断串口端是否有下降沿,如果有,则进入S_START状态,开始时计数器开始计数,计数器的大小和波特率的设置有关。波特率的含义是,每秒能传输的位数。因此如果波特率是9600,则一秒能传输9600个bits,而50M时钟一秒需要计数50000000次,因此cycle应该取50000000/9600。为了方便修改,用参数定义不失为一种好方法。计数器开始一位一位地计数,每一次发送都是八个字节,因此需要计数八次,此时进入下一个状态。到S_STOP状态时,因为马上要进入下一个发送状态,因此只要计数到计数器一半即可(这是普遍的方法),此时返回空闲状态,继续等待接收响应。
计数器我就省略不讲了,无非就是计数到一定的值然后归0。对于一位字节的计数,计数器计数到 时钟频率/波特率-1;对于一串数据即八个字节的,则计数八次,每一次的周期是单个字节的计数值。
此外,有两个信号要着重强调,这两个信号极其方便地为后面其它操作提供了时序逻辑关系。它们就是rx_data_valid和rx_data_ready。至于rx_data_ready,因为要保持串口的接收持续有效,因此只要用assign使其一直保持为1即可。而对于rx_data_valid则是每一次发送完数据之后的判断信号。
接下来是发送:
module uart_tx
#(
parameter CLK_FRE = 50, //clock frequency(Mhz)
parameter BAUD_RATE = 9600 //serial baud rate
)
(
input clk, //clock input
input rst_n, //asynchronous reset input, low active
input[7:0] tx_data, //data to send
input tx_data_valid, //data to be sent is valid
output reg tx_data_ready, //send ready
output tx_pin //serial data output
);
//calculates the clock cycle for baud rate
localparam CYCLE = CLK_FRE * 1000000 / BAUD_RATE;
//state machine code
localparam S_IDLE = 1;
localparam S_START = 2;//start bit
localparam S_SEND_BYTE = 3;//data bits
localparam S_STOP = 4;//stop bit
reg[2:0] state;
reg[2:0] next_state;
reg[15:0] cycle_cnt; //baud counter
reg[2:0] bit_cnt;//bit counter
reg[7:0] tx_data_latch; //latch data to send
reg tx_reg; //serial data output
assign tx_pin = tx_reg;
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
state <= S_IDLE;
else
state <= next_state;
end
always@(*)
begin
case(state)
S_IDLE:
if(tx_data_valid == 1'b1)
next_state <= S_START;
else
next_state <= S_IDLE;
S_START:
if(cycle_cnt == CYCLE - 1)
next_state <= S_SEND_BYTE;
else
next_state <= S_START;
S_SEND_BYTE:
if(cycle_cnt == CYCLE - 1 && bit_cnt == 3'd7)
next_state <= S_STOP;
else
next_state <= S_SEND_BYTE;
S_STOP:
if(cycle_cnt == CYCLE - 1)
next_state <= S_IDLE;
else
next_state <= S_STOP;
default:
next_state <= S_IDLE;
endcase
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
begin
tx_data_ready <= 1'b0;
end
else if(state == S_IDLE)
if(tx_data_valid == 1'b1)
tx_data_ready <= 1'b0;
else
tx_data_ready <= 1'b1;
else if(state == S_STOP && cycle_cnt == CYCLE - 1)
tx_data_ready <= 1'b1;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
begin
tx_data_latch <= 8'd0;
end
else if(state == S_IDLE && tx_data_valid == 1'b1)
tx_data_latch <= tx_data;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
begin
bit_cnt <= 3'd0;
end
else if(state == S_SEND_BYTE)
if(cycle_cnt == CYCLE - 1)
bit_cnt <= bit_cnt + 3'd1;
else
bit_cnt <= bit_cnt;
else
bit_cnt <= 3'd0;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
cycle_cnt <= 16'd0;
else if((state == S_SEND_BYTE && cycle_cnt == CYCLE - 1) || next_state != state)
cycle_cnt <= 16'd0;
else
cycle_cnt <= cycle_cnt + 16'd1;
end
always@(posedge clk or negedge rst_n)
begin
if(rst_n == 1'b0)
tx_reg <= 1'b1;
else
case(state)
S_IDLE,S_STOP:
tx_reg <= 1'b1;
S_START:
tx_reg <= 1'b0;
S_SEND_BYTE:
tx_reg <= tx_data_latch[bit_cnt];
default:
tx_reg <= 1'b1;
endcase
end
endmodule
如果你理解了接收的代码,发送的代码也是异曲同工。
波特率的设置和前面一样。但和接收不同,发送是没有规定的时序要求的(即没有下降沿),因此,这里有一个tx_data_valid,你可以自己定义什么时候发送数据。当判断你想要发送数据时,则进入状态机,也是一样的,先是进入开始状态,其次是一位一位地发共八次。发完八次以后则进入停止位,停止位也是计数到计数周期的一半即可,接着等待下一个状态。为了区分发送状态和其它状态,后面有一个状态机进行了补充,即空闲状态和停止位时,发送端都定义为高电平,而数据位时则发送数据。剩下的代码无非就是计数器和在什么状态下使能tx_data_ready。因为这个信号我需要用到,因此我就不详细讲了。
串口只是接收发的工具,接着进入本实例的核心代码。
module fre
(input clk,
input rst_n,
input rx_data_valid,
input [7:0]rx_data,
output sig_out,
output [7:0]tx_data,
output tx_data_valid
);
/**************只要接收到数据就存到移位寄存器里面**********************/
reg[7:0] rx_reg1;
reg[7:0] rx_reg2;
reg[7:0] rx_reg3;
reg[7:0] rx_reg4;
reg[7:0] rx_reg5;
reg[7:0] rx_reg6;
always@(posedge rx_data_valid or negedge rst_n)
if(!rst_n)
begin
rx_reg1<=8'b0;
rx_reg2<=8'b0;
rx_reg3<=8'b0;
rx_reg4<=8'b0;
rx_reg5<=8'b0;
rx_reg6<=8'b0;
end
else
begin
rx_reg1<=rx_data;
rx_reg2<=rx_reg1;
rx_reg3<=rx_reg2;
rx_reg4<=rx_reg3;
rx_reg5<=rx_reg4;
rx_reg6<=rx_reg5;
end
wire [13:0]ge_wei;
wire [13:0]bai_wei;
wire [13:0]shi_wei;
wire [13:0]qian_wei;
assign ge_wei = {6'd0,rx_reg3[7:0]};
assign shi_wei = {6'd0,rx_reg4[7:0]};
assign bai_wei = {6'd0,rx_reg5[7:0]};
assign qian_wei = {6'd0,rx_reg6[7:0]};
reg tx_data_valid0;
assign tx_data_valid=tx_data_valid0;
reg[7:0]str;
assign tx_data= str;
/*****************获取数据**********************/
reg [13:0]input_num;
always@(posedge clk)
if(rx_reg1 == 8'b00001010 && rx_reg2 ==8'b00001101)
begin
tx_data_valid0<=1'b1;
input_num <=(qian_wei-8'b00110000)*1000+(bai_wei-8'b00110000)*100+(shi_wei-8'b00110000)*10+(ge_wei-8'b00110000);
end
else
begin
input_num<=0;
tx_data_valid0<=1'b0;
end
/******************计数器计数得到一定频率的信号翻转**********************/
reg[25:0] count;
reg signal;
always@(posedge clk or negedge rst_n)
if(!rst_n)
count<=25'd0;
else if(count <= input_num*10000)
count <= count+25'd1;
else
count<=25'd0;
/************************************/
always@(posedge clk or negedge rst_n)
if(!rst_n)
signal<=0;
else if(count==input_num*10000)
signal <= ~signal;
assign sig_out= signal;
/**************验证一下数据***********************/
reg[3:0] state;
always @(posedge clk or negedge rst_n)
if(!rst_n)
state<=0;
else
case(state)
0:
if(tx_data_valid0)
begin
state<=state+4'b1;
end
else
state<=state;
1:
begin
state<=state+4'b1;
str<="i";
end
2:
begin
state<=state+4'b1;
str<="n";
end
3:
begin
state<=state+4'b1;
str<="p";
end
4:
begin
state<=state+4'b1;
str<="u";
end
5:
begin
state<=state+4'b1;
str<="t";
end
6:
begin
state<=state+4'b1;
str<=":";
end
7:
begin
state<=state+4'b1;
str<=rx_reg6;
end
8:
begin
state<=state+4'b1;
str<=rx_reg5;
end
9:
begin
state<=state+4'b1;
str<=rx_reg4;
end
10:
begin
state<=state+4'b1;
str<=rx_reg3;
end
11:
begin
state<=state+4'b1;
str<="\r";
end
12:
begin
state<=state+4'b1;
str<="\n";
end
13:
begin
state<=4'b0;
end
endcase
endmodule
很开心的是,这段代码完完全全是我自己写的且没有参考任何人。一开始真的是脑壳疼,感觉这种东西是会而不难,刚开始什么都不会,copy别人的代码还不会连线,写程序也不规范,直到我看到了黑金系列最早期的那些教程,强调我们需要用建模的思想,以及下载了正点原子的视频之后,我才慢慢找到了感觉,写这段代码一气呵成。呜呜,感动。
首先,我输入1000,你需要把1000存储到FPGA里,而串口每次只发送八位,因此,你需要用几个移位寄存器,进行保存,不然来一个数据丢失一个数据。这个时候的敏感电平就不是clk和rstn了,刚开始学的时候真的是无论什么always@都用了clk和rstn然后再用if,这样亲测过后是失败的。这里其实是数字电子技术里面的移位寄存器的思想,这个时候就用到刚刚提及的rx_data_valid了,当传完一次数据,则寄存器移一次。
为什么四个数据这里要定义六个寄存器呢?
原因是,当我们发送数据的时候,计算机是不清楚什么数据你想要而什么数据不要,这里采用了一个尾帧的思想,就是相当于一个标识符,当你检测到标识符的时候,前面的数据都有效。而这里的标识符则是\r\n,即行首与换行符。其对应的ASCII码是0x0d和0x0a。因此,当发送四位数据的时候,同时发送\r\n。
当判断到此次发送的数据是有尾帧时,首先,为了检验数据,我把数据发送出去。具体看代码行的最后一个状态机,返回的是input+数据。这个状态机用的是always@(posedge CLK),因此亲测如果波特率是115200的话数据会丢失,这里我设置的是9600,数据能够正常发送。
其次就是怎么获得数据了。首先,我把位数进行拓展,为什么要进行拓展。大家想想,8位最大只能加到2的8次方,因此乘以1000的时候数据明显会溢出,保证四位数二进制应该是2的14次方等于16384。因此我通过拼接的形式将得到的位数进行拓展。其次,正如我前面所提到,发送数字的时候是以ASCII码的形式发送的,要得到具体的数值,必须减去0x30,因此每一个数据减去0x30得到个位百位十位和千位。再分别将他们乘起来。即input_num <=(qian_wei-8'b00110000)*1000+(bai_wei-8'b00110000)*100+(shi_wei-8'b00110000)*10+(ge_wei-8'b00110000);
此时即可得到计数器的数值。则信号的翻转只要跟随着计数器的数值,自己定义即可。
最后则是将这些模块连接起来。
module fre_top
(
input clk,
input rst_n,
input rx_pin,
output tx_pin,
output sig_out
);
wire rx_data_ready;
assign rx_data_ready=1'b1;
wire [7:0] rx_data;
wire [7:0] tx_data;
wire rx_data_valid;
wire tx_data_valid;
uart_rx U1
(
.clk(clk),
.rst_n(rst_n),
.rx_pin(rx_pin),
.rx_data(rx_data),
.rx_data_valid(rx_data_valid),
.rx_data_ready(rx_data_ready)
);
uart_tx U2
(
.clk(clk),
.rst_n(rst_n),
.tx_pin(tx_pin),
.tx_data(tx_data),
.tx_data_valid(tx_data_valid)
);
fre U3
(
.clk(clk),
.rst_n(rst_n),
.rx_data_valid(rx_data_valid),
.tx_data_valid(tx_data_valid),
.tx_data(tx_data),
.rx_data(rx_data),
.sig_out(sig_out)
);
endmodule
这里还需注意时钟保持rx_data_ready为高电平,即始终为等待接收的状态。
到此,这个实例讲解完毕。
欢迎大家加这个群一起讨论FPGA。