2025寒假练 - 基于小脚丫FPGA STEP BaseBoard V4.0套件实现语音控制计算机
该项目使用了小脚丫FPGA STEP BaseBoard V4.0套件,Python语言,实现了语音控制计算机的设计,它的主要功能为:上位机识别语音,发送计算命令,FPGA接收命令进行计算,并在LCD屏幕上显示过程和结果。
标签
嵌入式系统
FPGA
数字逻辑
开发板
参加活动/培训
Ling_da_jin
更新2025-03-17
北京邮电大学
25

上位机可执行程序在这里

通过网盘分享的文件:2025寒假练上位机

链接: https://pan.baidu.com/s/1l7LfSm2GieBIFRN-PXV0yQ 提取码: 2hqn

项目介绍

  • 本项目为硬禾科技2025年寒假练任务三:语音控制计算机-使用大模型,使用小脚丫FPGA STEP BaseBoard V4.0套件和Python,通过上位机识别语音,将计算命令通过串口通讯发送给FPGAFPGA对命令解析后将算式进行计算,并在LCD屏幕上显示算式、计算过程和计算结果。

硬件介绍

  • 本项目基于小脚丫套件,STEP BaseBoard V4.0是第4代小脚丫FPGA扩展底板,可以用于全系列小脚丫核心板的功能扩展,采用100mm*161.8mm的黄金比例尺寸,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、87位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,配合小脚丫FPGA板能够完成多种实验,是数字逻辑、微机原理、可编程逻辑语言以及EDA设计工具等课程完美的实验平台。

方案框图和项目设计思路

为了实现本次任务,我将其分解为了以下几个任务:

  • 上位机发送串口数据
  • Uart串口接收
  • 运算式识别方法
  • 运算式存储方法
  • 运算式计算方法
  • LCD屏幕驱动显示任意字符

对于第1点,可以使用Python来写一个简易的上位机程序,通过调用语音识别库来将语音识别成运算式,再通过串口库发送出去。

对于第2点,由于本次任务提供的例程有现成的uart接收模块,所以仅需将其稍作移植,将uart_rx_data接线暴露出来即可。

对于第345点,可以定义一个串口传输协议,在接收到指定字符后进行相应操作,例如以”&”表示算式开始,以”$”表示算式结束,以”@”表示数字开始,”!”表示数字结束……

而为了在LCD屏幕上显示运算式,需要将传送过来的运算式存储到一个RAM中,而为了对算式进行运算,需要将发送来的字符型数字转换成二进制数字,再将其存储到RAM中,运算符也需要一个专门的RAM进行存储。

在一次接收完成后,协议解码模块可以发送一个get_ready信号来进行指示,计算模块在接收到该信号后开始工作,通过访问二进制数字RAM和运算符RAM来进行计算,而由于计算出的结果为二进制形式,为了在LCD屏幕上显示结果,需要将结果二进制数字转换成字符型数字ascii码形式,再将其存储到专门的结果RAM中。

对于第6点,由于我们已经把运算式和结果分别存储到了专门的RAM中,只需定义一个模块,将这两个RAM中的数据依次读取并显示到LCD屏幕中即可,而LCD驱动模块已在例程中给出,不过只给出了图片显示例程,为了显示任意字符还需定义一个字符显示模块和字符取模模块,将每个ascii码拆分为二进制,网上有专门的取模程序,再使用字符显示模块将其显示在LCD屏幕即可。

  • 具体的方案框图如下:

  • Verilog协议状态机框图如下:

定义了七个状态,分别为IDLE(闲置),STATE1(准备存储数字),NUM(存储数字),STATE3(数字存储完毕,准备存储运算符),OPERATOR(存储运算符),STATE5(运算符存储完毕),DONE(本次运算存储完毕),状态转换关系如下:

对于计算模块,我也使用了状态机代码,定义了八个状态,分别为IDLE(闲置),GET_NUM(获取运算数字),GET_OPERATOR(获取运算符),PLUS(进行加法运算),MINUS(进行减法运算),MULTIPLY(进行乘法运算),DIVIDE(进行除法运算),DONE(运算完毕),状态转移关系如下:

软件流程图和关键代码介绍

  • Verilog接线图:

  • 上位机软件流程图:

  • Verilog协议状态机关键代码介绍

根据以上所画的状态转移图,套用Verilog三段式状态机代码即可,给出三段式状态机基本代码:

always @(posedge clk or negedge rst) begin
    if(!rst) begin  //异步复位
        STATE_C <= IDLE;
    end
    else begin
        STATE_C <= STATE_N; //每个时钟上升沿到来时更新状态
    end
end

