快速傅里叶变换的原理


⭐️作者简介:小瑞同学,主要学习FPGA、信号处理、通信等。
🍎个人主页:小瑞同学的博客主页
🌻个人信条:越努力,越幸运!
⏰日期:2024.1.14
📖文章内容概述:介绍了FFT蝶形算法的基本原理,并对FFT进行了公式推导过程分析,最后给出了蝶形算法的编程思路参考代码


1.FFT概述

📄快速傅里叶变换(FFT)是于1965年由J.W.Cooley和T.W.Tukey提出的一种计算离散傅里叶变换(DFT)的高效算法。
📄FFT主要利用旋转因子的周期性、对称性和缩放性实现蝶形迭代运算,大幅降低了DFT的计算量。具体而言,如果信号长度为N,那么计算DFT的时间复杂度为 O ( N 2 ) O(N^2) O(N2),而计算FFT的时间复杂度仅为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)。这可以说是非常大的改进,而且N越大,FFT算法的优越性就越显著
📄FFT极大地推动了计算机或数字系统中离散傅里叶变换的应用,对数字信号处理的发展具有重要的意义,下面重点介绍一下FFT算法。

2.FFT蝶形算法原理

2.1 公式推导

📄首先来回忆一下N点DFT的计算公式:

X ( k ) = ∑ n = 0 N − 1 x ( n ) W N n k X(k)=\sum\limits_{n=0}^{N-1}x(n)W_N^{nk} X(k)=n=0N1x(n)WNnk,其中 W N n k = e − j 2 π N n k W_N^{nk}=e^{-j\frac{2\pi}{N}nk} WNnk=ejN2πnk,称为旋转因子。

📄在推导之前,我们还需要先明确旋转因子的特性:

旋转因子的特性:

  • 周期性: W N a = W N a + N W_N^{a}=W_N^{a+N} WNa=WNa+N
  • 对称性: W N a + N 2 = − W N a W_N^{a+\frac{N}{2}}=-W_N^a WNa+2N=WNa
  • 缩放性: W N a = W N m a m W_N^a=W_{\frac{N}{m}}^{\frac{a}{m}} WNa=WmNma

📄对于N点输入信号 x ( n ) x(n) x(n),可以将其分为奇偶两部分:

x ( n ) = { x ( 2 m ) x = 0 , 1 , 2... N 2 − 1 x ( 2 m + 1 ) x = 0 , 1 , 2... N 2 − 1 x(n)=\begin{cases}x(2m)&x=0,1,2...\frac{N}{2}-1\\x(2m+1)&x=0,1,2...\frac{N}{2}-1\end{cases} x(n)={x(2m)x(2m+1)x=0,1,2...2N1x=0,1,2...2N1

📄那么对应地,对于N点DFT,我们总是可以把它分成两部分:

X ( k ) = ∑ m = 0 N 2 − 1 x ( 2 m ) W N 2 m k + ∑ m = 0 N 2 − 1 x ( 2 m + 1 ) W N ( 2 m + 1 ) k , k = 0 , 1... N − 1 X(k)=\sum\limits_{m=0}^{\frac{N}{2}-1}x(2m)W_N^{2mk}+\sum\limits_{m=0}^{\frac{N} {2}-1}x(2m+1)W_N^{(2m+1)k},k=0,1...N-1 X(k)=m=02N1x(2m)WN2mk+m=02N1x(2m+1)WN(2m+1)k,k=0,1...N1

📄利用旋转因子的缩放性,可以得到:

X ( k ) = ∑ m = 0 N 2 − 1 x ( 2 m ) W N 2 m k + W N k ∑ m = 0 N 2 − 1 x ( 2 m + 1 ) W N 2 m k , k = 0 , 1... N − 1 X(k)=\sum\limits_{m=0}^{\frac{N}{2}-1}x(2m)W_{\frac{N}{2}}^{mk}+W_N^k\sum\limits_{m=0}^{\frac{N} {2}-1}x(2m+1)W_{\frac{N}{2}}^{mk},k=0,1...N-1 X(k)=m=02N1x(2m)W2Nmk+WNkm=02N1x(2m+1)W2Nmk,k=0,1...N1

📄可以写成:

X ( k ) = X e v e n ( k ) + W N k X o d d ( k ) , k = 0 , 1... N − 1 X(k)=X_{even}(k)+W_N^kX_{odd}(k),k=0,1...N-1 X(k)=Xeven(k)+WNkXodd(k)k=0,1...N1

📄这样看来,我们就可以把DFT的计算分解为对输入信号的奇偶部分分别进行DFT。但是这里有一个问题,如果将输入信号分成奇偶两部分的话,长度是减半的,这两部分 k k k的取值应该都为 0 , 1... N / 2 − 1 0,1...N/2-1 0,1...N/21,所以为了正确迭代,还需要对式子作进一步推导。

📄对于前 N / 2 N/2 N/2个点:

X ( k ) = X e v e n ( k ) + W N k X o d d ( k ) , k = 0 , 1... N / 2 − 1 X(k)=X_{even}(k)+W_N^kX_{odd}(k),k=0,1...N/2-1 X(k)=Xeven(k)+WNkXodd(k)k=0,1...N/21

📄对于后 N / 2 N/2 N/2个点:

X ( k + N 2 ) = X e v e n ( k + N 2 ) + W N k + N 2 X o d d ( k + N 2 ) , k = 0 , 1... N / 2 − 1 X(k+\frac{N}{2})=X_{even}(k+\frac{N}{2})+W_N^{k+\frac{N}{2}}X_{odd}(k+\frac{N}{2}),k=0,1...N/2-1 X(k+2N)=Xeven(k+2N)+WNk+2NXodd(k+2N)k=0,1...N/21

📄利用旋转因子的周期性和对称性,可以得到:

X ( k + N 2 ) = X e v e n ( k ) − W N k X o d d ( k ) , k = 0 , 1... N / 2 − 1 X(k+\frac{N}{2})=X_{even}(k)-W_N^kX_{odd}(k),k=0,1...N/2-1 X(k+2N)=Xeven(k)WNkXodd(k)k=0,1...N/21

📄至此,我们有:

{ X ( k ) = X e v e n ( k ) + W N k X o d d ( k ) k = 0 , 1... N / 2 − 1 X ( k + N 2 ) = X e v e n ( k ) − W N k X o d d ( k ) k = 0 , 1... N / 2 − 1 \begin{cases}X(k)=X_{even}(k)+W_N^kX_{odd}(k)&k=0,1...N/2-1\\X(k+\frac{N}{2})=X_{even}(k)-W_N^kX_{odd}(k)&k=0,1...N/2-1\end{cases} {X(k)=Xeven(k)+WNkXodd(k)X(k+2N)=Xeven(k)WNkXodd(k)k=0,1...N/21k=0,1...N/21

📄所以,一个 N N N点的DFT结果可以由两个 N / 2 N/2 N/2点的奇偶输入的DFT结果计算得到。然后 N / 2 N/2 N/2个点的DFT又可以由两个 N / 4 N/4 N/4点的DFT结果得到,依此类推,一直迭代下去,直到最后细化为两个点的运算,这时就很简便了。
📄蝶形算法的本质就是利用旋转因子的特性,来减少DFT中的重复计算。它的主要思想是分治求解,即不断地将原计算分解为两个子计算,直至最简。

2.2 过程分析

📄当然,实际应用和推导过程是相反的,由最开始两个点的计算,逐步进行下去得到N点DFT结果,以16点DFT为例,过程如下图所示。

在这里插入图片描述

📄可以看到,在第一级运算中,输入数据并不是顺序输入的。这是因为我们将N个点不断地分为奇偶两部分,经过这样的过程才得到最后的两个数。

对于16点DFT来说,划分过程:
第一次分:0,2,4,6,8,10,12,14;1,3,5,7,9,11,13,15。
第二次分:0,4,8,12;2,6,10,14;1,5,9,13;3,7,11,15。
第三次分:0,8;4,12;2,10;6,14;1,9;5,13;3,11;7,15。

📄这体现在二进制上就是将顺序数的高低位对称交换,得到的就是对应位置的倒序数。

倒序:
如5(0101),将其倒序得到1010,也就是10,对应于第6个位置的输入。

📄那旋转因子又是怎么回事呢?实际上,在我们分解的过程中,由于点数N是不断减小的,所以旋转因子也是在不断变化的。为了减少计算,利用旋转因子的缩放性,将下标统一为N。

第一次分: W N k , k = 0 , 1... N / 2 − 1 W_N^k,k=0,1...N/2-1 WNk,k=0,1...N/21
第二次分: W N / 2 k = W N 2 k , k = 0 , 1... N / 4 − 1 W_{N/2}^k=W_N^{2k},k=0,1...N/4-1 WN/2k=WN2k,k=0,1...N/41
第三次分: W N / 4 k = W N 4 k , k = 0 , 1... N / 8 − 1 W_{N/4}^k=W_N^{4k},k=0,1...N/8-1 WN/4k=WN4k,k=0,1...N/81
依此类推…

3.编程思路

这里介绍一下如何使用MATLAB实现蝶形算法,关键在于思路。

3.1 倒序算法

📄在蝶形算法的第一级,要先将输入进行倒序,原理并不难,但是如何写代码就需要思考一下了。
📄笔者最初写倒序的代码时,思路是由顺序数得到对应的倒序数,但是遇到些困难。这时不妨换一个思路,顺序数都是由0开始不断加1得到的,也就是说下一个顺序数可以由上一个顺序数得到。那么可以联想到,下一个逆序数也可以由上一个逆序数得到,而最初的逆序数同样也是0。
📄那么结论就是:顺序数二进制序列下一个数是由上一个数在最低位加1向高位进位得到,逆序数二进制序列下一个数是由上一个数在最高位加1向低位进位得到。
由此得到设计思路:

算法描述:在N个数中,若已知某数的倒序数是J,求下一个倒序数。

  • 判断J的最高位是否为0,可以通过与K=N/2进行比较(因为N位二进制其最高位的权值是N/2,如果最高位为0,它的值是一定小于N/2的)
  • ①如果 K > J K>J K>J,说明最高位为0,应把该位变为1,即J+K,这样就得到倒序数了。
  • ②如果 K ⩽ J K\leqslant J KJ,说明最高位为1,应该把该位变为0,即J-K,然后还需要再判断此高位;次高位的权值是N/4,令K=K/2=N/4,还是先判断①,如果成立,直接得到逆序数;如果不成立,再执行②,依此类推。

3.2 确定关键变量

📄在蝶形算法中,很多重要的变量确定后才能计算,如旋转因子的值,运算对象的位置等。这些都可以通过观察蝶形算法的结构图,找出一定的规律得到。

对于N点FFT:

  • M = l o g 2 N M=log_2^N M=log2N级蝶形运算
  • 第m级进行蝶形运算的两个点之间的距离是 2 m − 1 2^{m-1} 2m1
  • 如果将每级操作相同的部分归为一组,那么第m级每组有 2 m − 1 2^{m-1} 2m1个蝶形运算
  • 第m级有 P = 2 m − 1 − 1 P=2^{m-1}-1 P=2m11个不同的旋转因子
  • 第m级每组旋转因子的上标依次是 n ∗ 2 M − m , n = 0 , 1... P n*2^{M-m},n=0,1...P n2Mm,n=0,1...P
  • 第m级第一组的蝶形运算单元左上角的点的索引是n,各组旋转因子相同的蝶形单元左上角点的距离是 2 m 2^m 2m

3.3 MATLAB程序

3.3.1 代码

📄明晰以上所述后,可以写出程序了,代码如下:

clc;
clear;
close all;


f1=100;
fs=1000;
t=0:1/fs:16/fs-1/fs;
xn=cos(2*pi*f1*t);%测试信号

N=length(xn);%信号长度
M=log2(N);%蝶形运算级数&&N的二进制位数

f=(-N/2:N/2-1)*fs/N;
%倒序
order_reverse=zeros(1,N);
for i=2:N
    K=N/2;
    J=order_reverse(i-1);
        for j=1:M
            if(J<K)%如果最高位为0
                J=J+K;%直接将最高位置1
                order_reverse(i)=J;%得到下一个倒序数
                break;
            else   %如果最高位为1
                J=J-K;%将最高位置0
                K=K/2;%继续判断次高位,依此类推
            end
        end
end

dout=zeros(1,N);
dout=xn(order_reverse+1);%每一级的输出信号

for m=1:M %第m级蝶形运算
    distance=2^(m-1); %进行蝶形运算的两个点之间的距离&&每组蝶形运算的个数
    P=distance-1;%每一级有distance个不同的旋转因子
    for n=0:P%每组第n个旋转因子&&每组第n个蝶形运算
        nk=n*(2^(M-m));%旋转因子右上角的取值:前一级是后一级乘2并只取前P个
        W=exp(-1i*2*pi*nk/N); %旋转因子计算
        for k=n+1:2^m:N %找到各组相同旋转因子的蝶形运算单元左上角的点
            t1=dout(k);
            t2=dout(k+distance);
            dout(k)=t1+t2*W; %蝶形运算
            dout(k+distance)=t1-t2*W;           
        end
    end
end
subplot(121);
plot(f,abs(fftshift(fft(xn))));
title('使用fft函数得出的FFT');
subplot(122);
plot(f,abs(fftshift(dout)));
title('使用蝶形算法得出的FFT');

3.3.1 结果

📄可以看到,使用MATLAB的fft函数和使用蝶形算法得到的结果基本一致,验证了程序的正确性。

在这里插入图片描述

4.蝶形算法在verilog中的实现

📄这里笔者只是采用了比较笨拙的实现方式,即编写一个蝶形运算模块,模块的输入变量中有旋转因子的值,在顶层模块中重复地调用蝶形运算模块。更细致的程序,可参考: 菜鸟教程FFT设计
📄对于一个蝶形运算单元,设输入的两个数分别为 d i n I 0 + j d i n Q 0 、 d i n I 1 + j d i n Q 1 dinI0+jdinQ0、dinI1+jdinQ1 dinI0+jdinQ0dinI1+jdinQ1,蝶形因子为 a + j b a+jb a+jb,输出为 d o u t I 0 + j d o u t Q 0 、 d o u t I 1 + j d o u t Q 1 doutI0+jdoutQ0、doutI1+jdoutQ1 doutI0+jdoutQ0doutI1+jdoutQ1,经蝶形运算可得

d o u t I 0 = d i n I 0 + a ∗ d i n I 1 − b ∗ d i n Q 1 doutI0=dinI0+a*dinI1-b*dinQ1 doutI0=dinI0+adinI1bdinQ1
d o u t Q 0 = d i n Q 0 + b ∗ d i n I 1 + a ∗ d i n Q 1 doutQ0=dinQ0+b*dinI1+a*dinQ1 doutQ0=dinQ0+bdinI1+adinQ1
d o u t I 1 = d i n I 0 − a ∗ d i n I 1 + b ∗ d i n Q 1 doutI1=dinI0-a*dinI1+b*dinQ1 doutI1=dinI0adinI1+bdinQ1
d o u t Q 1 = d i n Q 0 − a ∗ d i n Q 1 − b ∗ d i n I 1 doutQ1=dinQ0-a*dinQ1-b*dinI1 doutQ1=dinQ0adinQ1bdinI1

📄那么我们可以将该过程分三步进行:

1.对应变量相乘
2.相加
3.对中间运算结果截位

蝶形运算模块的代码:


module rotate
#(  parameter DATA_WIDTH=12,
    parameter ROTATE_WIDTH=17,
    parameter EXPAND_WIDTH=16)
(
    input                clk        ,
    input                rstn       ,
    input  signed [DATA_WIDTH-1:0] din_I0     ,
    input  signed [DATA_WIDTH-1:0] din_Q0     ,
    input  signed [DATA_WIDTH-1:0] din_I1     ,
    input  signed [DATA_WIDTH-1:0] din_Q1     ,
    input  signed [ROTATE_WIDTH-1:0] Wn_real    ,
    input  signed [ROTATE_WIDTH-1:0] Wn_imag    ,
    input                s_valid0   ,
    input                s_valid1   ,
    output signed [DATA_WIDTH-1:0] dout_I0    ,
    output signed [DATA_WIDTH-1:0] dout_Q0    ,
    output signed [DATA_WIDTH-1:0] dout_I1    ,
    output signed [DATA_WIDTH-1:0] dout_Q1    ,
    output               m_valid0   ,
    output               m_valid1
);
//寄存输出数据
reg [DATA_WIDTH-1:0] dout_I0_r;
reg [DATA_WIDTH-1:0] dout_Q0_r;
reg [DATA_WIDTH-1:0] dout_I1_r;
reg [DATA_WIDTH-1:0] dout_Q1_r;
//寄存截位前的数据
reg [DATA_WIDTH+ROTATE_WIDTH:0] dout_I0_r_r;
reg [DATA_WIDTH+ROTATE_WIDTH:0] dout_Q0_r_r;
reg [DATA_WIDTH+ROTATE_WIDTH:0] dout_I1_r_r;
reg [DATA_WIDTH+ROTATE_WIDTH:0] dout_Q1_r_r;
//相加过程控制信号
reg s_valid0_r;
reg s_valid1_r;
//截位过程控制信号
reg s_valid0_r_r;
reg s_valid1_r_r;
//寄存输出有效信号
reg m_valid0_r;
reg m_valid1_r;
//相乘的结果
reg [DATA_WIDTH+ROTATE_WIDTH-1:0] dinI1xWn_real;//输入12位,旋转因子15位
reg [DATA_WIDTH+ROTATE_WIDTH-1:0] dinI1xWn_imag;
reg [DATA_WIDTH+ROTATE_WIDTH-1:0] dinQ1xWn_real;
reg [DATA_WIDTH+ROTATE_WIDTH-1:0] dinQ1xWn_imag;
//I0和Q0扩大的结果
reg [DATA_WIDTH+EXPAND_WIDTH-1:0] din_I0_expand;//输入12位,扩大2^13(14位)
reg [DATA_WIDTH+EXPAND_WIDTH-1:0] din_Q0_expand;

//第一步:相乘
always@(posedge clk or negedge rstn)begin
    if(!rstn)begin
       dinI1xWn_real<='b0;
       dinI1xWn_imag<='b0;
       dinQ1xWn_real<='b0;
       dinQ1xWn_imag<='b0;
       din_I0_expand<='b0;
       din_Q0_expand<='b0; 
       s_valid0_r<='b0;
       s_valid1_r<='b0;
    end
    else if(s_valid0&s_valid1)begin
        s_valid0_r<=s_valid0;
        s_valid1_r<=s_valid1;
        din_I0_expand<={din_I0[DATA_WIDTH-1],din_I0,{{EXPAND_WIDTH-1}{1'b0}}};
        din_Q0_expand<={din_Q0[DATA_WIDTH-1],din_Q0,{{EXPAND_WIDTH-1}{1'b0}}};
        dinI1xWn_real<=din_I1*Wn_real;
        dinI1xWn_imag<=din_I1*Wn_imag;
        dinQ1xWn_real<=din_Q1*Wn_real;
        dinQ1xWn_imag<=din_Q1*Wn_imag;
    end
    else begin
        s_valid0_r<='b0;
        s_valid1_r<='b0;
    end   
end
//第二步:相加
always@(posedge clk or negedge rstn)begin
    if(!rstn)begin
        dout_I0_r_r<='b0;
        dout_Q0_r_r<='b0;
        dout_I1_r_r<='b0;
        dout_Q1_r_r<='b0;
        s_valid0_r_r<='b0;
        s_valid1_r_r<='b0;
    end
    else if(s_valid0_r&s_valid1_r)begin
        s_valid0_r_r<=s_valid0_r;
        s_valid1_r_r<=s_valid1_r;
        dout_I0_r_r<={{2{din_I0_expand[DATA_WIDTH+EXPAND_WIDTH-1]}},din_I0_expand}+{{dinI1xWn_real[DATA_WIDTH+ROTATE_WIDTH-1]},dinI1xWn_real}-{{dinQ1xWn_imag[DATA_WIDTH+ROTATE_WIDTH-1]},dinQ1xWn_imag};
        dout_Q0_r_r<={{2{din_Q0_expand[DATA_WIDTH+EXPAND_WIDTH-1]}},din_I0_expand}+{{dinI1xWn_imag[DATA_WIDTH+ROTATE_WIDTH-1]},dinI1xWn_imag}+{{dinQ1xWn_real[DATA_WIDTH+ROTATE_WIDTH-1]},dinQ1xWn_real};
        dout_I1_r_r<={{2{din_I0_expand[DATA_WIDTH+EXPAND_WIDTH-1]}},din_I0_expand}-{{dinI1xWn_real[DATA_WIDTH+ROTATE_WIDTH-1]},dinI1xWn_real}+{{dinQ1xWn_imag[DATA_WIDTH+ROTATE_WIDTH-1]},dinQ1xWn_imag};
        dout_Q1_r_r<={{2{din_Q0_expand[DATA_WIDTH+EXPAND_WIDTH-1]}},din_I0_expand}-{{dinI1xWn_imag[DATA_WIDTH+ROTATE_WIDTH-1]},dinI1xWn_imag}-{{dinQ1xWn_real[DATA_WIDTH+ROTATE_WIDTH-1]},dinQ1xWn_real};
    end
    else begin
       s_valid0_r_r<='b0;
       s_valid1_r_r<='b0; 
    end
end
//第三步:截位
always@(posedge clk or negedge rstn)begin
    if(!rstn)begin
       m_valid0_r<='b0;
       m_valid1_r<='b0; 
       dout_I0_r<='b0;
       dout_Q0_r<='b0;
       dout_I1_r<='b0;
       dout_Q1_r<='b0;
    end
    else if(s_valid0_r_r&s_valid1_r_r)begin
        m_valid0_r<=s_valid0_r_r;
        m_valid1_r<=s_valid1_r_r;
        dout_I0_r<=dout_I0_r_r[DATA_WIDTH+ROTATE_WIDTH:ROTATE_WIDTH+1]+dout_I0_r_r[ROTATE_WIDTH];
        dout_Q0_r<=dout_Q0_r_r[DATA_WIDTH+ROTATE_WIDTH:ROTATE_WIDTH+1]+dout_Q0_r_r[ROTATE_WIDTH];
        dout_I1_r<=dout_I1_r_r[DATA_WIDTH+ROTATE_WIDTH:ROTATE_WIDTH+1]+dout_I1_r_r[ROTATE_WIDTH];
        dout_Q1_r<=dout_Q1_r_r[DATA_WIDTH+ROTATE_WIDTH:ROTATE_WIDTH+1]+dout_Q1_r_r[ROTATE_WIDTH];
    end
    else begin
       m_valid0_r<='b0;
       m_valid1_r<='b0; 
    end
end

assign dout_I0=dout_I0_r;
assign dout_Q0=dout_Q0_r;
assign dout_I1=dout_I1_r;
assign dout_Q1=dout_Q1_r;
assign m_valid0=m_valid0_r;
assign m_valid1=m_valid1_r;

endmodule

说明:

  • 由于在硬件中实现,所以旋转因子需要进行一定倍数的扩大,而旋转因子扩大的同时,dinI0和dinQ0也需要相同尺度的扩大,以保证结果的正确性。
  • 旋转因子扩大后的值直接作为输入变量,该值在顶层模块中给出,可通过matlab计算。
  • 截位是截取中间运算结果的高DATA_WIDTH位 ,既能保证数据位宽不至过大,又能确保前后调用连贯。而且这里可以再加一个低位,能够提高精度。

📄由于顶层模块程序比较冗长,这里就不贴出来了,主要就是按照流程图进行例化。


❤️如果觉得文章对你有所帮助的话,别忘了点个收藏哦~
❤️更多优质内容可浏览本人主页👇,期待再次与你相遇!
🎉🎉🎉🎉🎉🎉小瑞同学的博客主页🎉🎉🎉🎉🎉🎉

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hi小瑞同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值