一、项目介绍
1. 项目需求
通过 DAC 输出声波 BFSK 信号(例如 2 kHz 与 3 kHz 两个频点),外接扬声器播放;设计前导码、长度字段、数据区与 CRC 校验。
麦克风接收声波信号,完成带通滤波、频点能量检测、位同步与帧同步,解码出数据。
OLED 显示接收的字符或数据包内容与实时误包率;七段数码管显示当前码速。
实现噪声自适应门限或信噪比估计,提高在环境噪声下的稳定性。
2. 需求分析
可分为如下几个要点:
生成BFSK信号,设计数据格式和校验
adc模块接收
滤波,求频点能量,解码,校验
oled、数码管显示
自适应门限
二、功能实现
1. 硬件介绍和分析
Altera MAX10系列10M02SCM芯片的核心板:
只有2304个LE资源,故滤波器阶数不能过高,同时RAM不能初始化(无ROM),字库会占据很大一部分资源
10位/50Msps高速ADC:
只用提供时钟,并口读数据即可
OLED128*64:
写一个驱动+字库即可
麦克风放大模块:
实际上是麦克风放大加直流偏置,用跳线帽将MIC和Ain连接就可以使用了
电脑扬声器生成BFSK信号(DAC转扬声器也可以 但我的功放坏了)
同时注意到拓展板上没有留空余GPIO口和供电口,所以必须拆下DAC模块给麦克风模块供电
2. 实现方式
BFSK信号设计:3khz为逻辑1,2khz为逻辑0
数据格式及接收方式:仿照UART的工作方式,空闲时持续1,拉低0以作为起始位,后续接8位数据区(ASCII码),2位校验区(CRC-2),4位中止位(发现连续发送时1、2个中止位容易丢包),实际上就是写一个单工UART
滤波:有FIR和IIR两种方案,但matlab分析FIR较好效果好时对应阶数,LE资源占用过多,故使用二阶IIR滤波
频点能量测量:使用滑动窗口测量,窗口大小在后续平衡LE资源和准确率下选择48点
自适应门限:由于空闲时持续1,故不方便写噪声门限(否则1会被视作噪声),故选择仿照施密特触发器,测量1/0能量差绝对值的平均值,保证1/0本身能量大小变化及交界时能自适应调整且避免连续跳变
误包率计算:滑动窗记录最近20包正误并计算
OLED显示:由于LE资源不足,故字库只保留了数字,大小写字母和部分符号,第一行显示误包率,第二行滚动显示内容
码速显示:考虑到中途波特率不可调,故在编译时作为参数可调并静态显示,节省资源
3. 系统设计

4. 实现过程
使用的软件是Quartus编译,VSCode编写,Vivado纠错,ModelSim仿真,其中使用AI部分主要使用ChatGPT3.1
软件流程图:

BFSK实现:
原本有一段用dac实现的代码,但是扬声器模块用不了了,故用Gemini写了一个C语言程序用电脑扬声器播放
具体可以看附件中的代码,有点长不复制了
Adc实现:
主要只用看时钟设计即可触发之后等待转化再读就行了
module adc_clk (
input clk_12m,
input rst_n,
output reg adc_clk,
output reg sample_en,
output reg sample_ok
);
reg [7:0] cnt;
always @(posedge clk_12m or negedge rst_n) begin
if(!rst_n) begin
cnt <= 8'd0;
adc_clk <= 1'b0;
sample_en <= 1'b0;
sample_ok <= 1'b0;
end else begin
if(cnt >= 8'd124)
cnt <= 8'd0;
else
cnt <= cnt + 1'b1;
if(cnt == 8'd61)
adc_clk <= 1'b1;
else
adc_clk <= 1'b0;
if(cnt == 8'd71)
sample_en <= 1'b1;
else
sample_en <= 1'b0;
if(cnt == 8'd72)
sample_ok <= 1'b1;
else
sample_ok <= 1'b0;
end
end
endmodule
IIR实现:
对着递推式写就行了,系数可以让ai生成matlab脚本来算(最好不用ai提供的系数),可以最后用ai优化一下语法(强转有符号数之类的)
module IIR #(
parameter signed [17:0] B0 = 18'sd1907,
parameter signed [17:0] B1 = 18'sd0,
parameter signed [17:0] B2 = -18'sd1907,
parameter signed [17:0] A1 = -18'sd60420,
parameter signed [17:0] A2 = 18'sd28954, //系数
parameter integer OUT_SHIFT = 15
)(
input clk_12m,
input adc_sample_ok,
input rst_n,
input [9:0] adc_data,
output signed [9:0] filted_data
);
reg signed [10:0] x1, x2;
reg signed [9:0] y1, y2;
// 单极性10位ADC(0~1023) 先居中到 [-512,511]
wire signed [10:0] x0 = $signed({1'b0, adc_data}) - 11'sd512;
wire signed [28:0] term_b0 = $signed(B0) * $signed(x0);
wire signed [28:0] term_b1 = $signed(B1) * $signed(x1);
wire signed [28:0] term_b2 = $signed(B2) * $signed(x2);
wire signed [27:0] term_a1 = $signed(A1) * $signed(y1);
wire signed [27:0] term_a2 = $signed(A2) * $signed(y2);
wire signed [35:0] y_sum = $signed(term_b0) + $signed(term_b1) + $signed(term_b2)
- $signed(term_a1) - $signed(term_a2);
wire signed [35:0] y_scaled = y_sum >>> OUT_SHIFT;
wire signed [9:0] y_next = (!rst_n) ? 10'sd0 :
(y_scaled > 36'sd511) ? 10'sd511 :
(y_scaled < -36'sd512) ? -10'sd512 :
y_scaled[9:0];
always @(posedge clk_12m or negedge rst_n) begin
if (!rst_n) begin
x1 <= 11'sd0;
x2 <= 11'sd0;
y1 <= 10'sd0;
y2 <= 10'sd0;
end else if(adc_sample_ok) begin
x2 <= x1;
x1 <= x0;
y2 <= y1;
y1 <= y_next;
end
end
assign filted_data = y_next;
endmodule
自适应门限:
先判断总能量够不够大 再求差值绝对值平均值的3/4作为门限进行判决,类似施密特触发器,设计了一些参数方便调
中间一大段是求绝对值和因为资源不够用做的截断位数(仿真发现数挺大的,可以截)
测试时将信号用数码管小数点显示,很好用,故保留
module compare #(
parameter integer E_SHIFT = 10,
parameter integer TH_FLOOR = 12,
parameter integer ENERGY_MIN = 700
)(
input clk_12m,
input adc_sample_ok,
input rst_n,
input [29:0] energy3,
input [29:0] energy2, // 30位无符号能量输入
output reg sig_out // 1=3kHz, 0=2kHz
);
reg [16:0] diff_avg;
localparam [16:0] TH_FLOOR_U = TH_FLOOR[16:0];
localparam [16:0] ENERGY_MIN_U = ENERGY_MIN[16:0];
wire [15:0] e3_u = energy3[E_SHIFT+15:E_SHIFT];
wire [15:0] e2_u = energy2[E_SHIFT+15:E_SHIFT];
wire [16:0] e_sum = {1'b0, e3_u} + {1'b0, e2_u};
wire signed [16:0] diff_now = $signed({1'b0, e3_u}) - $signed({1'b0, e2_u});
wire [16:0] abs_diff_now = diff_now[16] ? (~diff_now + 17'd1) : diff_now;
wire [16:0] th_3_4 = (diff_avg >> 2) + (diff_avg>>1);
wire [16:0] th_eff = (th_3_4 < TH_FLOOR_U) ? TH_FLOOR_U : th_3_4;
wire signed [17:0] th_pos_s = $signed({1'b0, th_eff});
wire signed [17:0] th_neg_s = -$signed({1'b0, th_eff});
wire signed [17:0] avg_err = $signed({1'b0, abs_diff_now}) - $signed({1'b0, diff_avg});
wire signed [17:0] avg_next_s = $signed({1'b0, diff_avg}) + (avg_err >>> 4);
wire [16:0] avg_next = avg_next_s[16:0];
always @(posedge clk_12m or negedge rst_n) begin
if (!rst_n) begin
diff_avg <= 17'd200;
sig_out <= 1'b1;
end else if(adc_sample_ok) begin
diff_avg <= avg_next;
if(e_sum < ENERGY_MIN_U) begin
sig_out <= 1'b1; // 默认输出3kHz,避免误判
end
else
begin
if(sig_out==1'b0 && $signed({diff_now[16], diff_now}) > th_pos_s) begin
sig_out <= 1'b1;
end
else if(sig_out==1'b1 && $signed({diff_now[16], diff_now}) < th_neg_s) begin
sig_out <= 1'b0;
end
end
end
end
endmodule
UART实现:
分别实现波特率时钟,串口接收,校验模块,数据按顺序一个一个收就好了,通过边沿检测完成位同步,通过起始位完成帧同步(也可以设计个前导码?)
// 波特率生成器
module baud_rate_gen #(
parameter BAUD_RATE = 300,
parameter CLK_FREQ = 12000000
)(
input clk,
input rst_n,
input enable,
output reg bp_tick
);
localparam CNT_MAX = CLK_FREQ / BAUD_RATE;
localparam CNT_HALF = CNT_MAX / 2;
localparam BIT_WIDTH = $clog2(CNT_MAX); // 自动计算所需位宽
reg [BIT_WIDTH-1:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 0;
bp_tick <= 1'b0;
end else if(enable) begin
if(cnt >= CNT_MAX - 1) begin
cnt <= 0;
end else begin
cnt <= cnt + 1'b1;
end
if(cnt == CNT_HALF)
bp_tick <= 1'b1;
else
bp_tick <= 1'b0;
end else begin
cnt <= 0;
bp_tick <= 1'b0;
end
end
endmodule
//uart接收 1起始位 + 8数据位 + 2校验位 (CRC-2) + 4中止位
module uart_rx(
input clk_12m,
input rst_n,
input bp_clk, // 波特率脉冲 (采样点)
input uart_rx,
output reg [9:0] rx_data,//9-2数据,1-0校验
output reg rx_en,
output reg rx_done
);
// 状态定义
localparam S_IDLE = 0;
localparam S_START = 1; // 校验起始位
localparam S_DATA = 2; // 接收数据
reg [1:0] state;
reg [2:0] rx_data_buffer;
reg [3:0] bit_cnt;
wire neg_rx_start;
// 边沿检测
always @(posedge clk_12m or negedge rst_n) begin
if(!rst_n)
rx_data_buffer <= 3'b111;
else begin
rx_data_buffer <= {rx_data_buffer[1:0], uart_rx};
end
end
// 只有在空闲状态才检测下降沿
assign neg_rx_start = (rx_data_buffer[2] & ~rx_data_buffer[1]) && (state == S_IDLE);
always @(posedge clk_12m or negedge rst_n) begin
if(!rst_n) begin
rx_en <= 1'b0;
state <= S_IDLE;
bit_cnt <= 0;
rx_data <= 0;
end else begin
case(state)
S_IDLE: begin
rx_done <= 1'b0; // 复位接收完成标志
if(neg_rx_start) begin
rx_en <= 1'b1; // 启动波特率时钟
state <= S_START;
end
end
S_START: begin
if(bp_clk) begin // 到达起始位中间点
if(uart_rx == 1'b0) begin // 确认仍为低电平+将检测位置置于数据位中间
state <= S_DATA;
bit_cnt <= 0;
end else begin
// 毛刺干扰,复位
rx_en <= 1'b0;
state <= S_IDLE;
end
end
end
S_DATA: begin
if(bp_clk) begin
rx_data <= {rx_data[8:0], uart_rx};
if(bit_cnt == 4'd9) begin // 0-9 共10位 (8 Data + 2 CRC)
state <= S_IDLE; // 直接结束
rx_en <= 1'b0; // 关闭波特率时钟
rx_done <= 1'b1; // 接收完成标志
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule
//数据检验 rx_ok上升沿说明数据有效 send_data为有效数据 且计算最近20包数据的错误率err_rate
//仿真时发现复位到接收信号1会导致宽带激励 出现一小段信号0 导致误包率100(其实时丢时不丢的?) 故丢掉复位后第一个包(后发现没有产生问题 改回去了)
module data_judge(
input [9:0] rx_data,
input clk_12m,
input rst_n,
input rx_done,
output reg rx_ok,
output reg [7:0] send_data,
output reg [7:0] err_rate
);
reg [19:0] packet_status; // 1: 对, 0: 错
reg [4:0] error_count;
reg [4:0] packet_count;
reg [1:0] rx_done_buffer;
// reg first_packet_seen;
wire neg_rx_done;
always @(posedge clk_12m or negedge rst_n) begin
if(!rst_n)
rx_done_buffer <= 2'b0;
else begin
rx_done_buffer <= rx_done_buffer << 1;
rx_done_buffer[0] <= rx_done;
end
end
assign neg_rx_done = rx_done_buffer[1] & ~rx_done_buffer[0];
function [1:0] compute_crc2;
input [7:0] data;
reg [2:0] crc_reg;
integer i;
begin
crc_reg = 3'b000;
for(i = 7; i >= 0; i = i - 1) begin
crc_reg = {crc_reg[1:0], data[i]};
if(crc_reg[2]) crc_reg = crc_reg ^ 3'b101;
end
compute_crc2 = crc_reg[1:0];
end
endfunction
always @(posedge clk_12m or negedge rst_n) begin
if(!rst_n) begin
rx_ok <= 1'b0;
send_data <= 8'd0;
packet_status <= 20'b0;
error_count <= 5'd0;
packet_count <= 5'd0;
err_rate <= 8'd0;
// first_packet_seen <= 1'b0;
end else if(neg_rx_done) begin
// if(!first_packet_seen) begin
// first_packet_seen <= 1'b1;
// rx_ok <= 1'b0;
// send_data <= 8'd0;
// end else begin
// 先收数据(高8位),后收CRC(低2位)
// rx_data[9:2] 为数据, rx_data[1:0] 为CRC
if(compute_crc2(rx_data[9:2]) == rx_data[1:0]) begin
rx_ok <= 1'b1;
send_data <= rx_data[9:2];
end else begin
rx_ok <= 1'b0;
send_data <= 8'd0;
end
if(packet_count < 5'd20) begin
packet_count <= packet_count + 1'b1;
end
// 统计错误率: 检查 CRC 是否匹配
error_count <= error_count - packet_status[19] + ((compute_crc2(rx_data[9:2]) == rx_data[1:0]) ? 1'b0 : 1'b1);
packet_status <= {packet_status[18:0], (compute_crc2(rx_data[9:2]) == rx_data[1:0]) ? 1'b0 : 1'b1};
//计算误包率 查表减少资源占用
if(packet_count > 0) begin
case(packet_count)
5'd1: err_rate <= error_count * 8'd100;
5'd2: err_rate <= error_count * 8'd50;
5'd3: err_rate <= error_count * 8'd33;
5'd4: err_rate <= error_count * 8'd25;
5'd5: err_rate <= error_count * 8'd20;
5'd6: err_rate <= error_count * 8'd16;
5'd7: err_rate <= error_count * 8'd14;
5'd8: err_rate <= error_count * 8'd12;
5'd9: err_rate <= error_count * 8'd11;
5'd10: err_rate <= error_count * 8'd10;
5'd11: err_rate <= error_count * 8'd9;
5'd12: err_rate <= error_count * 8'd8;
5'd13: err_rate <= error_count * 8'd7;
5'd14: err_rate <= error_count * 8'd7;
5'd15: err_rate <= error_count * 8'd6;
5'd16: err_rate <= error_count * 8'd6;
5'd17: err_rate <= error_count * 8'd5;
5'd18: err_rate <= error_count * 8'd5;
5'd19: err_rate <= error_count * 8'd5;
5'd20: err_rate <= error_count * 8'd5;
default: err_rate <= 8'd0;
endcase
end else begin
err_rate <= 8'd0;
end
// end
end else begin
rx_ok <= 1'b0;
end
end
endmodule
OLED显示实现:
可以参照电子森林给出的一个例程:https://www.eetree.cn/wiki/oled_spi_verilog
但是这个例程不可用(对比来看不知道为什么,好像是时钟没分频太快了?)
不过本身用SSD1306驱动 网上资源有很多 AI也能写 写一个例程之后改改就好了(主要rx_ok触发写入显存 滚动写入 更新误包率)
注意AI的字库不可用(会乱码怀疑自己) 可以直接用上面例程里的字库或者网上的网站生成(可以让AI预留一个模板自己填进去)
能量计算和数码管比较简单 此处省略
剩余文件中有一个用来上电直接测试oled的Test_OLED_Top.v和一个用来调整adc到输出信号中间系数的仿真Tb_Wave_Test.v
5. 仿真波形图
主要是滤波器那块

3k->2k->3k 除了刚复位会有点问题 其余交界处无多次跳变,很稳定
三、功能展示和遇到的问题
1. 可达通信距离
主要限制在于扬声器声音太小(如视频),比较容易提高,但来不及了,考虑到电脑外壳,大概距离为2cm
2. 最高稳定码速和稳定性测试
经测试最高稳定码速大约710bps,过程如下:
输入字符均为1011121314151617(每次连续发送两个字符)
实测从100->800,梯度100->10二分法发现710之前都能稳定接收

3. FPGA占用报告


4. 遇到的问题
(1) LE资源不够
首先OLED显示驱动就会占据近900的LE资源,剩下的分配很紧张
这是第一次使用FIR时的占用

这是刚写完时的占用

解决方法:改用IIR,根据10位adc减少位宽,缩减字库,缩减窗大小
(2) 复位后接收1时会跳变0

解决方案:由于复位后相当于突然到3k,在IIR递推计算时会产生宽带激励,无法避免,原本选择丢掉复位后第一个包,但后来发现似乎没有影响?(可能持续时间太短,被当作毛刺了,可能波特率再高点就有问题了?)
四、心得体会
AI和网上的资源很丰富,遇到陌生的模块可以先用ai解释原理,提供雏形,但要有辨别能力,对于明显有问题,前后矛盾的解法要能够辨识。最好能够将复杂任务转化为多个已知模块连接(如本任务滤波测量->串口->oled显示),能够联系已学其他知识提供解法(如本次联系施密特触发器写门限)