串口控制led实验

串口通信协议

串口的一次发送或接收由四个部分组成:

  1. 起始位S(一般为逻辑‘0’);
  2. 数据位D0到D7(一般为6位到8位之间可变,数据低位在前);
  3. 校验位(奇校验、偶检验或不需要校验位);
  4. 停止位(通常为1位、1.5位、2位),停止位必须为逻辑1。

波特率: 数据传输速率。
在一次串口通信过程中,数据接收与发送双方没有共享时钟,因此,双方必须协商好数据传输波特率。
根据双方协议好的传输速率,接收端即可对发送端的数据进行采样。

串口的接收与发送,其主要时序设计包括两个部分:

  1. 波特率的产生时序;
  2. 数据传输时序,包括接收与发送。

波特率产生时序设计: 假设FPGA输入时钟100Mhz,为得到常用的波特率,仍然采用计数分频来得到。BAUD_DIV=100_000000/波特率。其中采样中心点为发送或接收时钟的中心点,即BAUD_DIV_CAP=100_000000/(2*波特率)。该部分在数据接收和发送部分均单独完成。

数据接收模块:在设置好传输波特率的情况下,根据串口传输时序,进行解串。空闲状态时,接收数据为逻辑高电平,等待起始位逻辑低电平的到来。当起始位到达后,由低位到高位,依次采集8位数据,并进行相应的解串,存入临时寄存器。接收有效数据完成后,判断结束位,接收完毕。

数据发送模块:设置发送使能信号和待发送的数据。通过计数器,表示10个数据发送的周期。这10个数据,依次为起始位、8位数据位、1位结束位,实现数据位的逐个发送。

代码整体结构思路

参考教程《14 从串口程序感受FPGA编程艺术》以及csdn博客使用Uart串口控制LED灯闪烁

代码分为三部分:

  1. uart的数据接收模块:将用户数据通过串口调试助手发到fpga(uart_rx_byte.v)
  2. 串口命令模块:处理uart接受模块的数据,通过一个自定义协议来发送led灯所需的控制信号(uart_cmd.v)
  3. led工作状态的控制模块:闪烁时间和闪烁状态由用户指定(led_ctrl_time.v)

串口命令模块对于控制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

led灯工作状态控制

通过两个计数器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插件快速发布