基于小脚丫FPGA设计电子琴
使用小脚丫fpga制作电子琴,可以实现模拟扬声器带谐波正弦波输出,双键和弦,蜂鸣器弹奏,扬声器和蜂鸣器自动演奏
标签
FPGA
数字逻辑
2022暑假在家练
游泳的鸟儿
更新2022-09-06
福州大学
959

一.项目需求:

目标:自己组装,并通过编程驱动模拟扬声器实现电子琴的功能

一.基于套件和工具,自己组装电子琴

二自己编程基于FPGA实现:

1.存储一段音乐,并可以进行音乐播放,

2.可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展

3.使用扬声器进行播放时,输出的音调信号除了对应于该音调的单频正弦波外,还必须包含至少一个谐波分量

4.音乐的播放支持两种方式,这两种方式可以通过开关进行切换: 

①当开关切换到蜂鸣器端,可以通过蜂鸣器来进行音乐播放

②当开关切换到扬声器端,可以通过模拟扬声器来进行音乐播放,每个音符都必须包含基频 + 至少一个谐波分量

本人已实现全部功能。

二.原理及实现方法:

1.整体原理及框图

Fr_iFA-PPFg23RkEL6hCaF4TspUG

上图是整个电子琴实现的框图。首先,是模拟扬声器部分。FPGA可以并行处理,所以我采用14个通道全部是并行存在的,即13个按键通道,一个自动演奏通道。若识别倒按键按下,则查询波表,这个波表是我提前写好的,一个正弦波含一个谐波,用128个点来离散描述,查询到当前时刻的点之后就送入累加器,这样可以保证有两个按键同时按下时,即和弦,可以实现。将两个正弦波叠加后,送入pwm生成的模块生成pwm。硬件上通过低通滤波器将pwm波滤波后形成带谐波的正弦波驱动模拟扬声器。而自动演奏部分就是新增加一个表,使得fpga在一定时间查谱表后再进行生成pwm。接着是蜂鸣器部分。同样采用并行,先读取按键,之后通过查表,得到分频值,在送入pwm生成的模块生成pwm波。蜂鸣器自动演奏部分也是新增加一个表,使得fpga在一定时间查谱表后再进行生成pwm。

2蜂鸣器和模拟喇叭的差别

模拟扬声器在频率响应比蜂鸣器好,模拟扬声器在人耳能听到的频率内电声转化效率差不多,音质较好。蜂鸣器声音较尖,说明在相对高一点的频率电声转化效率高,音质较差。

3用蜂鸣器和模拟喇叭的实现方法差别以及音效差别分析

①模拟喇叭

Fvot4zd5M85ZIU6Xl9yTtWf0YmOy

 

pwm是脉宽调制技术,当我们固定下pwm频率时,高电平脉冲时间的与脉冲周期的比值为占空比。当占空比越高,则输出的电压有效值越高。fpga只能发出pwm波,为了能发和弦,需要正弦波叠加。这时,我们用pwm波不断改变有效值,使其有效值变为一个正弦波,就是spwm波。

Fjt91ar9iJQDYBVyeRk9ZPBkN0DN

由于我们需要带谐波的正弦波,所以占空比不是单纯的正弦波数据,而是加上谐波后的数据,这样发出的spwm就是带谐波的正弦波。硬件电路上有rc组成的低通滤波器,这样pwm经过低通滤波器之后就变成了真正的带谐波的正弦波模拟信号,用它驱动喇叭就可以发出动人的声音,也可以用正弦波合成一下发出和弦。

②蜂鸣器

蜂鸣器发声就只需要pwm波即可。用计数器的方式对晶振频率进行分频,得到需要发声的频率,设置pwm占空比为50%,用这个pwm经过三极管后驱动蜂鸣器,即可实现电子琴功能。蜂鸣器由于腔体较小,膜片较薄,所以声音较尖,音效远不如模拟喇叭好,还不能叠加发和弦。

3模拟放大电路的仿真及分析

FhVl8zGf_-X6qpwrovmQzhlg8OMs

如上图是模拟放大部分。

第一部分为低通滤波,可将pwm数字信号变为相对平滑的模拟信号。这是让fpga输出转化为带谐波的正弦波关键部分,也是实现和弦的关键部分。

