上位机可执行程序在这里
通过网盘分享的文件:2025寒假练上位机
链接: https://pan.baidu.com/s/1l7LfSm2GieBIFRN-PXV0yQ 提取码: 2hqn
项目介绍
- 本项目为硬禾科技2025年寒假练任务三:语音控制计算机-使用大模型,使用小脚丫FPGA STEP BaseBoard V4.0套件和Python,通过上位机识别语音,将计算命令通过串口通讯发送给FPGA,FPGA对命令解析后将算式进行计算,并在LCD屏幕上显示算式、计算过程和计算结果。
硬件介绍
- 本项目基于小脚丫套件,STEP BaseBoard V4.0是第4代小脚丫FPGA扩展底板,可以用于全系列小脚丫核心板的功能扩展,采用100mm*161.8mm的黄金比例尺寸,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、8个7位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,配合小脚丫FPGA板能够完成多种实验,是数字逻辑、微机原理、可编程逻辑语言以及EDA设计工具等课程完美的实验平台。
方案框图和项目设计思路
为了实现本次任务,我将其分解为了以下几个任务:
- 上位机发送串口数据
- Uart串口接收
- 运算式识别方法
- 运算式存储方法
- 运算式计算方法
- LCD屏幕驱动显示任意字符
对于第1点,可以使用Python来写一个简易的上位机程序,通过调用语音识别库来将语音识别成运算式,再通过串口库发送出去。
对于第2点,由于本次任务提供的例程有现成的uart接收模块,所以仅需将其稍作移植,将uart_rx_data接线暴露出来即可。
对于第3、4、5点,可以定义一个串口传输协议,在接收到指定字符后进行相应操作,例如以”&”表示算式开始,以”$”表示算式结束,以”@”表示数字开始,”!”表示数字结束……
而为了在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内的数据清零。
- 上位机代码简介:
上位机代码较为简单,主要使用了serial和speech_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分钟才可仿真完成。
还有仿真与实机不一致问题,仿真波形完成,在实机上烧录的结果与仿真不一致,无法正常工作,需要一点点进行排错,使用8个LED灯和2个RGB灯将一些接线引出作为debug灯慢慢试错。
FPGA资源占用报告
波形仿真
心得体会
通过本次寒假练项目,我对FPGA的理解进一步加深,掌握了使用FPGA驱动SPI协议硬件的方法,也掌握了FPGA驱动uart串口的方法,进一步理解了FPGA中状态机的魅力,掌握了使用状态机自制接收协议的方法,使用Python编写上位机也让我对编程语言的逻辑有了更深一步的理解,并且掌握了语音模型的使用方法。