针对红外键盘HT6221的输出设计按键状态机

1. 前言

前言部分对全文无关紧要,可直接跳过。

距离1月9日发布的上一篇博客已经过去大约一个月的时间,期间旅游、过年以及各种事项接踵而至,并且更要命的是,矩阵4x4键盘不论如何调试,都会出问题。根据指导人所说的修改方案,即加上上拉电阻,问题依旧存在——状态机的跳转不如所愿,不该出现数据的信号出现了数据等——起初,我以为是我自己的问题,拼命找bug,代码反复修改多次,无疾而终,这便花去了大概五六天的时间;之后用梅雪松(梅哥)的代码加上SignalTap进行调试观察,出人意料的是,他的输出,比如多按几下按键S6,输出的数据会跳变到4或者7,再跳变成6,或者干脆不跳变了,直接是4或者7。比照他的代码,修改,润色,效果依然不理想。这里并不是说梅哥的代码有问题,我相信他的代码肯定是经过自己层层修改并验证过的,只是自己庶竭驽钝。看到配件箱中有个红外键盘,想了想,两者应该能达到同样的效果,并且在我看来,红外键盘比矩阵4x4键盘至少有如下两个好处:

  1. 拿来即用,且在一定范围内可以持续使用,省去接线等不必要的繁琐工作;
  2. 红外键盘按键有21个,比4x4的矩阵键盘多出5个按键,虽然说多出来的不一定能够用到,但是多一些总该是有点好处的;

出于对时间流逝的感到不安、焦虑,又出于想要尽快得到按键正确输出的目的,最终,决定弃用矩阵键盘,改用红外键盘。

这也是为什么上一篇博客分明是与矩阵键盘相关,而此篇博客又叙述红外键盘之内容的原因。

2. 键盘键值定义

虽说换了个按键终端,但是主题还是没有变——依旧是针对DDS的频率控制字fwd以及相位控制字pwd,进行设计。

比如,想让DDS输出的频率为1000Hz,那么在红外键盘上,分别按下1000后,再按OK键,即可得到相应的以1000Hz的频率输出的正弦波。

实际上我们用到的并不只是一个DDS,而是双通道的DDS,也就是说,一个DDS有两个输出通道,可以同时输出两个完全不相关的正弦波信号。可以想象成有两条路,一个车开得快,一个车开得慢这种。而每一个通道的输入都有两个控制字——频率控制字与相位控制字——故双通道就有四个控制字,分别为频率控制字fwd_afwd_b,相位控制字pwd_apwd_b。所以,除了输出相应的频率数据之外,还得对通道A和通道B进行选择。由于按下按键,FPGA开发板并不知道每个按键所对应的操作,这就需要对红外键盘每个按键的输出进行定义。

红外键盘外观如下:
在这里插入图片描述

根据《FPGA自学笔记——设计与验证》第5.4章节,编写出相应的RTL设计文件,上板调试并验证可得每个按键所对应的键值,如下表左边两侧所示。
在这里插入图片描述
进行状态划分之前,要先定义每个按键所对应的操作。设定如下:

键盘按键按键操作
EQOK
CH设定相位控制字
CH-选定通道A并设定频率控制字
CH+选定通道B并设定频率控制字
-回退

3 状态机的编写

3.1状态机的状态划分

考虑到按键使用的实际情况,将状态划分如下:

状态符号
空闲IDLE0
接收频率控制字RECEIVE_FWD1
过渡TRANSITION2
接收相位控制字RECEIVE_PWD3
读取数据READ4
产生标志信号FLAG5

每个状态的状态跳转条件如下:

IDLE状态下,等待通道A或通道B设置按键,若有,则进入RECEIVE_FWD状态;
若在此状态下按回退键,则依然保持IDLE

RECEIVE_FWD状态下,
在按下数字按键之前,按下回退键,则说明按下通道A设置按键按错了,则回到IDLE
在按下数字按键之后,按下回退键,则说明想要清除上一步输入的数据;
等待各种数字按键,
若按下OK键,说明不需要设置相位控制字,直接进入READ状态;
若按下CH键,则说明需要设置相位控制字,进入过渡状态TRANSITION

TARNSITION状态下直接进入RECEIVE_PWD状态;

