串口控制led闪烁项目设计思路总结

baud_rate_gen模块设计思路

查到的资料:一般对于串口的时钟,都是使用baud * 16。这样处理串口的接收和发送会变得容易。所以需要向实现一个波特率生成模块,将外部一个固定的时钟分频得到baud16x的时钟

这里默认使用9600的波特率,那个baud16x就为 9600*16 = 153600
50mhz/153000hz 约等于 325
直接按325倍分频即可。用一个计数器count,每个时钟加一。到162时将输出翻转,到325时将cnt重置并且将输出再次翻转即可。即可实现占空比接近0.5的的325倍分频。满足波特率9600的需求

module baud_rate_gen(
input clk50mhz,
input reset,
output baud16x 
    );
wire clk50mhz;
wire reset;
reg baud16x;

reg[15:0] count;

always@(posedge clk50mhz or negedge reset)
if (!reset) begin 
    baud16x <= 0;
    count <= 0;
end 
else if(count == 16'd162) begin
baud16x <= 1'b1;
count <= count + 16'd1;
end 
else  if(count == 325) begin 
baud16x <=  1'b0;
count <= 16'd0;
end 
else 
count <= count + 16'd1;
endmodule

Reveiver模块的设计思路

Receiver模块是串口的接收模块,将一个串行的输入转化为一个8位数据。
注意:串口在接收数据时,有信号格式的规定:
先收到一个低电平0,表示开始接收数据,即开始位
然后是8位的数据
然后是由两个或三个高电平1表示的检验位和终止位
所以我们需要首先检测输入信号的一个下降沿,然后后面8位就是真正的数据。

模块接口:
recv是串行数据输入。
rdata是并行数据输出。
recv_ready表示Reciver是否处于可接受新数据的状态。如果现在正在接收数据就是0,表示不可接收

input baud16x,rstn,
input recv,
output reg [7:0] rdata,
output reg recv_ready,//标识在当前时刻Receiver是否可以接受新来的数据
output reg rdata_valid//用来标识当前radata是否为最新接收到的有效数据

首先需要检测recv信号的下降沿,

//起始位获取
    reg recvIn0;//下降沿捕捉,recv当前时刻值
    reg recvIn1;//下降沿捕捉,recv上一个时刻值
    wire neg_rec;//检测第一个下降沿,即开始位,表示数据要开始传输了
    
    always@(posedge baud16x or negedge rstn)
    if(!rstn) begin
    recvIn0 <= 1'b1;
    recvIn1 <= 1'b1;
    end
    else begin
    /*下降沿采样*/
    recvIn0 <= recv;//当前时刻的recv给recvIn0
    recvIn1 <= recvIn0;//前一个时刻的recv给recvIn1
    end
    assign neg_rec = !recvIn0 && recvIn1;//下降沿就是要当前时钟周期为0 上一个时钟周期为1

因为时钟输入为baud16x,所以每16个时钟周期对应一位数据。根据串口传输数据的特点,可以知道。把检测到recv的下降沿的时刻记为0周期,那么:

  • [0,16): 起始位

  • [16,32):第一位数据

  • ...

  • [128,144):第八位数据位

  • [144,160):可能存在的校验位

  • [160,176):可能存在的终止位1

  • [176,192):可能存在的终止位2

所以我们关注的重点在[16,144]之间的一个字节的数据。
以下代码的实现思路:当检测到recv信号的下降沿时,表明这是开始位了,那么就进入接收状态,将RecvNeFlag置为1,并且将cnt_bit逐个加一,一次来确定到达了哪个数据位。

    reg[7:0] cnt_bit;//因为输入时钟是baud16x,所以需要每计算16个读取一个数值
    reg RecvNeFlag;//处于接收状态的标志  

    always@(negedge rstn or posedge baud16x)
    if(!rstn) begin
     RecvNeFlag <= 1'b0;
     recv_ready <= 1'b1;
    end
    else if(neg_rec == 1'b1)begin//检测到一个下降沿,说明检测到起始位,就进入接收状态了
     RecvNeFlag <= 1'b1;//标志Recv进入接收状态,开始接收新的字节
     recv_ready <= 1'b0;//当进入接收状态时,Receiver只能等到接收完才可以再次接收
     rdata_valid <= 1'b0;//开始准备接收数据的时候,把当前的rdata标记为无效,接收完再标记为有效
    end
    else if(cnt_bit >= 8'd152)begin//接收完的时候 
     RecvNeFlag <= 1'b0;
     recv_ready <= 1'b1;
    end

当将RecvNeFlag置为1之后,cnt_bit就要逐个时钟周期加1,等到RecvNeFlag为0时,表示数据接收完了,cnt_bit也要重新置0、

    always@(posedge baud16x or negedge rstn)
    if(!rstn)
    cnt_bit <= 1'b0;
    else if(RecvNeFlag == 1'b1)
    cnt_bit <= cnt_bit + 1'b1;
    else cnt_bit <= 8'd0;

以为[16,32)是第一位数据,所以我们在读取的时候,选择在这个区间的中间时刻读取。即24.其余数据位一样在区间的中间时刻读取。那么依次就是 24、40、56、72、88、104、120、136。

    always@(posedge baud16x or negedge rstn)
    if(!rstn)
    begin 
    rdata <= 8'b0000_0000;
    end
    else case(cnt_bit)
    8'd24:rdata[0]  <= recv;//数据位第1位
    8'd40:rdata[1]  <= recv;
    8'd56:rdata[2]  <= recv;
    8'd72:rdata[3]  <= recv;
    8'd88:rdata[4]  <= recv;
    8'd104:rdata[5] <= recv;
    8'd120:rdata[6] <= recv;
    8'd136:begin rdata[7] <= recv;rdata_valid <= 1'b1;end //数据位第8位,数据接收完毕,那么此时的数据才是最新的、有效的
    endcase

Transmitter模块设计思路

Transmitter模块是串口的发送模块,将一个8位的数据转化为串行信号输出。格式和Receiver接收的格式一样,先是一个低电平0表示的起始位,然后是8位数据位,然后是高电平表示的检验位和终止位
整体的实现思路和Receiver比较像,只是反过来

module Transmitter(
input [7:0] DataIn,//并行数据输入
input baud16x,TxEn,rstn,//baud16x=波特率×16,TxEn是并行数据装入使能信号,rstn复位信号
output reg DataOut,//串行数据输出
output reg TxBusy// 说明串口正忙的信号,检测其下降沿就可以判断是否可以装入新的数据
    );

首先检测TxEn写使能信号的上升沿,检测到之后Transmitter进入发送状态,TxBusy信号置为1。然后cnt开始计数,从数据的低位到高位,没16个周期发送一位数据。

检测TxEn信号的上升沿:

    //检测写使能信号的上升沿
    reg TransEnIn0;//当前TxEn状态采样
    reg TransEnIn1;//上一个TxEn状态采样
    wire pos_en;//采TxEn的上升沿,由TransEnIn0和TransEnIn1一起确定TxEn信号的上升沿
    always@(posedge baud16x or negedge rstn)
    if(!rstn)begin
     TransEnIn0<=1'b0;
     TransEnIn1<=1'b0;
    end
    else begin
     TransEnIn0 <= TxEn;//now
     TransEnIn1 <= TransEnIn0;// delay
    end
    assign pos_en = TransEnIn0 & !TransEnIn1;//上一个TxEn为0.当前ExTn为1.表示检测到一个上升沿信号
    

检测到TxEn的上升沿之后,

    reg [7:0] DataInReg;//输入数据寄存器
    reg [7:0] cnt;//计数器,用来确定输出信号的时间
    reg cnt_start;//计数开始标志位,也就是发送开始标志位
    //数据装入
    always@(negedge rstn or posedge baud16x)
     if(!rstn)begin
      DataInReg <= 8'd0;
      TxBusy <= 1'b0;
      cnt_start <= 1'b0;//
     end
     else if(pos_en == 1'b1)begin//检测到TxEn的上升沿之后,进入发送状态
      DataInReg <= DataIn;
      cnt_start <= 1'b1;
      TxBusy <= 1'b1;
     end
     else if(cnt >= 8'd160) begin//一个数据帧传输完毕
      cnt_start <= 1'b0;
      TxBusy <= 1'b0;
     end

当把cnt_start置为1之后,表示此时Transmitter处于发送状态,cnt开始计数,每16个周期发送一位数据即可。

     always@(posedge baud16x or negedge rstn)
     if(!rstn)
      cnt <= 8'd0;
     else if(cnt_start == 1'b1)
      cnt <= cnt + 1'b1;
     else cnt <= 8'd0;
      
     
     //UART 发送
     always@(posedge baud16x or negedge rstn)
     if(!rstn)
      DataOut <=1'b1;
     else if(cnt_start == 1'b1)
     case(cnt)
      8'd0:DataOut <= 1'b0;
      8'd16:DataOut <= DataInReg[0];
      8'd32:DataOut <= DataInReg[1];
      8'd48:DataOut <= DataInReg[2];
      8'd64:DataOut <= DataInReg[3];
      8'd80:DataOut <= DataInReg[4];
      8'd96:DataOut <= DataInReg[5];
      8'd112:DataOut <= DataInReg[6];
      8'd128:DataOut <= DataInReg[7];
      8'd144:DataOut <= 1'b1;
     endcase
     else DataOut <= 1'b1;

RecvToFIFO模块设计思路

因为Receiver模块后面要连接一个FIFO将接收到的数据存到FIFO中,并且在一条命令接收完毕之后给TwoLed模块发送一个命令接收完毕的信号,通知Twoled模块开始工作。
考虑到这一部分功能不属于一般串口Receiver模块功能的范畴,所以觉得另外写一个模块来是西安这部分功能跟好,所以就写了一个RecvToFIFO模块。 RecvToFIFo模块的主要功能是:
1 根据Recv的rdata_valid信号的值的变化,给出FIFO的写使能信号FIFOWrEn.
2 当一条命令全部写入FIFO中时,产生一个OncCMDEnd信号,这个信号将用于通知Twoled模块开始工作 另外由于此模块是为了配合Receiver进行工作的,所以时钟也设置为baud16x

Receiver模块中,有一个rdata_valid的输出信号,该信号用来标识当前Receiver输出端的数据rdata是否为最新接收的数据。rdata_valid会在一个新数据接收完的时候置为1,直到开始接收下一个字节才会置0。所以rdata由0->1即表示来了一个新数据,这个时候就要给出FIFOWrEn写使能信号。 检测rdata_valid的上升沿

reg rdata_valid_0;//数据是否有效,当前的值
reg rdata_valid_1;//数据是否有效,上一个周期的值
wire FIFOWrEn;
//捕捉rdata_valid的上升沿,表示一个字节刚好接收完毕,是一个新的数据,这时将FIFO的写使能打开
always@(posedge baud16x or negedge rstn) begin 
    if(!rstn) begin
       rdata_valid_0 <= 1'b0;
       rdata_valid_1 <= 1'b0;
    end 
    else begin 
        rdata_valid_0 <= rdata_valid;
        rdata_valid_1 <= rdata_valid_0;
    end  
end 
assign FIFOWrEn = rdata_valid_0 && (!rdata_valid_1);

然后每次给出一个FIFOWrEn就说明向FIFO写了一个字节,每次写一个字节就要记录写了的字节数,当写字节数到达4时,就说明一条命令已经读取完毕,这时就要产生一个OneCMDEnd信号,用于通知Twoled模块工作。

always@(posedge baud16x or negedge rstn) begin 
    if(!rstn)begin 
        OneCMDEnd <= 0;
        RecCMDByteNum <= 0;
    end 
    else if(FIFOWrEn == 1)begin
         RecCMDByteNum <= RecCMDByteNum + 1;
    end
    else if(RecCMDByteNum == 4) begin //接收玩一条指令
        OneCMDEnd <= 1;
        RecCMDByteNum <= 0;
    end
    else OneCMDEnd <= 0;
end 

仿真结果

由仿真图像我们可以知道,在检测到rdata_valid上升沿的下一个周期,RecvToFIFO会给出一个FIFOWrEn,再下一个周期记录已经写了几个字节的ByteNum会加一。在ByteNum变成4的下一个周期就会产生一个OneCMDEnd信号

TwoLed 模块设计思路

Twoled模块是主要完成控制两个led灯的模块。
主要工作是,从FIFO模块中读取控制两个灯的命令,一个灯两个字节。并且控制led灯的闪烁。FFFF FFFF表示熄灭。其余数字表示闪烁间隔,单位ms
然后回复两个灯泡状态,FF表示熄灭。01表示闪烁。

因为这里要控制led的闪烁频率,所以输入的时钟不能采用baud16x,直能用50mhz,和其余的几个模块时钟频率都不同意,所以处理起来会麻烦一些

状态机设计:
有5个状态:A:初始状态,B:读取完命令第一个字节,C:读取完命令第二个字节,D:读取完命令第三个字节,E:读取完命令第四个字节。
状态转换:

首先我们需要检测启动状态机的信号的上升沿,检测到之后状态机就开始运行,读取FIFO中的命令

OneCMDEnd信号由RecvToFIFO模块给出,表示一条命令接收完毕。这时可以从FIFO中读取命令
CMDEnd0表示OneCMDEnd信号的当前时钟周期的值
CMDEnd1表示OneCMDEnd信号的上一个时钟周期的值
CMDEndUpEdge = CMDEnd0 && (!CMDEnd1)即上一个周期为0,这一个周期为1,表示检测到一个上升沿
StartStateMa(StartStateMachine)检测到上升沿之后,状态机开始处于运行状态。

检测CMDEnd下降沿:

//检测OneCMDEnd的上升沿
always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn)begin 
       CMDEnd0 <= 0; 
       CMDEnd1 <= 0;
    end     
    else begin
        CMDEnd0 <= OneCMDEnd;
        CMDEnd1 <= CMDEnd0;
    end 
end 
assign CMEEndUpEdge=CMDEnd0 && (!CMDEnd1);

FIFO的结构如下所示:

要读取一个字节,需要给出读使能信号rd_en,就能从dout接收到一个字节。如果FIFO的empty为1,表示FIFO里面没有数据,则不能读取。所以这三个信号也要在我们的TwoLed模块中用到。
CMDByte接收来自FIFO读取到的命令字节
FIFOempty来自FIFO的empty,表示FIFO中是否有数据可以读取
FIFORnEn为FIFO读使能信号。

以下代码思路:当检测到一个OneCMDEnd的上升沿之后,状态机就进入运行,给出FIDO的读使能信号,并记录下从FIFO中读取的字节数,当读取了四个字节之后,一条完整的命令也就读取完毕。

//状态机的启动
reg FIFORdEn;//FIFO读使能信号
reg [2:0] FIFORdCnt;//从FIFO读取的命令的字节数

always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin 
        StartStateMa <= 0;
        FIFORdEn <= 0;
    end
    else begin 
        if(CMEEndUpEdge == 1'b1) begin StartStateMa<=1; FIFORdEn <= 1; FIFORdCnt <= 0; end 
        else if(FIFORdCnt>=4) begin  StartStateMa<= 0;  FIFORdEn <= 0; end //FIFORdCnt >= 4说明读取了一条完整的指令
    end 
end 

因为TwoLed模块使用的50mhz时钟,而FIFO和其他模块都使用的baud16x的时钟,默认实现的是9600的baud,所以时钟需要经过50M/(9600*16) = 325倍的分频。

reg [15:0] cnt;
//处理cnt 如果状态机开始运行i.e. StartStateMa为1,那么就要用cnt来计数,确定何时给出RdEn信号
always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin 
        cnt <= 0;
    end 
    else if(StartStateMa == 1'b1)begin 
        cnt <= cnt + 1;//状态机处于运行状态时,cnt累加
    end
    else cnt <= 0;//状态机一轮执行完毕或者处于初始状态,也就是状态机未运行的时候,保持cnt为0
end 

有了以上的信号之后,就可以完成状态机的状态转换

//状态机的状态转换
always@(posedge clk50mhz or negedge rstn) begin 
    if (!rstn) begin 
        state <= 0;//初始状态
        FIFORdCnt <= 0;
        LED0CMD <= 0;
        LED1CMD <= 0;
    end 
   else if((StartStateMa == 1'b1)&& (!FIFOempty)) begin 
            case(cnt)  
            16'd325:begin LED0CMD[15:8] <= CMDByte; FIFORdCnt <= 1; state<= 1; end //接收完第一个字节
            16'd650:begin LED0CMD[7:0] <= CMDByte; FIFORdCnt <= 2; state<= 2;end //接收完第2个字节
            16'd975:begin LED1CMD[15:8] <= CMDByte; FIFORdCnt <= 3; state<= 3;end //接收完第3个字节
            16'd1300:begin LED1CMD[7:0] <= CMDByte; FIFORdCnt <= 4; state<= 4; end//接收完第4个字节
        endcase
        end   
end 

当state为4时,表示接收完四个字节的命令,这时候就开始对控制led的相关信号进行处理。
首先就是控制led闪烁的信号。要向控制led根据命令随时调整闪烁的频率,就是要能够根据指令的内容,调整一个分频器的分频倍数。
这里的思路是这样的:我们一般都是设定一个固定的分频倍数 2m,用一个count计数器逐次加一,到达中间值时m时翻转一次,再次到达最大值 2m时将时钟再次翻转并且count置0;
为了实现变化的分频倍数,我们这里用一个寄存器存下这个分频的倍数,然后根据收到的命令修改寄存器中的数值即可。仍然是count到一半时翻转,到最大值时翻转并count置0;

首先计算一下,假设闪烁周期为x ms,那么频率为 1000/x ,对应分频倍数为 50000 * x倍,最大时间间隔为5s,最大分频倍数就位 50000 * 5000 需要32位的寄存器才能装下。
以下这部分代码的实现思路:接收完一条指令的四个字节后,state 为4,然后利用一个时钟周期,设置好两个led等分屏的倍数和回复给电脑端关于两个led状态的字节 led0state和led1state。然后重置控制led闪烁的count,之后就回到初始状态,在初始状态下,让led等运行闪烁,并且回复给电脑端两个灯的状态。
这里有一个地方值得注意,因为有led可能不闪烁。这里用LEDDivider来表示不闪烁,因为如果led闪烁的话,LEDDivider的数值会在 50000*1 ~ 50000*5000之间,所以用0来表示不闪烁很可以的,不会占用到正常情况下的数值。

reg[31:0] LED0Divider;//记录时钟的分频倍数。最大的分频倍数是 5s 0.2hz 50mhz/0.2hz = 25 000 0000 需要32位才装得下
reg[31:0] LED1Divider;
reg led0shine;//时钟分频后的信号,用于控制led闪烁
reg led1shine;
reg [7:0] led0state;//用于回复给电脑端的数据,表明两个led的状态
reg [7:0] led1state; 
reg [31:0] led0cnt;//用来分频的计数器
reg [31:0] led1cnt;  

//接收完命令之后的处理。通过命令来改变分频的频率,得到输出到LED的信号
always@(posedge clk50mhz or negedge rstn) begin 
    if (!rstn) begin 
        LED0Divider<=0;
        LED1Divider<=0;
        led0state <= 8'hff;
        led1state <= 8'hff;
    end 
    else if(state ==4) begin //表明命令都已经读取了
            led0cnt <= 0;
            led1cnt <= 0;
            led0shine <= 0;
            led1shine <= 0;
            if(LED0CMD == 16'hFFFF)begin
                LED0Divider <= 0;//表示不闪烁
                led0state <= 8'hff;
            end 
            else begin
                LED0Divider <= LED0CMD * 50000;//不是FFFF,计算得到分频背书     
                led0state <= 8'h01;      
            end 
            if(LED1CMD == 16'hFFFF) begin 
                LED1Divider <= 0;
                led1state <= 8'hff;
              end 
            else begin 
                LED1Divider <= LED1CMD * 50000;
                led1state <= 8'h01; 
              end
            state <= 0;
         end
end

然后就根据之前的分频器的思路,把50mhz的时钟分频成控制led闪烁的信号

    
//分频产生控制led闪烁的信号
always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin 
        led0cnt <= 0;
    end
    else begin 
        if(LED0Divider == 0) led0cnt <=0 ;//
        else led0cnt <= led0cnt + 1 ;
    end
end 

always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin
        led0shine <= 0;
    end  
    else if(led0cnt == LED0Divider/2) begin 
        led0shine <= 1;
    end
    else if(led0cnt >= LED0Divider) begin 
        led0shine<= 0;
        led0cnt <= 0;
    end
end

最后就是处理通过Transmitter发送给电脑端的用于回复两个led状态的数据了。
当state为4的时候, 在前面我们知道,已经设置好了led0state和led1state,我们只需要将这两个字节的数据通过transmitter发出去即可。
第一:我们要给出Transmitter的写使能信号并且要控制好这个信号存在的时间。因为两个模块的时钟不一致
第二:根据前面Trasmitter模块的实现代码,我们可以知道。需要在Transmitter模块接收到写使能信号的下一个时钟周期内给出需要发送的数据。 第三:Tranmitter发送一个完整的字节数据,需要先经过一个周期cnt开始计数并开始发送起始位。然后再经过160个周期才发送完毕。

     else if(pos_en == 1'b1)begin
      DataInReg <= DataIn;
      cnt_start <= 1'b1;
      TxBusy <= 1'b1; 

    always@(posedge baud16x or negedge rstn)
     if(!rstn)
      cnt <= 8'd0;
     else if(cnt_start == 1'b1)
      cnt <= cnt + 1'b1;
     else cnt <= 8'd0;
     //这两部分看出,接收到写入信号的上升沿后1个周期才开始计数
     
     
      else if(cnt >= 8'd160) begin//一个数据帧传输完毕
      cnt_start <= 1'b0;
      TxBusy <= 1'b0;
     end
     //这里说明160个周期之后才发送完毕

之前在state为4时,通过设置分频计数器LED0Divider和LED1Divider来改变led的闪烁频率。所以考虑在同样的时刻,开始回复led的状态给电脑端。
前面处理改变led闪烁频率是,时在state为4的时钟周期内设置好分频相关参数LED0Divider和LED1divider。然后就跳转到state =0;在state为0的时候真正执行控制led闪烁的逻辑。
同样在回复ledstate的时候,也只是在state为4的情况下设置回复开始标志,然后跳转到state为0的状态下执行回复的完整过程。

reg [31:0] ledstateTranscnt;//控制回复ledsatte的计数器
reg startTransledstate;//表示处于给电脑端回复led状态的过程中

//首先在刚刚上面设置LED0Divider的地方设置开始回复标志startTransledstate
    ....
    else if(state ==4) begin //表明命令都已经读取了
           led0cnt <= 0;
           led1cnt <= 0;
           led0shine <= 0;
           led1shine <= 0;
           startTransledstate <= 1;
           ledstateTranscnt <= 0;
    ....

/发送回复的过程
always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin 
        ledstateTranscnt <= 0;
    end
    else if(startTransledstate == 1) begin //处于传输状态时 cnt一直加1
        ledstateTranscnt <= ledstateTranscnt + 1;
    end
    else ledstateTranscnt <= 0;
end


always@(posedge clk50mhz or negedge rstn) begin 
    if(!rstn) begin
        ledstate <= 8'hFF;
        TransmitterWrEn <= 0;   
    end
    else case(ledstateTranscnt) 
    32'd0: TransmitterWrEn<=1;
    32'd325:begin TransmitterWrEn<= 0; ledstate<= led0state;end
    32'd52650: TransmitterWrEn <= 1;//325*162=52650,第一个led的状态传输完成
    32'd52975:begin ledstate<= led1state;TransmitterWrEn <= 0;end
    32'd105300:startTransledstate <= 0;//324*325=105300表示第二个led的状态也传输完毕   
    endcase    
end

首先当开始回复两个led的状态时。ledstateTranscnt开始计数。第一个周期(0-325)给Transmitter一个写使能信号,第二个周期Transmitter就会读取stateled的数值,然后经过 Transmitter的160个周期 第一个字节传输完成。类似的也可以完成第二个字节的传输。传输完成之后将startTransledstate置为0,表示传输结束。


本文章使用limfx的vsocde插件快速发布