串口的一次发送或接收由四个部分组成:
波特率: 数据传输速率。
在一次串口通信过程中,数据接收与发送双方没有共享时钟,因此,双方必须协商好数据传输波特率。
根据双方协议好的传输速率,接收端即可对发送端的数据进行采样。
串口的接收与发送,其主要时序设计包括两个部分:
波特率产生时序设计: 假设FPGA输入时钟100Mhz,为得到常用的波特率,仍然采用计数分频来得到。BAUD_DIV=100_000000/波特率。其中采样中心点为发送或接收时钟的中心点,即BAUD_DIV_CAP=100_000000/(2*波特率)。该部分在数据接收和发送部分均单独完成。
数据接收模块:在设置好传输波特率的情况下,根据串口传输时序,进行解串。空闲状态时,接收数据为逻辑高电平,等待起始位逻辑低电平的到来。当起始位到达后,由低位到高位,依次采集8位数据,并进行相应的解串,存入临时寄存器。接收有效数据完成后,判断结束位,接收完毕。
数据发送模块:设置发送使能信号和待发送的数据。通过计数器,表示10个数据发送的周期。这10个数据,依次为起始位、8位数据位、1位结束位,实现数据位的逐个发送。
参考教程《14 从串口程序感受FPGA编程艺术》以及csdn博客使用Uart串口控制LED灯闪烁
代码分为三部分:
串口命令模块对于控制led的8字节命令制定了协议:
0x55 0xA5 Time[7:0] Time[15:8] Time[23:16] Time[31:24] Ctrl[7:0] 0xF0
其中0x55与0xA5可以视为起始位,0xF0可以视为终止位。
Time为32位二进制数,表示led灯间隔多少个时钟周期后切换当前状态。例如,使用的时钟周期为50MHz,此时想要0.5秒led灯切换一次状态,则可以设置Time=25_000_000,十六进制表示即0x017D_7840。
Ctrl为8位二进制数,通过0与1来控制led灯亮灭规律。例如,Ctrl=1010_1010,则在上面Time=25_000_000的情况下,led灯循环亮0.5秒后灭0.5秒;而如果Ctrl=1111_0000,则led灯循环亮2秒后灭2秒。
由上面的的例子,可以知道,串口发送55 05 01 7D 78 40 AA F0,可以控制led灯循环0.5秒亮灭。
用于生成不同波特率所需的时钟频率:
always@(*)begin //波特率选择
case (baud_set)
3'd0: baud_dr <= 50_000_000 / 4800 /16 ;
3'd1: baud_dr <= 50_000_000 / 9600 /16 ;
3'd2: baud_dr <= 50_000_000 / 14400 /16 ;
3'd3: baud_dr <= 50_000_000 / 19200 /16 ;
3'd4: baud_dr <= 50_000_000 / 38400 /16 ;
3'd5: baud_dr <= 50_000_000 / 56000 /16 ;
3'd6: baud_dr <= 50_000_000 / 57600 /16 ;
3'd7: baud_dr <= 50_000_000 / 115200 /16 ;
default: baud_dr <= 50_000_000 / 4800 /16 ;
endcase
end
其中使用的时钟频率为50MHz,当baud_set=1时,生成波特率为9600的时钟频率。而后面除的16用于后续的多次采样计数器功能。在16次周期中,会采样中间7次。
实际上,如果对串口通信没有抗干扰的要求,一般可以直接采取中间值采样,在波特率分频计数器计数到中间值的时候读取RX总线的数据。
assign bps_clk16 = (div16_cnt == baud_dr/2 - 1); // 取每一小段的中间值为采样时刻产生每一小段的采样时刻
本程序中对串口RX总线在一个波特率的分频周期中,在16次周期中,采样中间7次。一次信号总共有10位,总计为160个周期。
always@(posedge clk ,negedge rst)begin
if(!rst)
t160_cnt <= 0;
else if(rx_en)begin
if(t160_cnt == 160 && bps_clk16)
t160_cnt <= 0;
else if(bps_clk16)
t160_cnt <= t160_cnt + 1;
else
t160_cnt <= t160_cnt;
end
else
t160_cnt <= 0;
end
在采样时,采样第i位,则会在第16i+5到第16i+11个周期采样。
always@(posedge clk ,negedge rst)begin
if(!rst)begin
start_d <= 0;
Data[0] <= 0;
Data[1] <= 0;
Data[2] <= 0;
Data[3] <= 0;
Data[4] <= 0;
Data[5] <= 0;
Data[6] <= 0;
Data[7] <= 0;
stop_d <= 0;
end
else if(bps_clk16) begin
case(t160_cnt)
0:begin //将0时刻作为寄存器清零的时刻
start_d <= 0;
Data[0] <= 0;
Data[1] <= 0;
Data[2] <= 0;
Data[3] <= 0;
Data[4] <= 0;
Data[5] <= 0;
Data[6] <= 0;
Data[7] <= 0;
stop_d <= 0;
end
5,6,7,8,9,10,11: start_d <= start_d + uart_rx;
21,22,23,24,25,26,27: Data[0] <= Data[0] + uart_rx;
37,38,39,40,41,42,43: Data[1] <= Data[1] + uart_rx;
53,54,55,56,57,58,59: Data[2] <= Data[2] + uart_rx;
69,70,71,72,73,74,75: Data[3] <= Data[3] + uart_rx;
85,86,87,88,89,90,91: Data[4] <= Data[4] + uart_rx;
101,102,103,104,105,106,107: Data[5] <= Data[5] + uart_rx;
117,118,119,120,121,122,123: Data[6] <= Data[6] + uart_rx;
133,134,135,136,137,138,139: Data[7] <= Data[7] + uart_rx;
149,150,151,152,153,154,155: stop_d <= stop_d + uart_rx;
default:;
endcase
end
end
此后判断采样的值,7次里采样里,高电平大于等于4次取1,小于4次取0.
always@(posedge clk ,negedge rst)begin
if(!rst)
data <= 8'b0000_0000;
else if(t160_cnt == 160 && bps_clk16 )begin
data[0] <= (Data[0]>=4)? 1 : 0;
data[1] <= (Data[1]>=4)? 1 : 0;
data[2] <= (Data[2]>=4)? 1 : 0;
data[3] <= (Data[3]>=4)? 1 : 0;
data[4] <= (Data[4]>=4)? 1 : 0;
data[5] <= (Data[5]>=4)? 1 : 0;
data[6] <= (Data[6]>=4)? 1 : 0;
data[7] <= (Data[7]>=4)? 1 : 0;
end
else
data <= data;
end
通过移位操作,将前面读取的命令信息,即rx_data,逐步存储在寄存器data_str里面。
always@(posedge clk ,negedge rst)begin
if(!rst)begin
data_str[0] <= 0;
data_str[1] <= 0;
data_str[2] <= 0;
data_str[3] <= 0;
data_str[4] <= 0;
data_str[5] <= 0;
data_str[6] <= 0;
data_str[7] <= 0;
end
else if(rx_done)begin
data_str[0] <= rx_data;
data_str[1] <= data_str[0];
data_str[2] <= data_str[1];
data_str[3] <= data_str[2];
data_str[4] <= data_str[3];
data_str[5] <= data_str[4];
data_str[6] <= data_str[5];
data_str[7] <= data_str[6];
end
else begin
data_str[0] <= data_str[0];
data_str[1] <= data_str[1];
data_str[2] <= data_str[2];
data_str[3] <= data_str[3];
data_str[4] <= data_str[4];
data_str[5] <= data_str[5];
data_str[6] <= data_str[6];
data_str[7] <= data_str[7];
end
end
之后对data_str存储的命令进行分析,当前两字节起始位与末字节终止位符合设计的协议时,读取Time与Ctrl。
always@(posedge clk ,negedge rst)begin
if(!rst)begin
r_Time <= 32'd0;
r_ctrl <= 8'd0;
end
else if((data_str[0] == 8'hf0) && (data_str[6]==8'h05) && (data_str[7]==8'h55))begin
r_Time <= {data_str[5],data_str[4],data_str[3],data_str[2]};
r_ctrl <= data_str[1];
end
else begin
r_Time <= 32'd0;
r_ctrl <= 8'd0;
end
end
通过两个计数器counter1与counter2控制,counter1用于led亮灭周期计时,counter2用于判断当前led灯亮灭应当处于Ctrl中哪一位代表的状态。 counter1每个周期+1,到达Time-1时counter2+1,同时led灯亮灭变为Ctrl下一位的状态。
led_ctrl_time led_ctrl_time_u0(
.clk ( clk ),
.rst ( rst ),
.Ctrl ( ctrl ),
.Time ( Time ),
.led ( led )
);
本文章使用limfx的vscode插件快速发布