一、项目需求
语音控制计算器 - 使用大模型
PC上语音控制生成命令
通过USB传输到FPGA扩展板
小脚丫FPGA逻辑实现计算器功能
计算过程和结果通过TFTLCD显示
使用板卡:STEP Baseboard4.0底板+STEP MXO2 LPC核心板
二、硬件思路
本项目实现了最多两位十进制进行一次加、减、乘、除四则计算器,从输入输出的角度设计,需要能够输入数字和运算符号的按键和能够显示输入数字和运算结果的显示器件。本项目软件部分使用python语言写了一个简单的上位机来实现串口通信、录音、语音识别的功能,其中语音功能接入了百度语音大模型(新用户可以领取免费时长)。硬件部分使用了小脚丫FPGA套件STEP BaseBoard V4.0上板载的串口转换芯片和320*240的LCD屏幕。串口通信模块对小脚丫开源平台上的320*240的LCD屏幕则通过符合ST7789数据手册的SPI通信方式,使用PC2LCD2002生成字模,最后进行显示相关算式。
三、方案框图和项目设计思路介绍
总体框图
软件端框图
上图是320*240的LCD模块电路,从上图可以看出,屏幕有六个引脚的输入,其中LED-引脚用于控制背光的亮度,RES引脚用于屏幕复位 ,DC引脚用于数据/命令的选择,SDA引脚用于SPI数据的输入,SCL引脚用于SPI时钟信号的输入,CS引脚用于SPI片选信号的输入。
四、关键代码介绍
串口数据处理
module uartboard (
input clk, // 系统时钟
input rst_n, // 复位信号
input [7:0] rx_data, // 串口接收输入
output reg [15:0] key // 键值输出
);
// 根据接收到的数据更新 key 输出
always @(*) begin
if (rst_n) begin
case(rx_data)
8'h2E: key = 16'b0010_0000_0000_0000; // '.'
8'h3D: key = 16'b0100_0000_0000_0000; // '='
8'h30: key = 16'b0001_0000_0000_0000; // '0'
8'h31: key = 16'b0000_0100_0000_0000; // '1'
8'h32: key = 16'b0000_0010_0000_0000; // '2'
8'h33: key = 16'b0000_0001_0000_0000; // '3'
8'h34: key = 16'b0000_0000_0001_0000; // '4'
8'h35: key = 16'b0000_0000_0010_0000; // '5'
8'h36: key = 16'b0000_0000_0100_0000; // '6'
8'h37: key = 16'b0000_0000_0000_0001; // '7'
8'h38: key = 16'b0000_0000_0000_0010; // '8'
8'h39: key = 16'b0000_0000_0000_0100; // '9'
8'h2B: key = 16'b1000_0000_0000_0000; // '+'
8'h2D: key = 16'b0000_1000_0000_0000; // '-'
8'h2A: key = 16'b0000_0000_1000_0000; // '*'
8'h2F: key = 16'b0000_0000_0000_1000; // '/'
default: key = 16'b0000_0000_0000_0000; // 默认无按键
endcase
end else begin
key = 16'b0000000000000000; // 无数据时,key清零
end
end
endmodule
状态机控制
always@(posedge clk,negedge rst_n)
begin
if(!rst_n)
begin
state<=idle;
dot<=0;
single<=2'b11;
end
else if(state==idle)
begin
if(|{key_e[2:0],key_e[6:4],key_e[10:8],key_e[12]})
state<=entered_num0_low;
single[0]<=1;
end
...
end
状态机的逻辑在always块中实现,根据当前状态和按键输入来更新状态和其他输出信号。以下是状态转移的简要说明:
空闲状态(idle):
当检测到数字(0-9)或小数点按键时,状态转移到entered_num0_low,表示开始输入第一个数字。
输入第一个数字(entered_num0_low):
继续输入数字时,状态转移到entered_num0_high,表示输入更大的数字。
如果输入的是符号(如加号、减号等),状态转移到entered_sign。
如果输入小数点,状态转移到entered_num0_dot。
输入小数点(entered_num0_dot):
继续输入数字时,状态转移到entered_num0_high。
输入符号(entered_sign):
输入下一个数字时,状态转移到entered_num1_low,开始输入第二个数字。
输入第二个数字(entered_num1_low):
继续输入数字时,状态转移到entered_num1_high。
如果输入等号,状态转移到calculate,表示需要进行计算。
如果输入小数点,状态转移到entered_num1_dot。
输入第二个数字的小数点(entered_num1_dot):
继续输入数字时,状态转移到entered_num1_high。
计算状态(calculate):
当计算完成后,如果再次输入数字,状态回到entered_num0_low,开始新的输入。
计算模块代码
module alu
(
clk,cal,num,sign,single,dot,ans,dot_ans
);
input [15:0]num;
input [1:0]sign,single,dot;
input clk; // 时钟信号
input cal; // 计算使能信号
output reg [15:0]ans; // 计算结果
output reg[3:0]dot_ans; // 计算结果的小数点位置
//内部寄存器和信号
reg [9:0]num0_bin; // 用于存储第一个数(转换成二进制)
reg [9:0]num1_bin; // 用于存储第二个数(转换成二进制)
reg [13:0]ans_bin; // 存储计算后的二进制结果
wire [19:0]quotient; // 除法结果(未转换为BCD)
wire [23:0]quotient_bcd;// 除法结果转换后的BCD
wire [15:0]ans_bcd; // 计算结果转换后的BCD
wire cal_en,div_en; //计算使能信号和除法使能信号
localparam add=0,
sub=1,
multiply=2,
div=3;
// div_en 仅在 sign 为 div(即除法)且 cal_en 有效时为高电平,表示可以进行除法运算。
assign div_en=cal_en&(sign==div);
edge_detect div_start(.signal(cal),.clk(clk),.signal_o(cal_en));// 边沿检测cal信号的上升沿,生成cal_en
bin_bcd trans(.bitcode(ans_bin[13:0]),.bcdcode(ans_bcd)); //二进制转BCD
bin_bcd_div trans_quotient(.bitcode(quotient),.bcdcode(quotient_bcd));
divider alu_div(.clk(clk),.rst(div_en),.dividend(num0_bin),.divisor(num1_bin),.quotient(quotient)); //除法模块
always @(*)
begin
case(sign)
add:
begin
case({dot,single})
4'b0000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0010:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10;
end
4'b0011:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10;
end
4'b0100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0110:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10;
end
4'b1000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10+num[15:12];
end
default:
begin
num0_bin=10'bxx_xxxx_xxxx;
num1_bin=10'bxx_xxxx_xxxx;
end
endcase
ans_bin=num1_bin+num0_bin;
if(ans_bcd[15:12]==0)
ans[15:12]=4'b1111;
else ans[15:12]=ans_bcd[15:12];
if(ans_bcd[11:8]==0&&ans_bcd[15:12]==0)
ans[11:8]=4'b1111;
else ans[11:8]=ans_bcd[11:8];
ans[7:4]=ans_bcd[7:4];
ans[3:0]=ans_bcd[3:0];
dot_ans=4'b0010;
end
sub:begin
case({dot,single})
4'b0000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0010:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10;
end
4'b0011:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10;
end
4'b0100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0110:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10;
end
4'b1000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10+num[15:12];
end
default:
begin
num0_bin=10'bxx_xxxx_xxxx;
num1_bin=10'bxx_xxxx_xxxx;
end
endcase
if(num0_bin<num1_bin)
begin
ans_bin=num1_bin-num0_bin;
if(ans_bcd[11:8]==0)
begin
ans[11:8]=4'b1101;
ans[15:12]=4'b1111;
end
else begin
ans[11:8]=ans_bcd[11:8];
ans[15:12]=4'b1101;
end
end
else begin
ans_bin=num0_bin-num1_bin;
ans[15:12]=4'b1111;
if(ans_bcd[11:8]==0)
ans[11:8]=4'b1111;
else ans[11:8]=ans_bcd[11:8];
end
ans[7:4]=ans_bcd[7:4];
ans[3:0]=ans_bcd[3:0];
dot_ans=4'b0010;
end
multiply:
begin
case(single)
2'b00:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10+num[15:12];
end
2'b01:
begin
num0_bin=num[3:0];
num1_bin=num[11:8]*10+num[15:12];
end
2'b10:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8];
end
2'b11:
begin
num0_bin=num[3:0];
num1_bin=num[11:8];
end
default:
begin
num0_bin=10'bxx_xxxx_xxxx;
num1_bin=10'bxx_xxxx_xxxx;
end
endcase
ans_bin=num0_bin*num1_bin;
case(dot)
2'b00:
begin
if(ans_bcd[15:12]==0)
ans[15:12]=4'b1111;
else ans[15:12]=ans_bcd[15:12];
if(ans_bcd[11:8]==0&&ans_bcd[15:12]==0)
ans[11:8]=4'b1111;
else ans[11:8]=ans_bcd[11:8];
if(ans_bcd[7:4]==0&&ans_bcd[11:8]==0&&ans_bcd[15:12]==0)
ans[7:4]=4'b1111;
else ans[7:4]=ans_bcd[7:4];
ans[3:0]=ans_bcd[3:0];
dot_ans=4'b0000;
end
2'b10,2'b01:
begin
if(ans_bcd[15:12]==0)
ans[15:12]=4'b1111;
else ans[15:12]=ans_bcd[15:12];
if(ans_bcd[11:8]==0&&ans_bcd[15:12]==0)
ans[11:8]=4'b1111;
else ans[11:8]=ans_bcd[11:8];
ans[7:4]=ans_bcd[7:4];
ans[3:0]=ans_bcd[3:0];
dot_ans=4'b0010;
end
2'b11:
begin
if(ans_bcd[15:12]==0)
ans[15:12]=4'b1111;
else ans[15:12]=ans_bcd[15:12];
ans[11:8]=ans_bcd[11:8];
ans[7:4]=ans_bcd[7:4];
ans[3:0]=ans_bcd[3:0];
dot_ans=4'b0100;
end
endcase
end
div:
begin
case({dot,single})
4'b0000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0010:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10;
end
4'b0011:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10;
end
4'b0100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*100+num[15:12]*10;
end
4'b0110:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10;
end
4'b1000:
begin
num0_bin=num[3:0]*100+num[7:4]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1001:
begin
num0_bin=num[3:0]*10;
num1_bin=num[11:8]*10+num[15:12];
end
4'b1100:
begin
num0_bin=num[3:0]*10+num[7:4];
num1_bin=num[11:8]*10+num[15:12];
end
default:
begin
num0_bin=10'bxx_xxxx_xxxx;
num1_bin=10'bxx_xxxx_xxxx;
end
endcase
if(num1_bin==10'b1)//此段代码对输入0.1特殊处理,配合除法器中代码进行特殊处理显示
begin
if(quotient_bcd[19:16]!=4'd0)
begin
dot_ans=4'b0010;
ans={quotient_bcd[19:4]};
end
else if(quotient_bcd[15:11]!=4'd0)
begin
dot_ans=4'b0100;
ans={quotient_bcd[15:4],4'b0000};
end
end
else
if(quotient_bcd[23:20]!=4'd0)
begin
dot_ans=4'b0010;
ans={quotient_bcd[23:8]};
end
else if(quotient_bcd[19:16]!=4'd0)
begin
dot_ans=4'b0100;
ans=quotient_bcd[19:4];
end
else begin
dot_ans=4'b0100;
ans={4'b1111,quotient_bcd[15:4]};
end
end
endcase
end
endmodule
软件python代码:
import pyaudio
import wave
import requests
import serial
import re
from aip import AipSpeech
# 用户ID输入,已进行隐私处理了,见谅!
APP_ID = '11XXX018'
API_KEY = 'kS1DuudEXXXXeJGqV'
SECRET_KEY = 'AIACXqWG7XvXXXX0tASRiiYT'
# 串口设置
ser = serial.Serial('COM11', 9600, timeout=2, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS)
# 初始化客户端
client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
# 两位数字,一次加减乘除运算
# 调用百度语音API进行语音转文字
def speech_to_text(audio_path):
# 读取音频文件(支持pcm/wav/amr格式)
with open(audio_path, 'rb') as f:
audio_data = f.read()
# 调用语音识别接口
result = client.asr(audio_data, 'wav', 16000, {
'dev_pid': 1537 # 普通话(支持英文数字)
})
if 'result' in result:
return result['result'][0]
else:
raise Exception("识别失败:" + str(result))
# 录音函数
def record_audio(filename):
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=1024)
print("Recording...")
frames = []
for _ in range(0, int(16000 / 1024 * 5)): # 录音5秒
data = stream.read(1024)
frames.append(data)
print("Recording finished.")
stream.stop_stream()
stream.close()
p.terminate()
# 保存为WAV文件
wf = wave.open(filename, 'wb')
wf.setnchannels(1)
wf.setsampwidth(p.get_sample_size(pyaudio.paInt16))
wf.setframerate(16000)
wf.writeframes(b''.join(frames))
wf.close()
# 处理文本,去掉句号并替换运算符
def process_expression(text):
text = text.strip()
text = text.rstrip('。') # 去掉句号
text = text.replace('加', '+').replace('减', '-').replace('×', '*').replace('÷', '/').replace('等于', '=').replace('三', '3')
return text
# # 发送表达式到FPGA并接收回传数据
# def send_and_receive(expression):
# ser.write(expression.encode('utf-8'))
# received = ser.readline().decode('utf-8').strip()
# print("FPGA回传的数据:", received)
# 逐个字符发送到FPGA
def send_and_receive(expression):
for char in expression:
ser.write(char.encode('utf-8'))
# received = ser.readline().decode('utf-8').strip() # 去掉换行
# print("FPGA回传的数据:"+received)
received = ser.readline() # 读取一行数据
received_hex = received.hex() # 转换为十六进制字符串
print("接收到的数据(十六进制):"+received_hex)
# 主程序
if __name__ == "__main__":
filename = "recording.wav"
record_audio(filename)
recognized_text = speech_to_text(filename)
expression = process_expression(recognized_text)
print(f"识别结果:{expression}") # 输出:1+3
send_and_receive(expression)
五、功能展示图(以34除5为例)
硬件部分
软件上位机部分
六、资源使用情况及仿真图
资源占用报告
网表分析图
计算模块仿真波形图
LCD驱动模块仿真波形图
七、难题和解决办法
在进行LCD屏幕驱动的过程中,首先遇到了资料获取的难题。面对众多的开源资料、商家资料以及数据手册,如何快速准确地筛选出适合当前项目的资料成为了一大挑战。为了解决这一问题,我们采取了多管齐下的策略。一方面,利用专业的技术论坛和社区平台,如小脚丫开源平台、GitHub、csdn等,搜索相关的开源项目和代码示例,从中提取有用的代码片段和配置参数。另一方面,直接联系LCD屏幕的供应商,获取最新最准确的数据手册和商家提供的技术支持资料,确保驱动程序的开发符合硬件的实际要求。
在LCD字模生成方面,遇到了如何将设计好的字模转换为易于FPGA识别的格式的问题。为了解决这个问题,借助了PC2LCD2002和image2LCD这两款专业软件。通过使用PC2LCD2002,可以将设计好的字模图像快速转换为LCD屏幕能够识别的点阵格式。而image2LCD软件则提供了更多的自定义选项,能够满足不同分辨率和颜色深度的需求。在格式转换过程中,还利用文本编辑器对生成的字模数据进行进一步的整理和优化,确保其格式完全符合FPGA的编程要求。
至于如何接入大模型以及选取合适的模型这一难题,我们通过在自媒体网站上搜索相关资料来解决。在自媒体平台上,有许多技术博主分享了关于大模型接入和选择的经验和案例。通过阅读这些文章和观看视频教程,我们了解到不同大模型的特点和适用场景。根据项目的具体需求,如计算资源、模型精度、响应速度等,综合考虑后选取了最适合的模型,并按照相应的接入文档和API进行集成和调试。
八、心得体会
读数据手册时序图的重要性;
SPI等基础时序很重要,不同的spi设备在撰写驱动时也有区别;
巩固了状态机的知识;
小脚丫有很多有用的知识!!(各种协议和参考资料的示例);
对知识点越了解才能越好的运用gpt,不然都不知道如何描述任务和需求;