FOC:【3】精品必看!利用Python实现System Verilog多字节UART串口有限状态机自动生成脚本

碎碎念:

向关注的朋友们道个歉,不好意思这一期鸽了这么久。( ̄(工) ̄)

这是一个懒狗不想写1000行的状态机,所以写了1000行的脚本的故事。

虽然本期内容与FOC的直接相关度并不大,由于是在整个项目中的一个小环节,因此还是放在这个专栏里面了。在FPGA片上调试数据收发时,多字节串口收发始终是我个人比较讨厌的环节,状态机的编写实在是让我苦不堪言(重复劳动过多)。

于是乎,在一晚上手写了1400多行状态机代码后,我实在受不了了,最后咨询J师兄,决定用Python写一个脚本,来实现自动读取Excel中的指令接收以及信息发送定义信息,利用字符串操作自动生成对应的System Verilog文件。

本文主要介绍一下简单的实现逻辑,并给出Python部分的代码,源码文件获取方法写在了文章结尾。

目录

1 主要思路

2 Receive与Send模块的输入输出端口设计

2.1 Receive模块的端口设计

2.2 Send模块的端口设计

3 控制台:Excel文件部分

3.1 Receive

3.2 Send

4 程序主体:Python实现部分

4.1 Receive

4.2 Send

5 运行结果:System Verilog部分

5.1 Recive

5.2 Send


Verilog多字节串口收发最优方案

 1 主要思路

本文的思路其实很清晰,但是读者最好首先对UART串口单字节收发时序有一定了解,之后阅读本文代码时,思路也会清晰很多。

目前默认大家已经了解单字节收发的原理啦,如果读者有需要的话,可以留言告诉我,我会单独写一篇介绍单字节收发时序的内容,也可以算是对本文的底层基础的进一步说明~

对应到FOC任务上,有以下两个具体需求(注意,下面的主语都是FPGA开发板,而不是上位机):

  1. 接收指令:当识别到指令头FF时,进一步开始识别指令的具体类别,之后按照类别将对应字节数的数据存储到寄存器中。这里需要注意的是,不同类型的指令所具有的字节数是不同的。
  2. 发送数据:由于不同数据具有的字节数不同,当需要发送多字节数据时,需要将之拆分成单字节,利用状态机以及单字节发送模块实现对上位机的发送。

有了具体需求,再看看实现可能性:

Python有一个很方便的库叫做xlwings,用来读取Excel中的信息;多字节收发的状态机编写过程中又具有较多的机械重复性(三段式状态机结构固定),因此必定是可以实现的。

2 Receive与Send模块的输入输出端口设计

在正式开始设计前,首先应该定义具体的端口类型(即使是可变的),这对整体时序逻辑非常重要。为了便于说明,我以脚本最终生成的端口为例进行展示。

2.1 Receive模块的端口设计

 这一模块的输入输出端口数量是固定的,下面分别进行说明:

  1. sys_clk_50m:50MHz的系统时钟
  2. sys_rst_n:系统复位信号
  3. uart_rx:串口接收信号
  4. Comm_type:表示当前指令的类型,宽度以8为倍数会自动改变,会结合Excel中对应字段的宽度进行修改(这样的好处是不会限制指令类型数量在128)
  5. Comm_content:表示指令的内容,这一字段是复用的,因此其宽度仅取决于内容最长的指令宽度,也是自动改变的。
  6. Comm_en:指令内容有效信号,当其为高电平,表示当前指令内容存储完毕。

2.2 Send模块的端口设计

用来将设定需要发送的内容传递给模块,内部利用状态机将数据按顺序逐字节发送给上位机。为了便于阅读,因此输入端口数量是自动改变的,下面进行分别说明:

  1. sys_clk_50m:50MHz的系统时钟
  2. sys_rst_n:系统复位信号
  3. uart_tx:串口发送信号
  4. Start_trans:高电平来临,表示开始发送数据
  5. 02部分,表示需要输入的7类数据(请忽略我乱起的名字,开始精神错乱受到Rick启发哈哈哈哈)

3 控制台:Excel文件部分

在已知需求的情况下,我对Excel表格进行了如下设计:

上述两个图是同一个Excel文件中的两个sheet,分别对应了FPGA Receive以及FPGA Send两个需求部分。

值得注意的是,表格的行数是可变的,可以随时增减行数,代码会进行自动识别。

3.1 Receive

这部分是为了对应需求1,实现:识别指令头-识别指令类型-接收指令内容三个部分的内容。

因此我设计了如下的几个属性列:

  1. 序号:用来便于表格的可读性,同时在实际工程应用中,便于工程文档与代码的对应。
  2. 指令名称(英文):用于在代码中方便给状态机起名字,增加代码的可读性。
  3. 指令名称(中文):不影响生成的代码,为了增加Excel可读性。
  4. 固定指令头(二进制,数值要相同):当识别到这部分时,表示开始接收指令了,作为状态机的Idle跳出条件,这个字段的字节长度是不可变的,但是内容可以自己修改。为了方便我就默认为了FF(注意其中的下划线时可有可无的,在Python进行了鲁棒性处理)。
  5. 指令类型(二进制):识别到指令来临,利用这一字段来判断具体指令类型。为了增加复用性,增大指令的表示数量,可以自行增加其到多字节(二字节宽度需为8的倍数)。
  6. 指令字节数:表示当前指令具体包含多少字节,需要为整型,可以为0。0时表示直接输出一个指令有效信号,但是指令类型就是本行对应的指令类型。可以用来执行特殊操作,这里我设置为了数据清零(本文不体现,在工程别的模块里)。
  7. 备注:为了提高Excel的可读性,不影响代码的生成。
  8. 模块名称:决定了脚本生成的System Verilog代码的模块名称。

可能有读者会问,指令头FF会不会受到指令内容中的FF影响呢?其实是不会的,因为是利用状态机来实现的,当进入对应状态的时候,就只会识别为对应位的指令内容,而不是指令头。

3.2 Send

这部分是为了需求2,实现:接收信息输入-发送数据两个部分的内容。