always @(*) begin
       case(STATE_C)
              STATE_1: begin
              …
              end
              STATE_2: begin
              …
              end
              …
              …
              …
       endcase
end

always @(posedge clk or negedge rst) begin
       if(!rst) begin
       …
       end
       else if(STATE_C == STATE_1) begin
       …
       end
       else if() begin
       …
       end
end
  • 而对于我所定义的协议状态机,具体状态转移方面的代码如下:
always @(*) begin
    case(STATE_C)
              IDLE: begin
                     if(rx_data_out == 8'b0010_0110) begin       //接收到ASICII码值'&',转换状态
                            STATE_N = STATE1;
                     end
                     else begin            //保持状态
                            STATE_N = IDLE;
                     end
              end
              STATE1: begin
                     if(rx_data_out == 8'b0100_0000) begin       //接收到ASICII码值'@',转换状态
                            STATE_N = NUM;
                     end
                     else begin            //保持状态
                            STATE_N = STATE1;
                     end
              end
              NUM: begin
                     if(rx_data_out == 8'b0010_0001) begin       //接收到ASICII码值'!',转换状态
                            STATE_N = STATE3;
                     end
                     else begin            //保持状态
                            STATE_N = NUM;
                     end
              end
              STATE3: begin
                     if(rx_data_out == 8'b0010_0011) begin       //接收到ASICII码值'#',转换状态
                            STATE_N = OPERATOR;
                     end
                     else if(rx_data_out == 8'b0010_0100) begin     //接收到ASICII码值'$',转换状态
                            STATE_N = DONE;
                     end
                     else begin            //保持状态
                            STATE_N = STATE3;
                     end
              end
              OPERATOR: begin
                     if(rx_data_out == 8'b0011_1111) begin       //接收到ASICII码值'?',转换状态
                            STATE_N = STATE5;
                     end
                     else begin            //保持状态
                            STATE_N = OPERATOR;
                     end
              end
              STATE5: begin
                     if(rx_data_out == 8'b0100_0000) begin       //接收到ASICII码值'@',转换状态
                            STATE_N = NUM;
                     end
                     else begin
                            STATE_N = STATE5;
                     end
              end
              DONE: begin
                     if(get_ready) begin
                            STATE_N = IDLE;
                     end
                     else begin
                            STATE_N = DONE;
                     end
              end
       endcase
end

其中的rx_data_out连接例程中的uart模块的输出接线,get_ready信号是完成一次协议转换后的标志位,可以将这个信号引出后作为calculator计算模块的开始标志。


  • 计算模块的状态机Verilog代码如下:
always @(*) begin
        case(STATE_C)
            IDLE: begin
                if(IDLE_get_ready) begin
                    STATE_N = GET_NUM;
                end
                else begin
                    STATE_N = IDLE;
                end
            end
            GET_NUM: begin
                if(get_num_done) begin
                    STATE_N = GET_OPERATOR;
                end
                else begin
                    STATE_N = GET_NUM;
                end
            end
            GET_OPERATOR: begin
                if(get_operator_plus) begin
                    STATE_N = PLUS;
                end
                else if(get_operator_minus) begin
                    STATE_N = MINUS;
                end
                else if(get_operator_multiply) begin
                    STATE_N = MULTIPLY;
                end
                else if(get_operator_divide) begin
                    STATE_N = DIVIDE;
                end
                else begin
                    STATE_N = GET_OPERATOR;
                end
            end
            PLUS: begin
                if(calculator_done) begin
                    STATE_N = DONE;
                end
                else begin
                    STATE_N = PLUS;
                end
            end
            MINUS: begin
                if(calculator_done) begin
                    STATE_N = DONE;
                end
                else begin
                    STATE_N = MINUS;
                end
            end
            MULTIPLY: begin
                if(calculator_done) begin
                    STATE_N = DONE;
                end
                else begin
                    STATE_N = MULTIPLY;
                end
            end
            DIVIDE: begin
                if(calculator_done) begin
                    STATE_N = DONE;
                end
                else begin
                    STATE_N = DIVIDE;
                end
            end
            DONE: begin
                if(rewrite_done) begin
                    STATE_N = IDLE;
                end
                else begin
                    STATE_N = DONE;
                end
            end
        endcase
    end

对于数据的存储,可以通过定义RAM来实现,可以使用diamond提供的IP核进行构建,也可以通过Verilog代码定义寄存器来实现,为了代码的可移植性,我使用了自己写Verilog代码定义数组寄存器来实现,自定义RAM存储器基本代码如下:

module num_ram (
    input wire clk,
    input wire rst_n,
    input wire rd_en, wr_en,
    input wire [7:0] addr_wr, addr_rd,
    input wire [27:0] wr_data,


    output reg [27:0] rd_data
);

    // Declare the RAM variable
    reg [27:0] ram [4:0];

    //写入
    always @(posedge clk or negedge rst_n)
        if(!rst_n) begin : init
            integer i;
            for (i = 0; i < 15 ;  i=i+1) begin
                ram[i] <= 28'd0;
            end
        end
        else if (wr_en)
            ram[addr_wr] <= wr_data;
        else
            ram[addr_wr] <= ram[addr_wr];

    //读取
    always @(posedge clk or negedge rst_n)
        if(!rst_n)
            rd_data <= 28'd0;
        else if(rd_en)
            rd_data <= ram[addr_rd];
        else
            rd_data <= 28'd0;


endmodule

通过改变寄存器数组内寄存器的个数和位数即可更改RAM的大小,当使能信号激活时,在clk信号的上升沿进行对应地址数据的存储与读取,rst复位信号可将RAM内的数据清零。


  • 上位机代码简介:

上位机代码较为简单,主要使用了serialspeech_recognition这两个模块,serial模块可以对串口进行通讯,speech_recognition模块可以调用网络模型或本地模型进行语音识别,这里我使用了微软的Azure AI Service进行语音识别。

  • 串口部分代码:
# 配置串口
        ser = serial.Serial(port = ports_list[port_choose - 1][0],
                          baudrate = 9600,
                          bytesize = serial.EIGHTBITS,
                          parity = serial.PARITY_NONE,
                          stopbits = serial.STOPBITS_ONE,
                          write_timeout = 5)

波特率选用9600,数据位为8位,无校验位,停止位为1位,与FPGA的配置一致即可。


  • 语音识别部分代码:
# 语音识别函数
def recognize_speech():
    """
    使用语音识别功能,识别用户说出的算式
    """
    r = sr.Recognizer()
    with sr.Microphone() as source:
        print("请说出您的计算式(例如:三十二加五十四再乘九十九):")
        print("正在聆听...")
        r.adjust_for_ambient_noise(source)
        audio = r.listen(source)
   
    try:
        print("正在识别...")
        temp = r.recognize_azure(audio, key= KEY , language='zh-CN', location="eastasia")
        text = temp[0]
        print(f"您说的是: {text}")
        return text
    except sr.UnknownValueError:
        print("无法识别语音")
        return None
    except sr.RequestError as e:
        print(f"无法从Microsoft Azure Ai Services服务获取结果; {e}")
        return None

只需调用库函数即可,并将函数所需参数传入,函数就可以将识别出的文本返回,再对返回文本进行处理即可,这里不再赘述。

功能展示图及说明

可以看到,这是进行连续四次运算的功能展示,在FPGA接收协议并进行计算前,RGB灯显示红色,在接收到正确协议后,RGB将变为绿色,并将算式、计算过程和结果显示在LCD如图所示位置。最多支持连续四次运算,操作数最高为两位,过程和结果数最高为五位。打开上位机程序,按照提示配置好串口后,说出算式,上位机将自行进行识别,并将其格式化为协议字符串形式进行发送,上位机支持算式纠错功能,当说出的算式不符合正确结构时,上位机将提示并再次进行识别。

难题和解决方法

主要是LCD驱动的问题,由于例程只给出了LCD显示图片的方法,需要自行制作模块,并对ascii码进行取模并存储,才可使LCD正确显示字符串,取模存储部分使用了寄存器模拟RAM 8*4096,对FPGA的资源占用也较大。

由于工程较为庞大,一边写代码并在实机测试十分耗时,需要编写仿真文件在modelsim进行仿真操作,但由于使用9600波特率串口,tb文件也模拟发送了串口数据,tb文件仿真一次的耗时十分长,需要大概3分钟才可仿真完成。

还有仿真与实机不一致问题,仿真波形完成,在实机上烧录的结果与仿真不一致,无法正常工作,需要一点点进行排错,使用8LED灯和2RGB灯将一些接线引出作为debug灯慢慢试错。

FPGA资源占用报告

波形仿真


心得体会

通过本次寒假练项目,我对FPGA的理解进一步加深,掌握了使用FPGA驱动SPI协议硬件的方法,也掌握了FPGA驱动uart串口的方法,进一步理解了FPGA中状态机的魅力,掌握了使用状态机自制接收协议的方法,使用Python编写上位机也让我对编程语言的逻辑有了更深一步的理解,并且掌握了语音模型的使用方法。

附件下载
VoiceControlCalculator.zip
Verilog和上位机源码
团队介绍
北京邮电大学数字逻辑
团队成员
Ling_da_jin
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号