2026寒假练 - 基于小脚丫FPGA的电赛训练平台实现声波数据通信收发器
该项目使用了小脚丫FPGA的电赛训练平台、Verilog语言,实现了声波数据通信收发器的设计,它的主要功能为:实现BFSK声波数据通信收发并在OLED屏上显示。
标签
FPGA
verilog
2026寒假练
飞到床头钻你被窝
更新2026-03-24
华中科技大学
31

一、项目介绍

1.  项目需求

通过 DAC 输出声波 BFSK 信号(例如 2 kHz 3 kHz 两个频点),外接扬声器播放;设计前导码、长度字段、数据区与 CRC 校验。 

麦克风接收声波信号,完成带通滤波、频点能量检测、位同步与帧同步,解码出数据。 

OLED 显示接收的字符或数据包内容与实时误包率;七段数码管显示当前码速。 

实现噪声自适应门限或信噪比估计,提高在环境噪声下的稳定性。

 

2.  需求分析

可分为如下几个要点:

生成BFSK信号,设计数据格式和校验

adc模块接收

滤波,求频点能量,解码,校验

oled、数码管显示

自适应门限

 

二、功能实现

1.  硬件介绍和分析

Altera MAX10系列10M02SCM芯片的核心板:

只有2304LE资源,故滤波器阶数不能过高,同时RAM不能初始化(无ROM),字库会占据很大一部分资源

10/50Msps高速ADC

只用提供时钟,并口读数据即可

OLED128*64

写一个驱动+字库即可

麦克风放大模块:

实际上是麦克风放大加直流偏置,用跳线帽将MICAin连接就可以使用了

电脑扬声器生成BFSK信号(DAC转扬声器也可以 但我的功放坏了)

同时注意到拓展板上没有留空余GPIO口和供电口,所以必须拆下DAC模块给麦克风模块供电

 

2.  实现方式

BFSK信号设计:3khz为逻辑12khz为逻辑0

数据格式及接收方式:仿照UART的工作方式,空闲时持续1,拉低0以作为起始位,后续接8位数据区(ASCII码),2位校验区(CRC-2),4位中止位(发现连续发送时12个中止位容易丢包),实际上就是写一个单工UART

滤波:有FIRIIR两种方案,但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预留一个模板自己填进去)

 

能量计算和数码管比较简单 此处省略

剩余文件中有一个用来上电直接测试oledTest_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,根据10adc减少位宽,缩减字库,缩减窗大小

 

(2)    复位后接收1时会跳变0

解决方案:由于复位后相当于突然到3k,在IIR递推计算时会产生宽带激励,无法避免,原本选择丢掉复位后第一个包,但后来发现似乎没有影响?(可能持续时间太短,被当作毛刺了,可能波特率再高点就有问题了?)

 

四、心得体会

AI和网上的资源很丰富,遇到陌生的模块可以先用ai解释原理,提供雏形,但要有辨别能力,对于明显有问题,前后矛盾的解法要能够辨识。最好能够将复杂任务转化为多个已知模块连接(如本任务滤波测量->串口->oled显示),能够联系已学其他知识提供解法(如本次联系施密特触发器写门限)

附件下载
Acoustic Wave Data Communication Transceiver.zip
团队介绍
华中科技大学
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号