因此我设计了如下的属性列:

  1. 序号:用来便于表格的可读性,同时在实际工程应用中,便于工程文档与代码的对应。
  2. 信息名称(英文):用于在代码中方便给状态机起名字,以及决定模块输入接口的名字,增加代码的可读性。
  3. 信息名称(中文):不影响生成的代码,为了增加Excel可读性。
  4. 信息字节数:对应每一类信息的字节数,影响代码中变量宽度。
  5. 备注:为了提高Excel的可读性,不影响代码的生成。
  6. 模块名称:决定了脚本生成的System Verilog代码的模块名称。

 感觉说得有些混乱,有问题的可以随时给我提出~

4 程序主体:Python实现部分

在确定了具体的模块需求以及Excel模块,就可以开始愉快(bushi)地编写Python脚本代码啦,下面我将会先提出两个部分代码中需要注意的地方以及编写思路,之后给出代码~

这部分是利用Python中的字符串操作来实现的,需要读者对Verilog逻辑有比较深入的理解,反而是Python语言的基础要求不高。

4.1 Receive

这一部分代码的tips如下:

  1. 在读取Excel数据时,需要注意读取到的字符串还是数字,对于读取类似“1111_1111”这种数据时,需要添加鲁棒性处理(删除“_”)。
  2. Excel数据需要首先定位一共有多少行是有效信息,这里当读取到None时,表示已经跳出了有效信息行范围。
  3. 指令的总字节数会影响状态机的总状态数,因此要先计数总数,判断一共需要多少个状态,同时确定状态变量的宽度。
  4. 指令内容的最大字节数,影响指令内容输出寄存器的宽度,因此也要进行确定。
  5. 当指令类型超过一字节可以表示范围时,指令类型确定就不能只使用一个状态实现了,因此需要对状态机的生成逻辑进行对应修改(对完美主义者不友好啊,这真是牵一发而动全身,这里我修改了整一天)。
  6. 当指令内容没有完全占用整个输出寄存器的时候,从高位到地位逐个占用。这里需要注意存储的对应到底是哪一位。
  7. 为了提高输出System Verilog代码的可读性以及易用性,同步输出了数据对应的位置、模块使用案例、以及状态机的图案。

目前就想到了这么多,之后在使用过程中,我也随时来更新这部分细节。其实很多地方我在代码中也有清晰的注释。

以上面Excel中的内容为例子,代码运行过程,控制台打印出下面的内容:

代码如下:

1# -*- coding: utf-8 -*-
"""
Created on Sat Jul  9 22:05:06 2022

@author: Alex_1
用途:读取Excel指令信息,自动生成状态机,实现不同指令的读取任务
"""
# 打开对应的表格
import xlwings as xw
wb      = xw.Book(".\\uart.xlsx")
receive = wb.sheets["receive"]
#send    = wb.sheets["send"]
print("表格文件已打开")