Fn37yKiVrcLk5eiAC92sQwEJwDshFtWqy-G5B9gbd9ESdhN-YZZwikC-

如图,是一个仿真部分,上图R1 R2 C1组成低通滤波,R1与R2分压,在50%时,输出电压为3.3*(100/(100+300))*0.5=0.41,与仿真结果相符;在80%时,输出电压为3.3*(100/(100+300))*0.8=0.66,与仿真结果相符;

第二部分为电位器分压调音量与电容通交阻直

FsU9tBM86cocX-pp71ZSLCi4yvpo

如上图,电位器设置50%,从示波器可见小正弦波赋值为大正弦波50%,且由于电容隔直使得小正弦波偏置电压为0。

第三部分为8002功放

FgYcM7Gj2sTkEcYTDybeNcPtHAFN

由于仿真软件找不到8002的模型,我就将8002根据内部框图拆开仿真。

Fsll3nNcwCjuztF1UwbDDHZ96qGj

FlDbbJX0IJiFnT3krhUD4qVhHqc38002内部框图,右边的运放固定增益,为40k/40k+1=2倍,左边的运放增益为外部设置。由电路图可知,电子琴将8002左边的运放接成反相放大,R23和R18决定倍数为47k/10k=4.7倍。所以总的放大倍数为2*4.7=9.4倍。由仿真图可知400mVp被放大约为4Vp,大致为10倍,与计算9.4倍差不多。

4.主要代码片段及说明

①模拟扬声器弹奏及和弦

驱动模拟扬声器,为了能实现双键按下和弦,那么需要使用正弦波。由于题目要求能带谐波,所以先生成一个含谐波的波表,使用时再去查表。

module wave(
	input key,
	input sys_clk,
	output reg [5:0] put,
	input  [9:0] tone
	);
	
reg [6:0] address;
reg [9:0] cnt1;


always @(posedge sys_clk )//wave点切换
begin
	if(cnt1 < tone)   
	begin
		cnt1 <= cnt1 + 1'b1;
	end
	else
	begin
		cnt1 <= 32'd0;
		if(address < 127)
		begin
				address <= address+ 1'b1;
		end
		else
		begin
				address <= 7'd0;
		end
	end
end


