实战一:FPGA串口控制PWM频率

今天所要实现的功能是,编写串口程序从串口输入数据从而控制某管脚的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。

 

 

 

 

 

 

 

 

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值