# 定义需要的列的名称
column_signal=['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
column_signal.extend(['AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM','AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ'])
column_signal.extend(['BA','BB','BC','BD','BE','BF','BG','BH','BI','BJ','BK','BL','BM','BN'])

# 获取当前时间
import datetime as dt
now_time = dt.datetime.now().strftime('%F %T')
print("代码文件创建时间:"+now_time)

# 获取模块名称
Module_name = ""
Module_name = receive.range(column_signal[7] + str(2)).value
print("模块名称:"+Module_name)

# 确定指令的最大数量以及对应的行列范围
row_max= 0
temp = "temp"
while(1):
    row_max = row_max + 1
    temp = receive.range(column_signal[0] + str(row_max)).value
    if(temp == None):
        break
row_max = row_max - 1
print("表格有效数据范围:1-"+str(row_max))

# 获取指令头
Command_Head = receive.range(column_signal[3] + str(2)).value
print("指令头:" + str(Command_Head))

# 获取全部的指令名称(英文)
Command_Type = []
for i in range(2,row_max+1):
    temp = receive.range(column_signal[1] + str(i)).value
    Command_Type.append(temp)
print("指令名称:" + str(Command_Type))

# 获取全部的指令类型(二进制)
Command_Type_binary = []
for i in range(2,row_max+1):
    temp = receive.range(column_signal[4] + str(i)).value
    Command_Type_binary.append(temp)
print("指令类型列表:" + str(Command_Type_binary))

# 获取输出指令类型的宽度(需要多少个字节)
Command_Type_Length = 0
Style_example = str(receive.range(column_signal[4] + str(2)).value).replace("_","")
Style_example_len = len(Style_example)
for i in range(0,64):
    if(i*8 >= Style_example_len):
        Command_Type_Length = i
        break
print("指令类型字节数:" + str(Command_Type_Length)) 

#Command_Type_Length = 3

#计算所需要的全部状态数
State_num = 1 #(包含了Idle)
State_num += Command_Type_Length #算上Idle,如果这个是两个字节的,就增加状态
for i in range(2,row_max+1):
    State_num += receive.range(column_signal[5] + str(i)).value
    if(receive.range(column_signal[5] + str(i)).value == 0):
        State_num += 1
State_num = int(State_num)
print("状态机状态数量:"+str(State_num))


#利用状态数量,判断需要多少位存储状态数
State_bit = 0
for i in range(0,64):
    if(2**i >= State_num):
        State_bit = i
        break
print("状态变量位宽:" + str(State_bit))        

# 获取输出指令的宽度(根据最大字节长度判断),同时获取每个Type的字节数量
Max_Command_length = 0
Command_Type_num   = []
for i in range(2,row_max+1):
    temp = receive.range(column_signal[5] + str(i)).value
    Command_Type_num.append(int(str(temp).replace(".0","")))
    if(temp > Max_Command_length):
        Max_Command_length = temp
Max_Command_length_Byte = Max_Command_length
Max_Command_length_Bit  = Max_Command_length_Byte * 8
print("最大指令字节数:" + str(int(Max_Command_length_Byte)))


# 根据当前的数量,建立状态机状态列表
State_name = ["Idle"]

if(Command_Type_Length == 1): #根据指令类型变量的宽度,确定这个部分需要几个状态
    State_name.append("Get_Type")
else:    
    for j in range(Command_Type_Length-1,-1,-1):
         State_name.append("Get_Type_"+str(j))
            
for i in range(0,row_max-1):
    if(Command_Type_num[i] == 0):
        State_name.append(Command_Type[i])
    elif(Command_Type_num[i] == 1):
        State_name.append(Command_Type[i])
    else:
        for j in range(Command_Type_num[i]-1,-1,-1):
            State_name.append(Command_Type[i]+"_"+str(j))
print("状态机状态列表:" + str(State_name))

# 获取每个指令状态循环的起点索引
State_Start_index = []
for i in range(0,len(Command_Type_binary)):
    s = 1 + Command_Type_Length              #这里就考虑到了类别字节不是1字节的情况
    for j in range(0,i):
        if(Command_Type_num[j] == 0):
            s += 1
        else:
            s += Command_Type_num[j]
    State_Start_index.append(s) 
print("各指令状态起点索引:" + str(State_Start_index))

# 单独创建列表存储Type部分的循环序列索引
State_Type_list_index = []
if(Command_Type_Length > 1):
    for i in range(0,Command_Type_Length):
        State_Type_list_index.append(i+1)
    State_Type_list_index.append(0)
    print("Type部分转移路径:" + str(State_Type_list_index))

# 获取每个指令状态循环的序列索引
State_list_index = []
for i in range(0,len(Command_Type_binary)):  #默认产生不包括Type部分的循环
    single_list_index = []
    single_list_index.append(State_Start_index[i])
    if(Command_Type_num[i] == 0):
        single_list_index.append(0)
    else:
        for j in range(1,Command_Type_num[i]):
            single_list_index.append(State_Start_index[i] + j)
        single_list_index.append(0)
    State_list_index.append(single_list_index)
print("各指令转移路径:" + str(State_list_index))

# 便于可视化,绘制状态转移图
print()
print("******************************************************状态转移图(简)******************************************************")
for i in range(0,len(State_list_index)):
    temp = "#" + str(i)+ " "*(6-len(str(i))) + Command_Type[i] + " "*(16-len(Command_Type[i])) + ": 0 ("+ State_name[0] +")→"
    for j in range(0,len(State_list_index[i])):
        temp += str(State_list_index[i][j])
        temp = temp +" (" + State_name[State_list_index[i][j]] + ")"
        if(j < len(State_list_index[i])-1):
            temp += "→"
    print(temp)
    
# 编写代码
with open(Module_name + ".sv","w") as f:
    #打印模块版权信息
    f.write("`timescale 1ns / 1ps \n")
    f.write("//\n")
    f.write("// Create Date: " + str(now_time) + "\n")
    f.write("// Module Name: " + Module_name + "\n")
    f.write("// Description: Test uart auto Machine \n")
    f.write("// ********** Powered By Alex_1 in 2022.07 ********** \n")
    f.write("// ****************** wangy.fun :)******************* \n")
    f.write("//\n")
    f.write("// ****************** Comm_content ****************** \n")
    #打印输出的指令各自对应的是Comm_content中的哪几位,便于复制使用
    for i in range(0,len(Command_Type)):
        temp = "// "
        temp += Command_Type[i] + " "*(20-len(Command_Type[i])) + ": Comm_content ["
        temp += str(int(Max_Command_length_Byte*8)-1)
        temp += ":"
        temp += str(int(Max_Command_length_Byte-len(State_list_index[i])+1)*8)
        temp += "]\n"
        f.write(temp)
    f.write("//\n")
    f.write("// ****************** User Example ****************** \n")
    # 打印模块的使用案例
    f.write("// " + Module_name + " " + Module_name + "(\n")
    f.write("//     .sys_clk_50m         (                ),       // System Clock\n")
    f.write("//     .sys_rst_n           (                ),       // System Reset\n")
    f.write("//     .uart_rx             (                ),       // Uart RX\n")
    f.write("//     .Comm_type           (                ),       // " + str(Command_Type_Length) + " Bytes \n")
    f.write("//     .Comm_content        (                ),       // " + str(int(Max_Command_length_Byte)) + " Bytes \n")         
    f.write("//     .Comm_en             (                )        // Comment Enable\n")
    f.write("//     );\n")
    f.write("//\n")
    f.write("//******************************************************State Transition Diagram******************************************************\n")
    for i in range(0,len(State_list_index)):
        temp = "// #" + str(i)+ " "*(6-len(str(i))) + Command_Type[i] + " "*(16-len(Command_Type[i])) + ": 0 ("+ State_name[0] +") --- "
        for j in range(0,len(State_list_index[i])):
            temp += str(State_list_index[i][j])
            temp = temp +" (" + State_name[State_list_index[i][j]] + ")"
            if(j < len(State_list_index[i])-1):
                temp += " --- "
        temp += "\n"
        f.write(temp)
    f.write("//\n\n")
    
    #打印模块接口定义部分
    f.write("module " + Module_name + "(\n")
    f.write("//00 System Clock and Reset-----------------------------------------------------------------------------------------------------------------\n")
    f.write("input  wire         sys_clk_50m,      // 50MHz\n")
    f.write("input  wire         sys_rst_n,        // Reset Signal\n\n")
    f.write("//01 UART RX\n")
    f.write("input  wire         uart_rx,          // UART_RX\n\n")
    f.write("//02 Command Output\n")
    f.write("output reg   ["+str(int(Command_Type_Length*8-1))+":0]  Comm_type,        // Command Type\n")
    f.write("output reg   ["+str(Max_Command_length_Bit-1).replace(".0","")+":0] Comm_content,     // Command Content\n")         
    f.write("output reg          Comm_en           // Command Enable\n")
    f.write(");\n\n")
    
    #打印模块状态定义部分
    f.write("// Define Needed State----------------------------------------------------------------------------------------------------------------\n")
    for i in range(0,State_num):
        temp  = "parameter  "
        temp +=  State_name[i]
        temp += " "*(30-len(State_name[i]))
        temp += "=   "
        temp += str(State_bit)
        temp += "\'d"
        temp += str(i)
        temp += ";\n"
        f.write(temp)
    f.write("\n\n")
    
    #打印需要的寄存器与导线
    f.write("// Define Registers and Conductor----------------------------------------------------------------------------------------------------------------\n")
    f.write("reg  [" + str(State_bit-1) + ":0] current_state;\n")
    f.write("reg  [" + str(State_bit-1) + ":0] next_state;\n")
    f.write("wire [7:0] uart_data;\n")
    f.write("wire       uart_done;\n\n")
    f.write("reg        uart_done_d0;\n")
    f.write("reg        uart_done_d1;\n")
    f.write("wire       uart_posedge;\n")
    f.write("assign     uart_posedge = (uart_done_d0)&&(~uart_done_d1);\n\n\n")
    
    #检测uart_done的上升沿信号
    f.write("// Detect rising edges of uart_done----------------------------------------------------------------------------------------------------------------\n")
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("     if (!sys_rst_n)\n")
    f.write("     begin\n")
    f.write("         uart_done_d0 <= 1'b0;\n")
    f.write("         uart_done_d1 <= 1'b0;\n")
    f.write("     end\n")
    f.write("     else\n")
    f.write("     begin\n")
    f.write("         uart_done_d0 <= uart_done;\n")
    f.write("         uart_done_d1 <= uart_done_d0;\n")
    f.write("     end\n")
    f.write("end\n\n\n")
    
    #三段式第一段:状态跳转
    f.write("//----------------------------------------------------------Three-stage---------------------------------------------------------------------------\n")
    f.write("//First part: statement transition----------------------------------------------------------------------------------------------------------------\n")
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("     if (!sys_rst_n)\n")
    f.write("     current_state <= Idle;\n")
    f.write("     else\n")
    f.write("     current_state <= next_state;\n")
    f.write("end\n\n\n")
    
    #三段式第二段:
    f.write("//Second part: combination logic, judge statement transition condition-----------------------------------------------------------------------------------\n")
    f.write("always @(*)\n")
    f.write("begin\n")
    f.write("    case(current_state)\n")
    f.write("        Idle:\n")
    f.write("        begin\n")
    f.write("            if(uart_posedge)\n")
    f.write("            begin\n")
    f.write("                if(uart_data == "+str(len(str(Command_Head).replace("_","")))+"'b" + str(Command_Head) + ")\n")
    f.write("                begin\n")
    if(Command_Type_Length > 1): #当状态转移需要多个字节
        f.write("                    next_state <= "+ str(State_name[State_Type_list_index[0]]) +";\n")
    else:
        f.write("                    next_state <= Get_Type;\n")
    f.write("                end\n")
    f.write("                else\n")
    f.write("                begin\n")
    f.write("                    next_state <= Idle;\n")
    f.write("                end\n")
    f.write("            end\n")
    f.write("            else\n")
    f.write("            begin\n")
    f.write("                next_state <= Idle;\n")
    f.write("            end\n")
    f.write("        end\n\n")
    
    f.write("//Get_Type\n")
    if(Command_Type_Length > 1):  #当指令类型字节数大于等于2
        for i in range(0,len(State_Type_list_index)-1):
            start_state = State_name[State_Type_list_index[i]]
            next_state  = State_name[State_Type_list_index[i+1]]
            f.write("        " + start_state + ":\n")
            f.write("        begin\n")        
            f.write("            if(uart_posedge)\n")
            f.write("            begin\n")
            
            if(i+1 < len(State_Type_list_index)-1):  #当还处于数据存储阶段
                f.write("                next_state <= "+next_state+";\n")
            else:#处于跳转判断阶段
                #由于差一个周期,因此这里根据符号跳转部分,需要利用Comm_type的高几位以及实时uart_data结合组成
                f.write("                case({Comm_type["+str(int(Command_Type_Length*8-1))+":8],uart_data[7:0]})\n")
                for i in range(0,len(Command_Type_binary)):
                    temp = "                    "
                    temp += str(Command_Type_Length*8)
                    temp += "\'b"
                    temp += str(Command_Type_binary[i])
                    temp += ": next_state <= "
                    temp += State_name[State_Start_index[i]]
                    temp += ";\n"
                    f.write(temp)    
                f.write("                    default:      next_state <= Idle;\n")
                f.write("                endcase\n")
            f.write("            end\n")
            f.write("            else\n")
            f.write("            begin\n")
            f.write("                next_state <= "+start_state+";\n")
            f.write("            end\n")
            f.write("        end\n\n")
            
    else:    #当指令类型字节数等于1
        f.write("        Get_Type:\n")
        f.write("        begin\n")
        f.write("            if(uart_posedge)\n")
        f.write("            begin\n")
        f.write("                case(uart_data)\n")
        for i in range(0,len(Command_Type_binary)):
            temp = "                    "
            temp += str(len(str(Command_Type_binary[i]).replace("_","")))
            temp += "\'b"
            temp += str(Command_Type_binary[i])
            temp += ": next_state <= "
            temp += State_name[State_Start_index[i]]
            temp += ";\n"
            f.write(temp)    
        f.write("                    default:      next_state <= Idle;\n")
        f.write("                endcase\n")
        f.write("            end\n")
        f.write("            else\n")
        f.write("            begin\n")
        f.write("                next_state <= Get_Type;\n")
        f.write("            end\n")
        f.write("        end\n\n")
    
    for i in range(0,len(Command_Type_binary)):   #遍历全部的指令
        temp = State_list_index[i]
        f.write("//"+ str(i) + "  " + Command_Type[i] +"\n")
        #print(temp)
        if(Command_Type_num[i] == 0):      #0字节指令
            #print("该状态直接跳回起点,使用简单模板")
            f.write("        " + State_name[temp[0]] + ":\n")
            f.write("        begin\n")
            f.write("            next_state <= Idle;\n")
            f.write("        end\n\n")
        else:   #非0字节指令
            #print("使用常规模板")
            for j in range(0,len(temp)-1):
                #print("j:" + str(j))
                start_state = State_name[temp[j]]
                next_state  = State_name[temp[j+1]]
                #print("start_state:" + start_state)
                #print("next_state:" + next_state)
                f.write("        "+start_state+":\n")
                f.write("        begin\n")
                f.write("            if(uart_posedge)\n")
                f.write("            begin\n")
                f.write("                next_state <= "+next_state+";\n")
                f.write("            end\n")
                f.write("            else\n")
                f.write("            begin\n")
                f.write("                next_state <= "+start_state+";\n")
                f.write("            end\n")
                f.write("        end\n\n")
    f.write("        default:\n")
    f.write("        begin\n")
    f.write("            if(uart_posedge)\n")
    f.write("            begin\n")
    f.write("                next_state <= Idle;\n")
    f.write("            end\n")
    f.write("            else\n")
    f.write("            begin\n")
    f.write("                next_state <= Idle;\n")
    f.write("            end\n")
    f.write("        end\n")
    f.write("    endcase\n")
    f.write("end\n\n\n")
    
    #三段式第三段
    f.write("//Last part: output data-----------------------------------------------------------------------------------\n") 
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("    if (!sys_rst_n)\n")
    f.write("    begin\n")
    f.write("        Comm_type    <= "+str(Command_Type_Length*8)+"'d0;\n")
    f.write("        Comm_content <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")
    f.write("        Comm_en      <= 1'd0;\n")
    f.write("    end\n")
    
    f.write("    else\n")
    f.write("    begin\n")
    f.write("        case(current_state)\n")
    f.write("            Idle:\n")
    f.write("            begin\n")
    f.write("                if(uart_posedge)\n")
    f.write("                begin\n")
    f.write("                    Comm_type    <= "+str(Command_Type_Length*8)+"'d0;\n")
    f.write("                    Comm_content <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")
    f.write("                    Comm_en      <= 1'd0;\n")
    f.write("                end\n")
    f.write("                else\n")
    f.write("                begin\n")
    f.write("                    Comm_type    <= "+str(Command_Type_Length*8)+"'d0;\n")
    f.write("                    Comm_content <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")
    f.write("                    Comm_en      <= 1'd0;\n")
    f.write("                end\n")
    f.write("            end\n\n")
    
    f.write("//Get_Type\n")
    if(Command_Type_Length > 1):  #当指令类型字节数大于等于2
        
        for i in range(0,len(State_Type_list_index)-1):
            start_state = State_name[State_Type_list_index[i]]
            next_state  = State_name[State_Type_list_index[i+1]]
            f.write("        " + start_state + ":\n")
            f.write("        begin\n")        
            f.write("            if(uart_posedge)\n")
            f.write("            begin\n")
            #按字节存储对应数据
            #print(i)
            index_x = int((Command_Type_Length-i)*8)-1
            index_y = str(index_x - 7)
            index_x = str(index_x)
            f.write("                Comm_type["+index_x+":"+index_y+"] <= uart_data;\n")
            f.write("            end\n")
            f.write("            else\n")
            f.write("            begin\n")
            f.write("                Comm_type <= Comm_type;\n")
            f.write("            end\n")
            f.write("        end\n\n")
        
        
        
    else:  #指令字节数是1
        f.write("            Get_Type:\n")
        f.write("            begin\n")
        f.write("                if(uart_posedge)\n")
        f.write("                begin\n")
        f.write("                    case(uart_data)\n")
        for i in range(0,len(Command_Type_binary)):
            temp = "                        "
            temp += str(len(str(Command_Type_binary[i]).replace("_","")))
            temp += "\'b"
            temp += str(Command_Type_binary[i])
            temp += ": Comm_type <= "+str(Command_Type_Length*8)+"'d"
            temp += str(i)
            temp += ";\n"
            f.write(temp)  
        f.write("                        default:      Comm_type <= "+str(Command_Type_Length*8)+"'d0;\n")
        f.write("                    endcase\n")
        f.write("                end\n")
        f.write("                else\n")
        f.write("                begin\n")
        f.write("                    Comm_type <= Comm_type;\n")
        f.write("                end\n")
        f.write("            end\n\n")
            
    
    for i in range(0,len(Command_Type_binary)):   #遍历全部的指令
        temp = State_list_index[i]
        f.write("//"+ str(i) + "  " + Command_Type[i] +"\n")
        #print(temp)
        if(Command_Type_num[i] == 0):      #0字节指令
            #print("该状态直接跳回起点,使用简单模板")
            f.write("            " + State_name[temp[0]] + ":\n")
            f.write("            begin\n")
            f.write("                Comm_content        <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")
            f.write("                Comm_en             <= 1'b1;\n")
            f.write("            end\n\n")
        else:   #非0字节指令
            #print("使用常规模板")
            for j in range(0,len(temp)-1):
                #print("j:" + str(j))
                start_state = State_name[temp[j]]
                next_state  = State_name[temp[j+1]]
                #print("start_state:" + start_state)
                #print("next_state:" + next_state)
                f.write("            "+start_state+":\n")
                f.write("            begin\n")
                f.write("                if(uart_posedge)\n")
                f.write("                begin\n")
                #需要根据i计算Comm_content的索引
                index_x = int((Max_Command_length_Byte-j)*8)-1
                index_y = str(index_x - 7)
                index_x = str(index_x)
                f.write("                    Comm_content["+index_x+":"+index_y+"] <= uart_data;\n")
                if(next_state == "Idle"):
                    f.write("                    Comm_en             <= 1'b1;\n")
                else:
                    f.write("                    Comm_en             <= 1'b0;\n")
                f.write("                end\n")
                f.write("                else\n")
                f.write("                begin\n")
                f.write("                    Comm_content        <= Comm_content;\n")
                f.write("                    Comm_en             <= 1'b0;\n")
                f.write("                end\n")
                f.write("            end\n\n")    
                                
    f.write("            default:\n")
    f.write("            begin\n")
    f.write("                if(uart_posedge)\n")
    f.write("                begin\n")
    f.write("                    Comm_type    <= "+str(Command_Type_Length*8)+"'d0;\n")
    f.write("                    Comm_content <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")     
    f.write("                    Comm_en      <= 1'd0;\n")
    f.write("                end\n")
    f.write("                else\n")
    f.write("                begin\n")
    f.write("                    Comm_type    <= "+str(Command_Type_Length*8)+"'d0;\n")
    f.write("                    Comm_content <= "+str(Max_Command_length_Bit).replace(".0","")+"'d0;\n")
    f.write("                    Comm_en      <= 1'd0;\n")
    f.write("                end\n")
    f.write("            end\n")
    f.write("        endcase\n")
    f.write("    end\n")
    f.write("end\n\n")
    
    #实例化单字节接收模块
    f.write("uart_recv uart_recv(\n")
    f.write(".sys_clk         ( sys_clk_50m     ),\n")
    f.write(".sys_rst_n       ( sys_rst_n       ),\n")
    f.write(".uart_rxd        ( uart_rx         ),\n")
    f.write(".uart_data       ( uart_data       ),\n")
    f.write(".uart_done       ( uart_done       )\n")
    f.write(");\n\n")

    f.write("endmodule\n")
                    
print("\n"+Module_name+".sv 生成完成!")

4.2 Send

对比Receive部分,Send的思路就简单了很多,无非就是将接收到到数据按顺序发送出去。一部分代码的tips如下:

  1. Excel数据需要首先定位一共有多少行是有效信息,这里当读取到None时,表示已经跳出了有效信息行范围。
  2. 需要结合信息名称以及信息的数量,字节宽度决定生成的模块的对应的接口数量以及位宽。
  3. 指令的总字节数会影响状态机的总状态数,因此要先计数总数,判断一共需要多少个状态,同时确定状态变量的宽度。
  4. 为了提高输出System Verilog代码的可读性以及易用性,同步输出了模块使用案例、以及状态机的图案(这个图就很简单,因为只有一个状态循环)。

以上面Excel中的内容为例子,代码运行过程,控制台打印出下面的内容: 代码如下:

1# -*- coding: utf-8 -*-
"""
Created on Mon Jul 11 09:14:38 2022

@author: Alex_1
用途:读取Excel指令信息,自动生成状态机,实现不同指令的读取任务
"""
# 打开对应的表格
import xlwings as xw
wb      = xw.Book(".\\uart.xlsx")
send    = wb.sheets["send"]
print("表格文件已打开")

# 定义需要的列的名称
column_signal=['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
column_signal.extend(['AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM','AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ'])
column_signal.extend(['BA','BB','BC','BD','BE','BF','BG','BH','BI','BJ','BK','BL','BM','BN'])

# 获取当前时间
import datetime as dt
now_time = dt.datetime.now().strftime('%F %T')
print("代码文件创建时间:"+now_time)

# 获取模块名称
Module_name = ""
Module_name = send.range(column_signal[5] + str(2)).value
print("模块名称:"+Module_name)

# 确定指令的最大数量以及对应的行列范围
row_max= 0
temp = "temp"
while(1):
    row_max = row_max + 1
    temp = send.range(column_signal[0] + str(row_max)).value
    if(temp == None):
        break
row_max = row_max - 1
print("表格有效数据范围:1-"+str(row_max))

# 获取信息的英文名称
Data_name = []
for i in range(2,row_max+1):
    temp = send.range(column_signal[1] + str(i)).value
    Data_name.append(temp)
print("信息名称:" + str(Data_name))

#  获取每类信息的字节数
Data_Byte = []
Data_Byte_sum = 0
for i in range(2,row_max+1):
    temp = int(send.range(column_signal[3] + str(i)).value)
    Data_Byte.append(temp)
    Data_Byte_sum += int(temp)
print("各信息字节数:" + str(Data_Byte))
print("总字节数:" + str(Data_Byte_sum))

# 总状态数
State_num = 2         #原始包含Idle 以及Savedata
State_num += Data_Byte_sum
print("总状态数:" + str(State_num))

#利用状态数量,判断需要多少位存储状态数
State_bit = 0
for i in range(0,64):
    if(2**i >= State_num):
        State_bit = i
        break
print("状态变量位宽:" + str(State_bit))  

# 定义全部的状态名称
State_name = ["Idle", "Save_Data"]
for i in range(0, len(Data_name)):
    if(Data_Byte[i] == 1):
        temp = "State_" + Data_name[i]
        State_name.append(temp)
    else:
        for j in range(Data_Byte[i]-1,-1,-1):
            temp = "State_" + Data_name[i]
            temp += "_"
            temp += str(j)
            State_name.append(temp)
print("状态机状态列表:" + str(State_name))

# 定义全部的输入接口,
interface = []
for i in range(0, len(Data_name)):
    index_start = str(Data_Byte[i]*8-1)
    temp = ""
    temp += "["
    temp += index_start
    temp += ":"
    temp += "0]"
    interface.append(temp) 
print("输入接口宽度列表:" + str(interface))

# 计算存储时需要的分段情况
save_part = []
for i in range(0, len(Data_name)):
    for j in range(Data_Byte[i]*8-1,-1,-8):
        temp = Data_name[i]
        temp += "_Buff"
        temp += "["
        temp += str(j)
        temp += ":"
        temp += str(j-7)
        temp += "]"
        save_part.append(temp)
print("存储分段情况:"+str(save_part))

# 便于可视化,绘制状态转移图
print()
print("******************************************************状态转移图(简)******************************************************")
temp = ""
for i in range(0,len(State_name)):
    temp += str(i)
    temp += "("
    temp += State_name[i]
    temp += ")"
    if(i < len(State_name)-1):
        temp += " -- "
print(temp)

# 输入一堆数据,会改变模块的端口,这里就需要提供能够用来实例化复制的部分了!
# 编写代码
with open(Module_name + ".sv","w") as f:
    #打印模块版权信息
    f.write("`timescale 1ns / 1ps \n")
    f.write("//\n")
    f.write("// Create Date: " + str(now_time) + "\n")
    f.write("// Module Name: " + Module_name + "\n")
    f.write("// Description: Test uart auto Machine \n")
    f.write("// ********** Powered By Alex_1 in 2022.07 ********** \n")
    f.write("// ****************** wangy.fun :)******************* \n")
    f.write("//\n")
    f.write("// ****************** User Example ****************** \n")
    #打印模块的使用案例
    f.write("// " + Module_name + " " + Module_name + "(\n")
    f.write("//     .sys_clk_50m         (                ),       // System Clock\n")
    f.write("//     .sys_rst_n           (                ),       // System Reset\n")
    f.write("//     .uart_tx             (                ),       // Uart TX\n")
    f.write("//     .Start_trans         (                ),       // Start Send Data To Computer\n")
    for i in range(0, len(Data_name)):
        temp = "//     ."
        temp += Data_name[i]
        temp += (20-len(Data_name[i]))*" "
        temp += "(                )"
        if(i < len(Data_name)-1):
            temp += ",       // "
        else:
            temp += "        // "
        temp += str(Data_Byte[i])
        temp += " Bytes\n"
        f.write(temp)
    f.write("// );\n")    
    f.write("//\n")
    #打印状态转移图
    f.write("// ********************************************State Transition Diagram********************************************\n")
    temp = "// "
    for i in range(0,len(State_name)):
        temp += str(i)
        temp += "("
        temp += State_name[i]
        temp += ")"
        if(i < len(State_name)-1):
            temp += " -- "
    temp += "\n"
    f.write(temp)
    f.write("//\n\n")
    
    
    #打印模块接口定义部分
    f.write("module " + Module_name + "(\n")
    f.write("//00 System Clock and Reset-----------------------------------------------------------------------------------------------------------------\n")
    f.write("input  wire         sys_clk_50m,      // 50MHz\n")
    f.write("input  wire         sys_rst_n,        // Reset Signal\n\n")
    f.write("//01 UART TX\n")
    f.write("output wire         uart_tx,          // UART_TX\n")
    f.write("input  wire         Start_trans,      // Start Send Data To Computer\n\n")
    f.write("//02 Information for UART\n")
    for i in range(0, len(Data_name)):
        temp = "input  wire    "
        temp += interface[i]
        temp += " "*(15-len(interface[i]))
        temp += Data_name[i]
        if(i < len(Data_name)-1):
            temp += ",\n"
        else:
            temp += "\n"
        f.write(temp)
    f.write(");\n\n")
    
    #打印模块状态定义部分
    f.write("// Define Needed State----------------------------------------------------------------------------------------------------------------\n")
    for i in range(0,State_num):
        temp  = "parameter  "
        temp +=  State_name[i]
        temp += " "*(20-len(State_name[i]))
        temp += "=     "
        temp += str(State_bit)
        temp += "\'d"
        temp += str(i)
        temp += ";\n"
        f.write(temp)
    f.write("\n")
    
    #定义存储状态的变量
    f.write("// Define State ----------------------------------------------------------------------------------------------------------------\n")
    f.write("reg [" + str(State_bit-1) + ":0] current_state;\n")
    f.write("reg [" + str(State_bit-1) +":0] next_state;\n\n")
    
    #定义存储输入数据的寄存器
    f.write("// Define Data Buffer----------------------------------------------------------------------------------------------------------------\n")
    for i in range(0, len(Data_name)):
        temp = "reg    "
        temp += interface[i]
        temp += " "*(15-len(interface[i]))
        temp += Data_name[i]
        temp += "_Buff"
        temp += ";\n"
        f.write(temp)
    f.write("\n")
    
    #定义一些中间寄存器以及导线,并定义连接关系
    f.write("// Define intermediate variables and wires-------------------------------------------------------------------------------------------------\n")
    f.write("reg        tx_flag_d0;\n")
    f.write("reg        tx_flag_d1;\n")
    f.write("reg  [7:0] uart_data_buff;\n")
    f.write("reg  [1:0] clk_cnt;\n")
    f.write("reg        pause_done;\n\n")
    f.write("wire [7:0] uart_data;\n")
    f.write("wire       send_done;\n")
    f.write("wire       uart_en_wire;\n\n")

    f.write("assign send_done = tx_flag_d1 & (~tx_flag_d0);\n")
    f.write("assign uart_data = uart_data_buff;\n")
    f.write("assign uart_en_wire = (clk_cnt == 2'd2)? 1'b1: 1'b0;\n\n")
    
    #获取发送完成后的下降沿信号
    f.write("// Detect dropping edges of tx_flag----------------------------------------------------------------------------------------------------------------\n")
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("    if (!sys_rst_n)\n")
    f.write("    begin\n")
    f.write("        tx_flag_d0 <= 1'd0;\n")
    f.write("        tx_flag_d1 <= 1'd0;\n")
    f.write("    end\n")
    f.write("    else\n")
    f.write("    begin\n")
    f.write("        tx_flag_d0 <= tx_flag;\n")
    f.write("        tx_flag_d1 <= tx_flag_d0;\n")
    f.write("    end\n")
    f.write("end\n\n")
    
    #进行数据存储
    f.write("// Save Data----------------------------------------------------------------------------------------------------------------\n")
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("    if (!sys_rst_n)\n")
    f.write("    begin\n")
    
    for i in range(0, len(Data_name)):
        temp = "        " + Data_name[i]
        temp += "_Buff  <= "
        temp += str(Data_Byte[i]*8)
        temp += "'d0"
        temp += ";\n"
        f.write(temp)
    
    f.write("    end\n")
    f.write("    else\n")
    f.write("    begin\n")
    f.write("        if(current_state == Save_Data)\n")
    f.write("        begin\n")
    
    for i in range(0, len(Data_name)):
        temp = "            " + Data_name[i]
        temp += "_Buff  <=  "
        temp += Data_name[i]
        temp += ";\n"
        f.write(temp)
    
    f.write("        end\n")
    f.write("        else\n")
    f.write("        begin\n")
    
    for i in range(0, len(Data_name)):
        temp = "            " + Data_name[i]
        temp += "_Buff  <=  "
        temp += Data_name[i]
        temp += "_Buff;\n"
        f.write(temp)
    
    f.write("        end\n")
    f.write("    end\n")
    f.write("end\n\n")
    
    #三段式第一段:状态跳转
    f.write("//----------------------------------------------------------Three-stage---------------------------------------------------------------------------\n")
    f.write("//First part: statement transition----------------------------------------------------------------------------------------------------------------\n")
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("     if (!sys_rst_n)\n")
    f.write("     current_state <= Idle;\n")
    f.write("     else\n")
    f.write("     current_state <= next_state;\n")
    f.write("end\n\n\n")
        
    #三段式第二段:
    f.write("//Second part: combination logic, judge statement transition condition-----------------------------------------------------------------------------------\n")
    f.write("always @(*)\n")
    f.write("begin\n")
    f.write("    case(current_state)\n\n")
    
    f.write("        Idle:\n")
    f.write("        begin\n")
    f.write("            if (Start_trans)\n")
    f.write("            next_state <= Save_Data;\n")
    f.write("            else\n")
    f.write("            next_state <= Idle;\n")
    f.write("        end\n\n")
    
    f.write("        Save_Data:\n")
    f.write("        begin\n")
    f.write("            next_state <= " + State_name[2] + ";\n")
    f.write("        end\n\n")
    
    for i in range(2,len(State_name)-1):
        start_state = State_name[i]
        next_state  = State_name[i+1]
        f.write("        "+start_state+":\n")
        f.write("        begin\n")
        f.write("            if(clk_cnt == 2'd3)\n")
        f.write("            begin\n")
        f.write("                if(send_done == 1'd1)\n")
        f.write("                    next_state <= "+next_state+";\n")
        f.write("                else\n")
        f.write("                    next_state <= "+start_state+";\n")
        f.write("            end\n")
        f.write("            else\n")
        f.write("            begin\n")
        f.write("                next_state <= "+start_state+";\n")
        f.write("            end\n")
        f.write("        end\n\n")
    
    f.write("        "+State_name[-1]+":\n")
    f.write("        begin\n")
    f.write("            if(clk_cnt == 2'd3)\n")
    f.write("            begin\n")
    f.write("                if(send_done == 1'd1)\n")
    f.write("                    next_state <= "+State_name[0]+";\n")
    f.write("                else\n")
    f.write("                    next_state <= "+State_name[-1]+";\n")
    f.write("            end\n")
    f.write("            else\n")
    f.write("            begin\n")
    f.write("                next_state <= "+State_name[-1]+";\n")
    f.write("            end\n")
    f.write("        end\n\n")
    
    f.write("        default:\n")
    f.write("            next_state <= Idle;\n\n")
    f.write("    endcase\n")
    f.write("end\n\n")
    
    #三段式第三段
    f.write("//Last part: output data-----------------------------------------------------------------------------------\n") 
    f.write("always @(posedge sys_clk_50m or negedge sys_rst_n)\n")
    f.write("begin\n")
    f.write("    if (!sys_rst_n)\n")
    f.write("    begin\n")
    f.write("        clk_cnt        <= 2'd0;\n")
    f.write("        pause_done     <= 1'b0;\n")
    f.write("        uart_data_buff <= 8'd0;\n")
    f.write("    end\n")
    f.write("    else\n")
    f.write("    begin\n")
    f.write("        case(current_state)\n\n")
    
    for i in range(0,2):
        f.write("            "+State_name[i]+":\n")
        f.write("            begin\n")
        f.write("                clk_cnt        <= 2'd0;\n")
        f.write("                pause_done     <= 1'b0;\n")
        f.write("                uart_data_buff <= 8'd0;\n")
        f.write("            end\n\n")
    
    for i in range(2,len(State_name)):
        f.write("            "+State_name[i]+":\n")
        f.write("            begin\n")
        f.write("                uart_data_buff <= "+save_part[i-2]+";\n")
        f.write("                if((clk_cnt    <= 2'd2) && (pause_done == 1'd0))\n")
        f.write("                begin\n")
        f.write("                    clk_cnt    <= clk_cnt + 2'd1;\n")
        f.write("                end\n")
        f.write("                if(clk_cnt     == 2'd3)\n")
        f.write("                begin\n")
        f.write("                    pause_done <= 1'd1;\n")
        f.write("                end\n")
        f.write("                if((pause_done == 1'd1) && (send_done  == 1'd1))\n")
        f.write("                begin\n")
        f.write("                    pause_done <= 1'd0;\n")
        f.write("                    clk_cnt    <= 2'd0;\n")
        f.write("                end\n")
        f.write("            end\n\n")
    
    f.write("            default:\n")
    f.write("            begin\n")
    f.write("                clk_cnt        <= 2'd0;\n")
    f.write("                pause_done     <= 1'd0;\n")
    f.write("                uart_data_buff <= 8'd0;\n")
    f.write("            end\n\n")
    f.write("        endcase\n")
    f.write("    end\n")
    f.write("end\n\n")
    
    #实例化单字节模块
    f.write("uart_send uart_send(\n")
    f.write("    .sys_clk         (  sys_clk_50m    ),\n")
    f.write("    .sys_rst_n       (  sys_rst_n      ),\n")
    f.write("    .uart_din        (  uart_data      ),\n")
    f.write("    .uart_en         (  uart_en_wire   ),\n")
    f.write("    .uart_txd        (  uart_tx        ),\n")
    f.write("    .tx_flag         (  tx_flag        )\n")
    f.write(");\n\n")

    f.write("endmodule")

print("\n"+Module_name+".sv 生成完成!")

5 运行结果:System Verilog部分

这部分其实就是Python脚本的输出结果啦,下面简单介绍一下两个输出的文件,具体的时序逻辑就不详细介绍了,本文的重点还是在Python的实现上。

值得注意的是,生成的代码文件都包含了单字节收发的子模块,这两个子模块我是从正点原子的视频中参考修改得来的,读者可以自行搜索。

5.1 Recive

运行结果的文件头如下图所示:

包含了信息介绍、指令信息与寄存器索引关系、模块实例化例子、状态转移图。在用户体验上还是非常不错的。

后面就是比较常规的寄存器定义以及三段式逻辑啦,如果对串口时序比较熟悉,还是容易理解的,这里就不再赘述了。

经过Vivado仿真,也证明了模块的使用是正常的,下图给出Vivado的仿真结果:

5.2 Send

运行结果的文件头如下图所示:

这里包含了信息介绍、模块实例化例子、状态转移图。

后面的内容同理,也是三段式状态机来完成,不过这个包含了一些简单的子模块,用来检测当前字节是否发送完成。

经过Vivado仿真,也证明了模块的使用是正常的,下图给出Vivado的仿真结果:


如果有想要子模块源码文件以及对应Excel文件的小伙伴,可以在公众号“Alex的书桌与实验室”回复“2SV”获取下载链接~

这就是本期的全部内容啦,如果你喜欢我的文章,不要忘了点赞收藏,分享给身边的朋友哇~

猜你喜欢

转载自blog.csdn.net/Alex497259/article/details/125716885
今日推荐