RECEIVE_PWD状态下,等待各种数字按键,当按下OK后,说明频率控制字和相位控制字已设置完毕,进入READ状态;

READ状态下,读取数据,并进入FLAG状态;

FLAG状态,产生操作完成的out_flag信号,并回到IDLE状态。
其实产生标志信号的操作在READ状态下就能够完成,此处为了理得更清楚,多加一个状态。

3.2 状态机所需的信号

状态机各种信号定义如下:

/*=============================================================================
#     FileName: key_FSM.v
#         Desc: 
#       Author: ohliver
#        Email: ohliver@foxmail.com
#     HomePage: https://blog.csdn.net/qq_15062763
#      Version: 0.0.1
#   LastChange: 2020-02-02 20:02:12
#      History:
=============================================================================*/
module key_FSM(
	//I
	clk_50M 	,
	rstn		,
	in_addr		,//这是红外键盘每个按键所对应的按键地址
	in_data		,//这是红外键盘每个按键所对应的按键数据
    in_flag     ,//这是红外键盘解码完成后所发出的标志信号
	
	//O
	fwd_a       ,
	fwd_b       ,
	pwd_a       ,
	pwd_b       ,
    out_flag     //这是状态机跳转完成发出的标志信号
);

input 		 clk_50M	;
input 		 rstn		;
input [15:0] in_addr	;
input [15:0] in_data	;
input        in_flag    ;

output reg [63:0] fwd_a     ;
output reg [63:0] fwd_b     ;
output reg [18:0] pwd_a     ;  //为什么是19位而不是32位
output reg [18:0] pwd_b     ;  //因为FPGA上没有那么多IO端口,所以必须少分配点。
output reg        out_flag  ;

parameter IDLE 			= 4'd0;
parameter RECEIVE_FWD  	= 4'd1;
parameter TRANSITION    = 4'd2;
parameter RECEIVE_PWD  	= 4'd3;
parameter READ          = 4'd4;
parameter FLAG			= 4'd5;

reg [ 3:0] state		;
reg [ 3:0] n_state		;

reg        channel_a    ;
reg        channel_b    ;
reg        in_flag_r    ;
reg [ 3:0] key_value_tmp;

reg [31:0] key_value_fout;
reg [31:0] read_fout     ;
reg [31:0] key_value_pout;
reg [31:0] read_pout     ;

wire [31:0] key_data = {in_data, in_addr}; 

/*
说明:这里为什么需要将in_flag信号打一拍?
假设一个时钟周期为20ns(实际上也是20ns);
比如按下一次1键,则对应的key_data == 32'hF30CFF00; 信号key_one拉高
又假设我们按下一次按键需要10ms,而在这10ms内,key_data一直为F30CFF00,也就是说,信号key_one在10ms内一直拉高。
如果直接用key_one信号进行按键1的输入,则对于FPGA来说,它会认为我们按下了10ms/20ns =  500000次按键1,
而我们分明只是按了一次
所以,需要一个时钟周期的脉冲信号,对key_one的高电平进行限制,让我们按下一次按键1,FPGA也就只收到一次按键1.

很巧的是,按下按键1之后,红外键盘对应的解码模块在解码完成后,正好产生一个时钟周期的标志信号flag,在此文件中对应输入的信号 input in_flag;
同时,解码模块在产生标志信号flag的下一个时钟周期,才知道按键1被按下,也就是说,key_one是在flag(flag等同于in_flag)信号产生后的下一拍才会被拉高。
故需要将in_flag信号往后延迟一拍,变成in_flag_r信号,并和信号key_one进行逻辑与,产生出我们所需要的一个时钟周期的按键输入。

为了节省资源,将数字按键0~9,合起来一起与上in_flag_r,即为:
wire number = in_flag_r && (key_zero  || key_one  || key_two  || 
							key_three || key_four || key_five ||
							key_six   || key_seven|| key_eight|| 
							key_nine);
*/

always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn)
        in_flag_r <= 1'b0;
    else 
        in_flag_r <= in_flag;
end