always@(*)
begin
if(key==1'd0)
	begin
			case(address)
             7'd0 : put= 6'd32;
              7'd1 : put= 6'd33;
              7'd2 : put= 6'd35;
              7'd3 : put= 6'd37;
              7'd4 : put= 6'd39;
              7'd5 : put= 6'd41;
              7'd6 : put= 6'd42;
              7'd7 : put= 6'd44;
              7'd8 : put= 6'd45;
              7'd9 : put= 6'd46;
              7'd10 : put= 6'd47;
              7'd11 : put= 6'd47;
              7'd12 : put= 6'd48;
              7'd13 : put= 6'd48;
              7'd14 : put= 6'd48;
              7'd15 : put= 6'd48;
              7'd16 : put= 6'd48;
              7'd17 : put= 6'd47;
              7'd18 : put= 6'd46;
              7'd19 : put= 6'd45;
              7'd20 : put= 6'd44;
              7'd21 : put= 6'd43;
              7'd22 : put= 6'd43;
              7'd23 : put= 6'd41;
              7'd24 : put= 6'd41;
              7'd25 : put= 6'd40;
              7'd26 : put= 6'd38;
              7'd27 : put= 6'd37;
              7'd28 : put= 6'd36;
              7'd29 : put= 6'd36;
              7'd30 : put= 6'd35;
              7'd31 : put= 6'd35;
              7'd32 : put= 6'd35;
              7'd33 : put= 6'd35;
              7'd34 : put= 6'd35;
              7'd35 : put= 6'd35;
              7'd36 : put= 6'd36;
              7'd37 : put= 6'd37;
              7'd38 : put= 6'd38;
              7'd39 : put= 6'd39;
              7'd40 : put= 6'd40;
              7'd41 : put= 6'd41;
              7'd42 : put= 6'd42;
              7'd43 : put= 6'd43;
              7'd44 : put= 6'd43;
              7'd45 : put= 6'd44;
              7'd46 : put= 6'd46;
              7'd47 : put= 6'd46;
              7'd48 : put= 6'd47;
              7'd49 : put= 6'd47;
              7'd50 : put= 6'd48;
              7'd51 : put= 6'd48;
              7'd52 : put= 6'd48;
              7'd53 : put= 6'd47;
              7'd54 : put= 6'd47;
              7'd55 : put= 6'd46;
              7'd56 : put= 6'd45;
              7'd57 : put= 6'd44;
              7'd58 : put= 6'd43;
              7'd59 : put= 6'd42;
              7'd60 : put= 6'd40;
              7'd61 : put= 6'd37;
              7'd62 : put= 6'd36;
              7'd63 : put= 6'd34;
              7'd64 : put= 6'd32;
              7'd65 : put= 6'd32;
              7'd66 : put= 6'd29;
              7'd67 : put= 6'd28;
              7'd68 : put= 6'd26;
              7'd69 : put= 6'd23;
              7'd70 : put= 6'd22;
              7'd71 : put= 6'd20;
              7'd72 : put= 6'd20;
              7'd73 : put= 6'd18;
              7'd74 : put= 6'd17;
              7'd75 : put= 6'd17;
              7'd76 : put= 6'd16;
              7'd77 : put= 6'd16;
              7'd78 : put= 6'd15;
              7'd79 : put= 6'd16;
              7'd80 : put= 6'd16;
              7'd81 : put= 6'd17;
              7'd82 : put= 6'd17;
              7'd83 : put= 6'd18;
              7'd84 : put= 6'd20;
              7'd85 : put= 6'd20;
              7'd86 : put= 6'd21;
              7'd87 : put= 6'd22;
              7'd88 : put= 6'd23;
              7'd89 : put= 6'd24;
              7'd90 : put= 6'd25;
              7'd91 : put= 6'd26;
              7'd92 : put= 6'd27;
              7'd93 : put= 6'd28;
              7'd94 : put= 6'd29;
              7'd95 : put= 6'd29;
              7'd96 : put= 6'd29;
              7'd97 : put= 6'd29;
              7'd98 : put= 6'd29;
              7'd99 : put= 6'd29;
              7'd100 : put= 6'd28;
              7'd101 : put= 6'd28;
              7'd102 : put= 6'd27;
              7'd103 : put= 6'd26;
              7'd104 : put= 6'd24;
              7'd105 : put= 6'd24;
              7'd106 : put= 6'd23;
              7'd107 : put= 6'd21;
              7'd108 : put= 6'd22;
              7'd109 : put= 6'd20;
              7'd110 : put= 6'd19;
              7'd111 : put= 6'd19;
              7'd112 : put= 6'd17;
              7'd113 : put= 6'd17;
              7'd114 : put= 6'd16;
              7'd115 : put= 6'd17;
              7'd116 : put= 6'd16;
              7'd117 : put= 6'd17;
              7'd118 : put= 6'd17;
              7'd119 : put= 6'd18;
              7'd120 : put= 6'd18;
              7'd121 : put= 6'd19;
              7'd122 : put= 6'd21;
              7'd123 : put= 6'd22;
              7'd124 : put= 6'd24;
              7'd125 : put= 6'd26;
              7'd126 : put= 6'd27;
              7'd127 : put= 6'd30;
			endcase
	end
else
begin
put<=6'd32;
end
end
endmodule

↑其中tone是顶层文件输入该音调对应的计数器上限,这样当计数器累加到溢出就切换查表,address会加一,address再从case中查表,put输出,从而发出正弦波的每个点。

//*****************************************喇叭弹奏*********************************
wave kk1(.sys_clk  (sys_clk),	.key     (key_in[0]),	.put     (q1),	.tone(tone1));
wave kk2(.sys_clk  (sys_clk),	.key     (key_in[1]),	.put     (q2),	.tone(tone2));
wave kk3(.sys_clk  (sys_clk),	.key     (key_in[2]),	.put     (q3),	.tone(tone3));
wave kk4(.sys_clk  (sys_clk),	.key     (key_in[3]),	.put     (q4),	.tone(tone4));
wave kk5(.sys_clk  (sys_clk),	.key     (key_in[4]),	.put     (q5),	.tone(tone5));
wave kk6(.sys_clk  (sys_clk),	.key     (key_in[5]),	.put     (q6),	.tone(tone6));
wave kk7(.sys_clk  (sys_clk),	.key     (key_in[6]),	.put     (q7),	.tone(tone7));
wave kk8(.sys_clk  (sys_clk),	.key     (key_in[7]),	.put     (q8),	.tone(tone8));
wave kk9(.sys_clk  (sys_clk),	.key     (key_in[8]),	.put     (q9),	.tone(tone9));
wave kk10(.sys_clk  (sys_clk),	.key     (key_in[9]),	.put     (q10),	.tone(tone10));
wave kk11(.sys_clk  (sys_clk),	.key     (key_in[10]),	.put     (q11),	.tone(tone11));
wave kk12(.sys_clk  (sys_clk),	.key     (key_in[11]),	.put     (q12),	.tone(tone12));
wave kk13(.sys_clk  (sys_clk),	.key     (key_in[12]),	.put     (q13),	.tone(tone13));

wave auto(.sys_clk  (sys_clk),	.key     (key_auto),	.put     (q14),	.tone(tone14));


always@(*)begin
if(key_in==13'b0111111111111||key_in==13'b1011111111111||key_in==13'b1101111111111||key_in==13'b1110111111111||key_in==13'b1111011111111||key_in==13'b1111101111111||key_in==13'b1111110111111||key_in==13'b1111111011111||key_in==13'b1111111101111||key_in==13'b1111111110111||key_in==13'b1111111111011||key_in==13'b1111111111101||key_in==13'b1111111111110)
 qout<=(q1+q2+q3+q4+q5+q6+q7+q8+q9+q10+q11+q12+q13-416)+32;
 else
    if(key_auto==0)
        qout<=q14;
    else
        qout<=(q1+q2+q3+q4+q5+q6+q7+q8+q9+q10+q11+q12+q13-416)/2+32;
end

我们将每个键和生成波形点进行例化,可以使顶层文件简洁。下面使用一个always语句,让只按下一个键时输出对应波形,而按下多个键时,就累加两个波形并除以2,保证和弦时输出的音量和单键时输出的音量相等。

②蜂鸣器部分。

蜂鸣器采用直接分频的方式得到对应音符频率。

//*******************************************************************************************
//******************************蜂鸣器*******************************************************
always@(posedge sys_clk)
begin
    case(key_in)
    13'b1111111111110:freq_data2<=Tone1;
    13'b1111111111101:freq_data2<=Tone2;
    13'b1111111111011:freq_data2<=Tone3;
    13'b1111111110111:freq_data2<=Tone4;
    13'b1111111101111:freq_data2<=Tone5;
    13'b1111111011111:freq_data2<=Tone6;
    13'b1111110111111:freq_data2<=Tone7;
    13'b1111101111111:freq_data2<=Tone8;
    13'b1111011111111:freq_data2<=Tone9;
    13'b1110111111111:freq_data2<=Tone10;
    13'b1101111111111:freq_data2<=Tone11;
    13'b1011111111111:freq_data2<=Tone12;
    13'b0111111111111:freq_data2<=Tone13;
    default:freq_data2<=4;
    endcase
end

↑根据按键选择freq_data2的值,fre_data2是分频系数,决定方波频率,也就决定了蜂鸣器的音调。

//设置50%占空比:音阶分频计数值的一半即为占空比的高电平数
assign  duty_data   =   freq_data   >>    1'b1;
//freq_cnt:当计数到音阶计数值或跳转到下一音阶时,开始重新计数
always@(posedge sys_clk or  negedge sys_rst_n)
begin
    if(key_auto==1)freq_data=freq_data2;
    else freq_data=freq_data1;
    
    
    if(sys_rst_n == 1'b0)
        freq_cnt    <=  18'd0;
    else    if(freq_cnt == freq_data )
        freq_cnt    <=  18'd0;
    else
        freq_cnt    <=  freq_cnt +  1'b1;
end
//beep:输出蜂鸣器波形
always@(posedge sys_clk )
    if(sys_rst_n == 1'b0 )
        beep    <=  1'b0;
    else    if(freq_cnt >= duty_data  && switch==1'b0)
        beep    <=  1'b1;
    else
        beep    <=  1'b0;

↑duty_data为freq_data的一半,发出来的就是50%的方波。freq_data为我们设好的频率,当计数器累加到duty_data(freq_data的一半)就切换电平,这样就可以发出方波了。

③高低音切换

//**************************切换高低声部*************************
always @(posedge sys_clk)
      begin
         
	     if (key_high==1'b1 && key_low==1'b0)
	        begin
	        tone1<=10'd360;
	        tone2<=10'd340;
	        tone3<=10'd322;
	        tone4<=10'd304;
	        tone5<=10'd286;
	        tone6<=10'd270;
	        tone7<=10'd256;
	        tone8<=10'd240;
	        tone9<=10'd228;
	        tone10<=10'd214;
	        tone11<=10'd202;
	        tone12<=10'd192;
	        tone13<=10'd180;
	        
	        Tone1<=18'd45801;
	        Tone2<=18'd43230;
	        Tone3<=18'd40804;
	        Tone4<=18'd38514;
	        Tone5<=18'd36352;
	        Tone6<=18'd34312;
	        Tone7<=18'd32386;
	        Tone8<=18'd30568;
	        Tone9<=18'd28853;
	        Tone10<=18'd27233;
	        Tone11<=18'd25705;
	        Tone12<=18'd24262;
	        Tone13<=18'd22900;
	        

	        end
	   else
	   begin
	        if (key_low==1'b1&&key_high==1'b0)
	        begin

	        tone1<=10'd180;
	        tone2<=10'd170;
	        tone3<=10'd161;
	        tone4<=10'd152;
	        tone5<=10'd143;
	        tone6<=10'd135;
	        tone7<=10'd128;
	        tone8<=10'd120;
	        tone9<=10'd114;
	        tone10<=10'd107;
	        tone11<=10'd101;
	        tone12<=10'd96;
	        tone13<=10'd90;
	        
	        Tone1<=18'd22944;
	        Tone2<=18'd21656;
	        Tone3<=18'd20441;
	        Tone4<=18'd19293;
	        Tone5<=18'd18211;
	        Tone6<=18'd17188;
	        Tone7<=18'd16224;
	        Tone8<=18'd15313;
	        Tone9<=18'd14454;
	        Tone10<=18'd13642;
	        Tone11<=18'd12877;
	        Tone12<=18'd12154;
	        Tone13<=18'd11472;
	        end
	        else    
	        begin
            tone1 <= tone1;
            tone2 <= tone2;
            tone3 <= tone3;
            tone4 <= tone4;
            tone5 <= tone5;
            tone6 <= tone6;
            tone7 <= tone7;
            tone8 <= tone8;
            tone9 <= tone9;
            tone10 <= tone10;
            tone11 <= tone11;
            tone12 <= tone12;
            tone13 <= tone13;
            
	        Tone1<=Tone1;
	        Tone2<=Tone2;
	        Tone3<=Tone3;
	        Tone4<=Tone4;
	        Tone5<=Tone5;
	        Tone6<=Tone6;
	        Tone7<=Tone7;
	        Tone8<=Tone8;
	        Tone9<=Tone9;
	        Tone10<=Tone10;
	        Tone11<=Tone11;
	        Tone12<=Tone12;
	        Tone13<=Tone13;
            end
	   end
	 end
//**************************************************************************************

↑通过检测按键key_high与key_low的状态来选择高低声部,这部分是扬声器和蜂鸣器同时选高声部或低声部。

④自动演奏部分

//***********************自动演奏*******************************************************
reg     [24:0]  cnt_yin         ;   //每个音时常累加器
reg     [6:0]   cnt_pai   ;   //音的个数


parameter   TIME_cnt =   25'd3_700_000;   //每个音阶鸣叫时间
//循环计数器
always@(posedge sys_clk or negedge sys_rst_n)
    if(!sys_rst_n)
        cnt_yin <=   25'd0;
    else if(cnt_yin == TIME_cnt )
        cnt_yin <=   25'd0;
    else 
		cnt_yin <=  cnt_yin +   1'b1;
		
		
//对音个数进行计数
always@(posedge sys_clk or negedge sys_rst_n)
    if((cnt_yin == TIME_cnt && cnt_pai ==  36)||(!sys_rst_n) )
        cnt_pai   <=  7'd0;
    else    if(cnt_yin == TIME_cnt)
        cnt_pai   <=  cnt_pai + 1'b1;

//不同时间鸣叫不同的音阶
always@(posedge sys_clk)
        case(cnt_pai)
		  0:begin tone14<=10'd180;freq_data1<=22944;end//1
		  1:begin tone14<=10'd161;freq_data1<=20441;end//2
		  2:begin tone14<=10'd143;freq_data1<=18211;end//3
		  3:begin tone14<=10'd107;freq_data1<=13642;end//6
		  4:begin tone14<=10'd120;freq_data1<=15313;end//5
		  5:begin tone14<=10'd107;freq_data1<=13642;end//6
		  6:begin tone14<=10'd120;freq_data1<=15313;end//5
		  7:begin tone14<=10'd107;freq_data1<=13642;end//6
		  8:begin tone14<=10'd120;freq_data1<=15313;end//5
		  9:begin tone14<=10'd161;freq_data1<=20441;end//2
		  
		  10:begin tone14<=10'd143;freq_data1<=18211;end//3
		  11:begin tone14<=10'd107;freq_data1<=13642;end//6
		  12:begin tone14<=10'd120;freq_data1<=15313;end//5
		  13:begin tone14<=10'd107;freq_data1<=13642;end//6
		  14:begin tone14<=10'd120;freq_data1<=15313;end//5
		  15:begin tone14<=10'd107;freq_data1<=13642;end//6
		  16:begin tone14<=10'd120;freq_data1<=15313;end//5
		  17:begin tone14<=10'd143;freq_data1<=18211;end//3
		  
		  18:begin tone14<=10'd161;freq_data1<=20441;end//2
		  19:begin tone14<=10'd180;freq_data1<=22944;end//1
		  20:begin tone14<=10'd214;freq_data1<=27233;end//.6
		  21:begin tone14<=10'd180;freq_data1<=22944;end//1
		  22:begin tone14<=10'd161;freq_data1<=20441;end//2
		  23:begin tone14<=10'd180;freq_data1<=22944;end//1
		  24:begin tone14<=10'd214;freq_data1<=27233;end//.6
		  25:begin tone14<=10'd180;freq_data1<=22944;end//1
		  
		  26:begin tone14<=10'd143;freq_data1<=18211;end//3
		  27:begin tone14<=10'd143;freq_data1<=18211;end//3
		  28:begin tone14<=10'd143;freq_data1<=18211;end//3
		  29:begin tone14<=10'd135;freq_data1<=17188;end//4
		  30:begin tone14<=10'd143;freq_data1<=18211;end//3
		  31:begin tone14<=10'd161;freq_data1<=20441;end//2
		  32:begin tone14<=10'd161;freq_data1<=20441;end//2
		  33:begin tone14<=10'd1;freq_data1<=4;end//停
		  34:begin tone14<=10'd1;freq_data1<=4;end
		  35:begin tone14<=10'd1;freq_data1<=4;end
		  36:begin tone14<=10'd1;freq_data1<=4;end

    endcase

↑这部分是音乐自动演奏,是模拟扬声器和蜂鸣器共用的。如上左图,第一个计数器是是决定每个音符响多久的时间,当第一个计数器累加溢出,第二个计数器就会累加,第二个计数器会去查表,在case语句来选择每个时间对应的音符,把这些音符送入模拟扬声器或蜂鸣器就可以自动演奏了。

5.遇到的主要难题及解决方法

①难题:网页版fpga软件无法使用ROM IP核来存储波形表。

解决方法:在程序中直接采用寄存器来存储。

②难题:不知道怎么进行和弦。

解决方法:查书,书中有讲到正弦波的叠加,有了灵感后就用verilog语言来写正弦波叠加。

6.改进建议

①资源使用还可优化,一下是资源使用报告

FjCPR84nT7tO-PJSAYOKXYYZC_dK

②可增加音色变换,来模拟打击乐、吉他、二胡等乐器。

③可增加节拍提示功能。

④可增加录音功能。

 

附件下载
archive.zip
网页ide下载的工程文件
implement.jed
网页ide下载的文件
团队介绍
福州大学 郑凯文
团队成员
游泳的鸟儿
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号