项目介绍
本次2025寒假在家一起练,选择的板卡为:小脚丫FPGA STEP BaseBoard V4.0套件。由 STEP Baseboard4.0底板 和 STEP MXO2 LPC核心板 组成!
选择的任务为:语音控制计算器 - 使用大模型。具有以下功能:
- PC上语音控制生成命令
- 通过USB传输到FPGA扩展板
- 小脚丫FPGA逻辑实现计算器功能
- 计算过程和结果通过TFTLCD显示
本次项目所使用到的技能树:
- verilog语言的使用
- python脚本的编写
- 对接大模型
硬件介绍
STEP BaseBoard V4.0是第4代小脚丫FPGA扩展底板,可以用于全系列小脚丫核心板的功能扩展,采用100mm*161.8mm的黄金比例尺寸,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、8个7位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,配合小脚丫FPGA板能够完成多种实验,是数字逻辑、微机原理、可编程逻辑语言以及EDA设计工具等课程完美的实验平台。
更多具体的介绍参见:STEP BaseBoard V4.0
项目设计介绍
将整个项目拆分出来,需要完成以下几个点:
- 语音文字识别
- 电脑数据传输到FPGA中
- FPGA的数学运算
- 显示到LCD屏幕上
语音文字识别
语音技术中,开通了以下的服务:
流程:
- 获取token
- 将音频文件进行转换,然后发送出去
- 解析JSON格式的字符串,得到需要的字段
FPGA
以下框图进行简化,展示了重要的几个部分
- Uart_Rx:串口的接收
- Uart_Tx:串口的发送
- digit_extractor:四则运算
- pic_ram:预置数字0-9
- lcd_show_pic:刷新显示数据到LCD上
代码流程图介绍
PC端
python脚本的流程图如下:
主要的代码
if __name__ == '__main__':
print("正在扫描可用的串行端口...")
available_ports = list_serial_ports()
if not available_ports:
print("未发现任何可用的串行端口。")
else:
select_port(available_ports)
print("按空格键开始/停止录音(Ctrl+C退出程序)")
try:
while True:
if RECORD:
record_audio()
except KeyboardInterrupt:
if RECORD:
stop_recording()
p.terminate()
注意点:
1 - 运行之前需要安装几个python包,若提示:ModuleNotFoundError: No module named 'xxx',就需要pip instal xxx。
2 - 需要使用到API相关的KEY,在上面提到的链接中注册,注册完成后将自己的值填入到下面,若不想注册可以直接使用串口发送数据来测试功能
FPGA端
以下是顶层文件,初始化了所使用的模块。
项目中串口发送和图片显示都是使用了官方例程,在此基础上进行了修改
判断Uart_Rx_uut模块接收到的数据,在digit_extractor_uut模块中进行计算,随后在LCD上面显示出来
module picture_display
(
input clk ,
input rst_n ,
output lcd_rst ,
output lcd_blk ,
output lcd_dc ,
output lcd_sclk ,
output lcd_mosi ,
output lcd_cs ,
input rs232_rx, //FPGA中UART接收端,分配给UART模块中的发送端TXD
output rs232_tx, //FPGA中UART发送端,分配给UART模块中的接收端RXD
output Led,
input key_send
);
wire [8:0] data;
wire en_write;
wire wr_done;
wire [8:0] init_data;
wire en_write_init;
wire init_done;
wire en_size ;
wire show_pic_flag ;
wire [6:0] ascii_num ;
wire [8:0] start_x ;
wire [8:0] start_y ;
wire [8:0] show_pic_data ;
wire en_write_show_pic ;
wire show_char_done ;
wire [8:0] rom_addr;
wire [239:0] rom_q;
wire clk_50MHz;
wire [3:0] ones;
wire [3:0] tens;
wire [7:0]result;
wire bps_en_rx,bps_clk_rx;
wire [7:0] rx_data;
wire bps_en_tx,bps_clk_tx;
assign lcd_blk = 1'b1;
pll pll_u1(
.CLKI(clk ),
.CLKOP(clk_50MHz )
);
lcd_write lcd_write_inst
(
.sys_clk_50MHz(clk_50MHz ),
.sys_rst_n (rst_n ),
.data (data ),
.en_write (en_write ),
.wr_done (wr_done ),
.cs (lcd_cs ),
.dc (lcd_dc ),
.sclk (lcd_sclk ),
.mosi (lcd_mosi )
);
control control_inst
(
.sys_clk_50MHz (clk_50MHz ),
.sys_rst_n (rst_n ),
.init_data (init_data ),
.en_write_init (en_write_init ),
.init_done (init_done ),
.show_pic_data (show_pic_data ),
.en_write_show_pic (en_write_show_pic ),
.show_pic_flag (show_pic_flag ),
.data (data ),
.en_write (en_write )
);
lcd_init lcd_init_inst
(
.sys_clk_50MHz(clk_50MHz ),
.sys_rst_n (rst_n ),
.wr_done (wr_done ),
.lcd_rst (lcd_rst ),
.init_data (init_data ),
.en_write (en_write_init),
.init_done (init_done )
);
lcd_show_pic lcd_show_pic_inst
(
.sys_clk (clk_50MHz ),
.sys_rst_n (rst_n ),
.wr_done (wr_done ),
.show_pic_flag (show_pic_flag),
.rom_addr (rom_addr),
.rom_q (rom_q),
.show_pic_data (show_pic_data ),
.en_write_show_pic (en_write_show_pic )
);
pic_ram pic_ram_u0
(
.address(rom_addr),
.q(rom_q),
.ones(ones),
.tens(tens)
);
digit_extractor digit_extractor_uut
(
.result(result),
.ones(ones),
.tens(tens)
);
Baud #
(
.BPS_PARA (1250 )
)
Baud_rx
(
.clk_in (clk ), //系统时钟
.rst_n_in (rst_n ), //系统复位,低有效
.bps_en (bps_en_rx ), //接收时钟使能
.bps_clk (bps_clk_rx ) //接收时钟输出
);
Uart_Rx Uart_Rx_uut
(
.clk_in (clk ), //系统时钟
.rst_n_in (rst_n ), //系统复位,低有效
.bps_en (bps_en_rx ), //接收时钟使能
.bps_clk (bps_clk_rx ), //接收时钟输入
.rs232_rx (rs232_rx ), //UART接收输入
.rx_data (rx_data ), //接收到的数据
.result(result),
.Led(Led)
);
Baud #
(
.BPS_PARA (1250 )
)
Baud_tx
(
.clk_in (clk ), //系统时钟
.rst_n_in (rst_n ), //系统复位,低有效
.bps_en (bps_en_tx ), //发送时钟使能
.bps_clk (bps_clk_tx ) //发送时钟输出
);
Uart_Tx Uart_Tx_uut
(
.clk_in (clk ), //系统时钟
.rst_n_in (rst_n ), //系统复位,低有效
.bps_en (bps_en_tx ), //发送时钟使能
.bps_clk (bps_clk_tx ), //发送时钟输入
.rx_bps_en (key_send ), //因需要自收自发,使用接收时钟使能判定:接收到新的数据,需要发送
.tx_data (result ), //需要发出的数据
.rs232_tx (rs232_tx ) //UART发送输出
);
endmodule
资源使用:
难点总结
1 - 在进行分离数字的个位和十位的时候发送结果不太一样,代码如下
// 提取个位
ones = result % 10;
// 提取十位
tens = result / 10;
个位是可以正常分离出来的,但是十位不知道为啥有点问题,下面是测试出来的结果
9*9=81
8*9=42
7*9=03
6*9=04
5*9=25
4*9=46
3*9=67
2*9=68
1*9=89
解决办法:使用查表的方式来进行判断,代码如下
// 使用查表
ones = ones_table[result]; // 查找个位
tens = tens_table[result]; // 查找十位
// 有问题
//ones = result % 10;
//tens = result / 10;
2 - 语音识别不准确
解决办法:将录制和上传音频的格式中通道改成1
3 - 不熟悉FPGA的开发,虽然都是使用官方的例程,但是在写代码的时候经常会碰到一个语法错误,或者是一些其他错误
3_1 - 处理接收串口的数据的时候,碰到了一些问题,解析的数据不太对
3_2 - 显示数字的时候,不知道如何刷新屏幕
解决方法:不断的重试或者问AI
4 - 因为不懂FPGA里面的资源分布,竟然在逻辑综合的时候爆内存了!
解决方法:少用 / 或者 <<,合理使用资源。最后是删除掉了很多的内容。。
项目总结
本次活动中,在FPGA中编写的代码不是很符合人们的使用,并且在显示数字的所占用的空间是非常巨大的,定义了好几个非常大的数组。还需要解决的几个问题:1 - 当PC端发送命令的时候,需要按按键才会将数据重新返回到PC端。 2- 计算一次后,需要点击按键才能接收下次的数据。
似乎文件数据多起来,或者一个文件特别上的时候,逻辑综合和FPGA映射就会非常的长,有时候都达到了几分钟。可能是因为不熟悉FPGA中的相关语法,导致有些特别占用资源。