wire key_ch       = in_flag_r && (key_data == 32'hB946FF00);  //CH
wire key_ch_minus = in_flag_r && (key_data == 32'hBA45FF00);  //CH-
wire key_ch_plus  = in_flag_r && (key_data == 32'hB847FF00);  //CH+
wire key_eq		  = in_flag_r && (key_data == 32'hF609FF00);  //EQ
wire key_minus    = in_flag_r && (key_data == 32'hF807FF00);  //-
wire key_zero	  = 			 (key_data == 32'hE916FF00);
wire key_one 	  = 			 (key_data == 32'hF30CFF00);
wire key_two 	  = 			 (key_data == 32'hE718FF00);
wire key_three	  = 			 (key_data == 32'hA15EFF00);
wire key_four 	  = 			 (key_data == 32'hF708FF00);
wire key_five 	  = 			 (key_data == 32'hE31CFF00);
wire key_six 	  = 			 (key_data == 32'hA55AFF00);
wire key_seven    = 			 (key_data == 32'hBD42FF00);
wire key_eight	  = 			 (key_data == 32'hAD52FF00);
wire key_nine 	  = 			 (key_data == 32'hB54AFF00);

wire number = in_flag_r && (key_zero  || key_one  || key_two  || 
							key_three || key_four || key_five ||
							key_six   || key_seven|| key_eight|| 
							key_nine);

//对按键所代表的数值进行解码
always @ (*) begin
    case (key_data)
        32'hE916FF00: key_value_tmp = 4'd0; 
        32'hF30CFF00: key_value_tmp = 4'd1; 
        32'hE718FF00: key_value_tmp = 4'd2;
        32'hA15EFF00: key_value_tmp = 4'd3;
        32'hF708FF00: key_value_tmp = 4'd4;
        32'hE31CFF00: key_value_tmp = 4'd5;
        32'hA55AFF00: key_value_tmp = 4'd6;
        32'hBD42FF00: key_value_tmp = 4'd7;
        32'hAD52FF00: key_value_tmp = 4'd8;
        32'hB54AFF00: key_value_tmp = 4'd9;
        default	    : key_value_tmp = 4'd0; 
    endcase
end

wire fout_zero = (key_value_fout == 0);  //定义key_value_fout=0
wire pout_zero = (key_value_pout == 0);  //定义key_value_pout=0

/*
定义寄存器channel_a和channel_b的意义是
对通道A和通道B的fwd和pwd进行赋值
详见最后20行的代码
*/
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) 
        channel_a <= 1'b0;
    else if (key_ch_minus)
        channel_a <= 1'b1;
    else if (state == IDLE)
        channel_a <= 1'b0;
    else
        channel_a <= channel_a;
end

always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) 
        channel_b <= 1'b0;
    else if (key_ch_plus)
        channel_b <= 1'b1;
    else if (state == IDLE)
        channel_b <= 1'b0;
    else
        channel_b <= channel_b;
end
3.3 状态转换

状态转换图如下:
在这里插入图片描述三段式状态机编写如下:

//=================================================
//FSM part one
//=================================================
always @ (posedge clk_50M or negedge rstn) begin
	if (!rstn)
		state <= IDLE;
	else
		state <= n_state;
end

//==================================================
//FSM part two
//==================================================
always @ (*) begin
	case (state)
		IDLE: 
            if (key_ch_minus || key_ch_plus)
                n_state = RECEIVE_FWD;
            else if (key_minus)
                n_state = IDLE;
            else
                n_state = IDLE;
            
        RECEIVE_FWD:
            if (key_minus && fout_zero) 
                n_state = IDLE;
            else if (key_minus && (!fout_zero))  
                n_state = RECEIVE_FWD;
            else if (key_eq)
                n_state = READ;
            else if (key_ch)
                n_state = TRANSITION;
            else
                n_state = RECEIVE_FWD;

		  TRANSITION: n_state = RECEIVE_PWD;
					 
        RECEIVE_PWD:
            if (key_minus && pout_zero)
                n_state = TRANSITION;
            else if (key_minus && (!pout_zero))
                n_state = RECEIVE_PWD;
            else if (key_eq)
                n_state = READ;
            else
                n_state = RECEIVE_PWD;

        READ: n_state = FLAG;

        FLAG: n_state = IDLE;
        
		default: n_state = IDLE;
	endcase
end

//==================================================
//FSM part three
//==================================================
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) begin
        key_value_fout   <= 32'b0;
        read_fout        <= 32'b0;
        key_value_pout   <= 19'b0;  
        read_pout        <= 19'b0;  
        out_flag         <= 1'b0 ;
    end
    else begin
        case (state)
            IDLE:begin
                key_value_fout   <= 32'b0;
                key_value_pout   <= 19'b0;  
                out_flag         <= 1'b0 ;
                read_fout        <= read_fout;
                read_pout        <= read_pout;            
            end
            
            RECEIVE_FWD: begin
                if (number)
                    key_value_fout <= (key_value_fout<<1) + (key_value_fout<<3) + key_value_tmp;
                else if (key_minus && (!fout_zero))
                    key_value_fout <= (key_value_fout - key_value_tmp)/10;
                else
                    key_value_fout <= key_value_fout;
            end

				TRANSITION: begin end
				
            RECEIVE_PWD: begin
                if (number)
                    key_value_pout <= (key_value_pout<<1) + (key_value_pout<<3) + key_value_tmp;
                else if (key_minus && (!pout_zero))
                    key_value_pout <= (key_value_pout - key_value_tmp)/10;
                else
                    key_value_pout <= key_value_pout;
            end

            READ: begin
                read_fout <= key_value_fout;
                read_pout <= key_value_pout;
            end

            FLAG: out_flag <= 1'b1;

            default:begin
                out_flag         <=  1'b0;
                key_value_fout   <= 32'b0;
                read_fout        <= 32'b0;
                key_value_pout   <= 19'b0;
                read_pout        <= 19'b0;           
            end
        endcase
    end
end

说明:

  1. 对于key_value_fout <= (key_value_fout<<1) + (key_value_fout<<3) + key_value_tmp;语句,翻译成数学表达式即为:
    key_value_fout = key_value_fout * 10 + key_value_tmp;
    为了节省寄存器,将数字10拆分成2+8,而key_value_fout乘2或乘8分别对应key_value_fout左移一位和三位的操作,相比于使用乘法器,能够省下一些寄存器。
  2. 对于按下回退按键导致的除法操作:
    key_value_fout <= (key_value_fout - key_value_tmp)/10;
    本身在FPGA中很忌讳使用除法的,想到在硬件设计中,10 = 32/3,故将上式转变为:
    key_value_fout <= ((key_value_fout - key_value_tmp) * 3) >> 5;
    但是在仿真的过程中发现,输入1000,回退一位,最终的结果是93,而非100,故勉为其难地用了除法。不过需要提醒的一点是,不使用除法,误差较大,优点是逻辑资源占用少,为641个;若使用除法,而且还是两处,虽然结果准确,但逻辑占用激增至1315个,两个除法所占用的逻辑数为674个,比我不使用除法写的代码所占用的逻辑资源还要多。
3.4 输出计算
//===========================================================
//caculate and output
//===========================================================
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) begin
        fwd_a <= 64'b0    ;
        fwd_b <= 64'b0    ;
        pwd_a <= 19'b0    ;
        pwd_b <= 19'b0    ;
    end
    else if (out_flag && channel_a) begin
        fwd_a <= (1441151880*read_fout) >> 24  ;  //(2882303761*read_fout)>>25
        pwd_a <= read_pout                     ;
        fwd_b <= fwd_b                         ;
        pwd_b <= pwd_b                         ;
    end
    else if (out_flag && channel_b) begin
        fwd_a <= fwd_a                         ;
        pwd_a <= pwd_a                         ;
        fwd_b <= (1441151880*read_fout) >> 24  ;  //(2882303761*read_fout)>>25
        pwd_b <= read_pout                     ;
    end
    else begin            
        fwd_a <= fwd_a  ;
        pwd_a <= pwd_a  ;
        fwd_b <= fwd_b  ;
        pwd_b <= pwd_b  ;
    end 
end

endmodule

说明:
根据DDS中输出频率以及频率控制字的计算关系可得(详见《FPGA自学笔记——设计与验证》第294页):
F o u t = B ∗ F c l k / 2 n F_{out} = B*F_{clk}/2^n Fout=BFclk/2n
其中,n = 32,Fout是输出的频率值,B为频率控制字,在此verilog文件中,用fwd表示。由于开发板使用的时钟频率为50MHz,故频率控制字B的计算方式为:
B = 2 32 ∗ F o u t / F c l k = 2 32 / 50 , 000 , 000 ∗ F o u t B = 2^{32} * F_{out} / F_{clk} = 2^{32}/50,000,000 * F_{out} B=232Fout/Fclk=232/50,000,000Fout

B = 85.89934592 ∗ F o u t B= 85.89934592 * F_{out} B=85.89934592Fout

而FPGA中是没有浮点小数运算的,也就是说,当输入85.89934592,FPGA会自动截断小数点,将其当做85进行计算,这样导致频率控制字的误差较大。采用如下方式能够避免精度不够的问题:

小数点之后的数89934592,通过计算可知,其二进制表示的数总共27位。

在这里插入图片描述

对89.89934592乘上227,既
89.89934592 ∗ 2 27 = 11529215046.06846976 89.89934592*2^{27} = 11529215046.06846976 89.89934592227=11529215046.06846976
保留整数11529215046,将频率控制字的计算方式变为
B = ( 2 27 ∗ 85.89934592 ∗ F o u t ) > > 27 B= (2^{27}*85.89934592 * F_{out}) >> 27 B=(22785.89934592Fout)>>27
B = ( 11529215046 ∗ F o u t ) > > 27 B= (11529215046* F_{out}) >> 27 B=(11529215046Fout)>>27

这样能很好地避免由于小数点的出现而造成的精度损失,不过缺点便是本来应该为32位的fwd需要扩容成64位。另外,考虑到89.89934592 *227对应的整数,其二进制位数大于32位,故需要调整乘数因子,变为225;而89.89934592 * 227= 2882303761;同时
在这里插入图片描述可见,该数对应的二进制正好是32位,且最高位为1,在Modelsim仿真中会报警告,即Modelsim会将这个二进制数当做有符号数处理,故再次改变乘数因子,调整为224。最终
B = ( 2 24 ∗ 85.89934592 ∗ F o u t ) > > 24 B= (2^{24}*85.89934592 * F_{out}) >> 24 B=(22485.89934592Fout)>>24

B = ( 1441151880 ∗ F o u t ) > > 24 B= (1441151880 * F_{out}) >> 24 B=(1441151880Fout)>>24
所以,对应的verilog代码为
fwd_a <= (1441151880*read_fout) >> 24 ;
fwd_b <= (1441151880*read_fout) >> 24 ;

4. testbench测试

/*=============================================================================
#     FileName: key_FSM_tb.v
#         Desc: 
#       Author: ohliver
#        Email: ohliver@foxmail.com
#     HomePage: https://blog.csdn.net/qq_15062763
#      Version: 0.0.1
#   LastChange: 2020-02-03 20:58:48
#      History:
=============================================================================*/
`timescale 1ns/1ns
`define p 20

module key_FSM_tb;

reg clk_50M ;
reg rstn    ;

reg [15:0] in_addr  ;
reg [15:0] in_data  ;
reg        in_flag  ;

wire [63:0] fwd_a   ;
wire [63:0] fwd_b   ;
wire [31:0] pwd_a   ;
wire [31:0] pwd_b   ;
wire        out_flag;

initial         clk_50M = 1'b1      ;
always #(`p/2)  clk_50M = ~clk_50M  ;

parameter CHANNEL_A = 5'd10;  //button CH-
parameter CHANNEL_B = 5'd12;  //button CH+
parameter PHASE     = 5'd11;  //button CH
parameter DELETE    = 5'd16;  //button -
parameter OK        = 5'd18;  //button EQ 
initial begin
    rstn    <=  1'b0;
    in_flag <=  1'b0;
    in_addr <= 16'b0;
    in_data <= 16'b0;

    #(`p*10); 
    rstn <= 1'b1;


//=======================================
//通道A输出fwd=1000Hz,pwd=256
//channel_A
    press_HT6221(CHANNEL_A);
//frequency = 1000Hz
    press_HT6221(1 );   
    press_HT6221(0 );   
    press_HT6221(0 );   
    press_HT6221(0 );   
//phase = (256/4096)*2*pi 
    press_HT6221(PHASE);
    press_HT6221(2    );   
    press_HT6221(5    );   
    press_HT6221(6    );   
//OK
    press_HT6221(OK);
	#(`p*3000);

//======================================
//通道B输出fwd=100Hz,pwd=25
    press_HT6221(CHANNEL_B);
//frequency = 100Hz
    press_HT6221(1 );   
    press_HT6221(0 );   
    press_HT6221(0 );   
    press_HT6221(0 );
    press_HT6221(DELETE);
    
//phase = (25/4096)*2*pi 
    press_HT6221(PHASE);
    press_HT6221(2    );   
    press_HT6221(5    );   
    press_HT6221(6    );

    press_HT6221(DELETE);
	 
//OK
    press_HT6221(OK);
    #(`p*3000);

//=====================================
//通道A输出fwd=10Hz,pwd=2
    press_HT6221(CHANNEL_B);
//frequency = 10Hz
    press_HT6221(1 );   
    press_HT6221(0 );   
    press_HT6221(0 );   
    press_HT6221(0 );
    press_HT6221(DELETE);
	press_HT6221(DELETE);
	
//phase = (2/4096)*2*pi 
    press_HT6221(PHASE);
	press_HT6221(DELETE);
    press_HT6221(2    );   
    press_HT6221(5    );   
    press_HT6221(6    );

    press_HT6221(DELETE);
	press_HT6221(DELETE);
	 
//OK
    press_HT6221(OK);
    #(`p*3000);
    
//===============================
//什么都不输入,直接按OK键
	 press_HT6221(CHANNEL_B);
	 press_HT6221(OK);
	 #(`p*3000);

//===============================
//什么都不输入,直接按回退键
	 press_HT6221(CHANNEL_B);
	 press_HT6221(DELETE);
	 #(`p*3000);

	 $stop;
end


task press_HT6221(input [4:0] key_board);
    begin
        in_flag <= 1'b1;
        #(`p);
        in_flag <= 1'b0;
        
        LUT(key_board);
		#(`p*100); 
		//20ns *100 = 2us, 
        //waiting 2us and then 
	    //push another key
    end
endtask

    
task LUT (input [4:0] key_board_r);
     begin
        case(key_board_r)
            5'd0 : {in_data, in_addr} <= 32'hE916FF00;
            5'd1 : {in_data, in_addr} <= 32'hF30CFF00;
            5'd2 : {in_data, in_addr} <= 32'hE718FF00;
            5'd3 : {in_data, in_addr} <= 32'hA15EFF00;
            5'd4 : {in_data, in_addr} <= 32'hF708FF00;
            5'd5 : {in_data, in_addr} <= 32'hE31CFF00;
            5'd6 : {in_data, in_addr} <= 32'hA55AFF00;
            5'd7 : {in_data, in_addr} <= 32'hBD42FF00;
            5'd8 : {in_data, in_addr} <= 32'hAD52FF00;
            5'd9 : {in_data, in_addr} <= 32'hB54AFF00;
            5'd10: {in_data, in_addr} <= 32'hBA45FF00;
            5'd11: {in_data, in_addr} <= 32'hB946FF00;
            5'd12: {in_data, in_addr} <= 32'hB847FF00;
            5'd13: {in_data, in_addr} <= 32'hBB44FF00;
            5'd14: {in_data, in_addr} <= 32'hBF40FF00;
            5'd15: {in_data, in_addr} <= 32'hBC43FF00;
            5'd16: {in_data, in_addr} <= 32'hF807FF00;
            5'd17: {in_data, in_addr} <= 32'hEA15FF00;
            5'd18: {in_data, in_addr} <= 32'hF609FF00;
            5'd19: {in_data, in_addr} <= 32'hE619FF00;
            5'd20: {in_data, in_addr} <= 32'hF20DFF00;
            
            default: {in_data, in_addr} <= 32'h0;
        endcase
    end
endtask

key_FSM key_FSM(
	.clk_50M 	 (clk_50M),
	.rstn		 (rstn	 ),
	.in_addr     (in_addr),
	.in_data	 (in_data),
    .in_flag     (in_flag),
	
	.fwd_a        (fwd_a       ),
	.fwd_b        (fwd_b       ),
	.pwd_a        (pwd_a       ),
	.pwd_b        (pwd_b       ),
    .out_flag     (out_flag    )
);


endmodule

经过调试,状态跳转以及数据输出都符合预期,是作此篇。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值