初识MIMO-OFDM(三):完整的MIMO-OFDM仿真

零.缘起

MIMO-OFDM的仿真是一个大的不能再大的话题了,写得好的仿真平台是可以赚大钱的,我后续也会更新一个著名仿真平台的学习博客,不过我们这里先来学习一下陈老湿在通信家园看到的这个仿真https://zhuanlan.zhihu.com/p/392827532。这个博客主要是希望梳理每一个函数,然后给出2*2Alamouti+OFDM完整的仿真流程。
代码地址:https://github.com/liu-zongxi/MIMO_OFDM_simulation

发射机

一. 发射机步骤一:生成所需要发送的数据

%-----------------------生成比特-----------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月5日14点33分-----------------%
function Frame_bit = FrameBitGenerator(N_data, N_user, N_mod, N_symbol)
% 输入
% N_data:一个符号中要发送的符号数
% N_user:用户数
% N_mod:调制数
% N_symbol:一帧中有多少个符号
% 输出
% Frame_bit:一个(1,N_user)的结构体,包含data和num两个field,分别表示每个用户的数据和数据长度
    N_bit_per_symbol = N_mod * N_data;
    % 可以选择用户比特分布为:
    % 1) 所有用户比特相同。 
    N_bit_per_user_per_symbol = repmat(N_bit_per_symbol/N_user,1,N_user);

    % 2) 用户按照比例 (1:N_user)/((1 + N_user)*N_user/2) 发送比特数据
    % 目的是为了仿真不同用户的信息比特长度不同的情况,也可以修改得到其他的用户数据比例
    % N_bit_per_user_per_symbol = round([1:N_user]/((1 + N_user)*N_user/2) * N_bit_per_symbol);
    N_bit_per_user = N_bit_per_user_per_symbol * N_symbol;
    for iuser = 1:N_user
        % frame_bit_temp{iuser} = rand(N_bit_per_user(iuser), 1) > 0.5;
        Frame_bit(iuser).data = rand(N_bit_per_user(iuser), 1) > 0.5;
        Frame_bit(iuser).num = N_bit_per_user(iuser);
    end
end

该函数主要是生成所需要发送的数据,这里是为了扩展性用了结构体,实际上该仿真根本无法处理不同长度的数据。

一个OFDM符号所承载的比特数是他用于data传输的子载波数*调制数

由于四个用户等分数据,因此每个用户收到数据个数都要被除以N_user

最后我们得到的数据形式是

(N_data*N_mod/N_user*N_symbol, N_user)

二.发射机步骤二:子载波分配

%-----------------------子载波分配-----------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月5日16点35分-----------------%
function [index_data_per_user, Frame_zero_padding]=SubcarrierAllocation(Frame_bit_coded, index_data, N_data, N_user,N_symbol, N_mod, type_alloc)
Frame_zero_padding = cell(1,N_user);
index_data_per_user = cell(1,N_user);
% 计算填零的个数
N_subcarrier_per_user = N_data / N_user;    % 平均分配,每个用户所被分配的子载波个数
for iuser = 1:N_user
    N_zero_padding = N_mod * N_subcarrier_per_user * N_symbol - Frame_bit_coded(iuser).num;
    % 子载波分配方式,这里只给出相邻分配
    if type_alloc == "neighbour"
        index_data_per_user{iuser} = index_data((iuser-1)*N_subcarrier_per_user + 1: iuser*N_subcarrier_per_user)';
    end
    Frame_zero_padding{iuser} = [Frame_bit_coded(iuser).data; zeros(N_zero_padding, 1)];
end

该函数还是装神弄鬼了,他输出的是针对步骤一所得到的(N_data*N_mod/N_user*N_symbol, N_user)的数据,每一列的每一个symbol使用编号多少的子载波进行发送?

原仿真里给出了多种分配方式,但我这里单纯是实现一下,就是按照顺序分配的

最终输出的index_data_per_user是一个(N_data/N_user,N_user)的矩阵

Frame_zero_padding是一个零填充帧,如果现实情况中不是像我们步骤一中恰好生成那么多比特呢?这时要用0把他填满

三.发射机步骤三:调制

%-----------------------调制函数---------------------%
%-----------------------author:lzx--------------------------%
%-----------------------date:2022年5月5日23点22分-----------------%
function Frame_mod = Modulator(Frame_zero_padding, index_data_per_user, N_data, N_user, N_symbol, N_mod, N_subcarrier)

Frame_mod = zeros(N_subcarrier, N_symbol);
N_subcarrier_per_user = N_data / N_user;
for iuser = 1:N_user
    for isymbol = 1:N_symbol
        L_symbol = N_subcarrier_per_user* N_mod;
        index_symbol = (isymbol-1)*L_symbol+1 : isymbol*L_symbol;
        Symbol = Frame_zero_padding{iuser}(index_symbol);
        Symbol_premod = reshape(Symbol, N_mod, N_subcarrier_per_user);
        Symbol_mod = SymbolModulator(Symbol_premod);
        Frame_mod(index_data_per_user{iuser}, isymbol) = Symbol_mod.';
    end
end
%-----------------------MIMO-OFDM主函数---------------------%
%-----------------------author:lzx--------------------------%
%-----------------------date:2022年5月5日10点29分-----------------%
function Symbol_mod = SymbolModulator(Symbol_premod)

N_mod = size(Symbol_premod,1);

switch N_mod
    % BPSK调制
    case 1
        matrix_mapping = [-1 1];
        Symbol_mod = matrix_mapping(Symbol_premod + 1);
    % QPSK调制
    case 2
        % 比特的映射关系,00:-3/4*pi,01:3/4*pi,10: -1/4*pi,11: 1/4*pi
        matrix_mapping = exp(1j*[-3/4*pi 3/4*pi -1/4*pi 1/4*pi]);
        index_mod = [2 1]*Symbol_premod;
        % 把输入比特映射为符号
        Symbol_mod = matrix_mapping(index_mod + 1);
    % 8PSK调制
    case 3
        matrix_mapping = exp(1j*[0  1/4*pi 3/4*pi 1/2*pi  -1/4*pi -1/2*pi pi -3/4*pi ]);
        index_mod = [4 2 1]*Symbol_premod ;
        % 把输入比特映射为符号
        Symbol_mod = matrix_mapping(index_mod + 1);
    % 16QAM调制
    case 4
        % 映射关系参见说明文档
        m=1;
        for k=-3:2:3
            for l=-3:2:3
                % 对符号能量进行归一化
                matrix_mapping(m) = (k+1j*l)/sqrt(10);
            m=m+1;
            end
        end
        matrix_mapping = matrix_mapping([0 1 3 2 4 5 7 6 12 13 15 14 8 9 11 10]+1);
        index_mod = [8 4 2 1]*Symbol_premod ;
        Symbol_mod = matrix_mapping(index_mod + 1);
    % 64QAM调制         
    case    6
        % 映射关系参见说明文档
        m=1;
        for k=-7:2:7
            for l=-7:2:7
                % 对符号能量进行归一化
                matrix_mapping(m) = (k+1j*l)/sqrt(42); 
            m=m+1;
            end
        end
        matrix_mapping = matrix_mapping(...
         [[ 0  1  3  2  7  6  4  5]...
         8+[ 0  1  3  2  7  6  4  5]... 
         24+[ 0  1  3  2  7  6  4  5]...
         16+[ 0  1  3  2  7  6  4  5]...
         56+[ 0  1  3  2  7  6  4  5]...
         48+[ 0  1  3  2  7  6  4  5]...
         32+[ 0  1  3  2  7  6  4  5]...
         40+[ 0  1  3  2  7  6  4  5]]+1);
        index_mod = [32 16 8 4 2 1]*Symbol_premod ;
        Symbol_mod = matrix_mapping(index_mod + 1);
end

该函数是一个调制函数

分为两部分,首先在Frame选出一个user一个symbol所需要发送的比特,改变形状后进行调制,调制后放入步骤二所获得的对应的子载波中。

进行调制输入比特为(N_mod, N_subcarrier_per_user),输出一个(1,N_subcarrier_per_user),然后反转后放入到对应子载波中

调制采用的是非常巧妙的方式,首先获得格雷码的映射,然后一一对应,可惜仔细学习一下该函数。

四.步骤四:Alamouti编码

%-----------------------STBC时空块码-----------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月5日15点52分-----------------%
function Frame_STBC = STBCCoding(Frame_mod, N_subcarrier, N_symbol, N_Tx)
Frame_STBC = zeros(N_subcarrier, N_symbol, N_Tx);
if (mod(N_symbol,N_Tx))
    error('空时编码器输入符号不匹配,子程序st_coding出错');
else
    for ispace = 1:N_symbol/N_Tx
        if N_Tx == 2
            X1=Frame_mod(:,(ispace-1)*N_Tx+1);%取第一列,即第一个OFDM符号
            X2=Frame_mod(:,(ispace-1)*N_Tx+2);%取第二列,即第二个OFDM符号
            Symbol_STBC = [X1 X2;-conj(X2) conj(X1)];%alamouti编码
        elseif N_Tx == 4
            X1=Frame_mod(:,(ispace-1)*N_Tx+1);%取第一列,即第一个OFDM符号
            X2=Frame_mod(:,(ispace-1)*N_Tx+2);%取第二列,即第二个OFDM符号
            X3=Frame_mod(:,(ispace-1)*N_Tx+3);%取第一列,即第一个OFDM符号
            X4=Frame_mod(:,(ispace-1)*N_Tx+4);%取第二列,即第二个OFDM符号
            Symbol_STBC = [ X1 X2 X3  X4;...
                            -X2 X1 -X4 X3;...
                            -X3 X4 X1 -X2;...
                            -X4 -X3 X2 X1;...
                            conj(X1) conj(X2) conj(X3) conj(X4);...
                            -conj(X2) conj(X1) -conj(X4) conj(X3);...
                            -conj(X3) conj(X4) conj(X1) -conj(X2);...
                            -conj(X4) -conj(X3) conj(X2) conj(X1)];
        end
        for iant = 1:N_Tx
            Symbol_STBC_per_ant = reshape(Symbol_STBC(:,iant), N_subcarrier, N_Tx);
            Frame_STBC(:, (ispace-1)*N_Tx+1:ispace*N_Tx, iant) = Symbol_STBC_per_ant;
        end
    end
end

这是一个标准的STBC编码,每次取出相邻的两个symbol的符号(同时包含导频和数据,就是一个完整的symbol)(因为这才满足Alamouti编码0时刻和T时刻的要求)。然后进行STBC的编码把原本(N_subcarrier,2)变为(2*N_subcarrier,2)。之后对于编码后的每一列,是由一根天线发送的,把每一列reshape成(N_subcarrier, 2)(这个2是因为2*2编码),放入总矩阵中,当每一列都摆放后,得到一个(N_subcarrier,2,2)的矩阵,这其中第二个2是由于这个2是因为2*2编码,第三个2是由于两根天线,其实是一样的。

当所有的symbol都被取出后,最后得到一个(N_subcarrier,N_symbol,2)的矩阵,这就是编码结果了。

从结果上来看,它将原本(N_subcarrier,N_symbol)个信号生成为了(N_subcarrier,N_symbol,2)个信号,给两个天线分别发送,这就是发射分集。

五.步骤五:OFDM调制

%-----------------------调制函数---------------------%
%-----------------------author:lzx--------------------------%
%-----------------------date:2022年5月6日21点11分-----------------%
function Frame_transmit = OFDMModulator(Frame_pilot, N_sym, N_subcarrier, N_symbol, N_Tx, N_GI)

Frame_transmit = zeros(1,N_sym*N_symbol,N_Tx);

for iant = 1:N_Tx
    % ifft乘sqrt(N_subc)以保证变换前后能量不变
    % 我们假设频域的样点是在[-fs/2  fs/2]中的, fs是采样频率
    % 使用fftshift函数目的是使得变换前的频域样点转换到[0 fs]中,以满足IFFT变换的要求
    Frame_TD = sqrt(N_subcarrier) * ifft( fftshift( Frame_pilot(:,:,iant), 1 ) );
    Frame_CP = Frame_TD(N_subcarrier - N_GI + 1:N_subcarrier ,:);
    Frame_withCP = [Frame_CP; Frame_TD];
    % 转换为串行信号
    Frame_transmit(:,:,iant) = reshape( Frame_withCP, 1, N_sym*N_symbol);
end

该函数是对信号进行OFDM调制,调制按照天线来进行,将(N_subcarrier,N_symbol)大小的数据在列这一维上做fftshift然后进行ifft,这样还会得到(N_subcarrier,N_symbol)的数据,然后加CP,CP的大小是(N_GI, N_symbol),叠在数据的头上,最后,发送时转换为串行信号,发送一个(1,(N_GI+N_subcarrier)*N_symbol)的信号,多跟天仙的结果是(1,(N_GI+N_subcarrier)*N_symbol,2).

这里为什么要做并串转换呢?实际上就是按顺序发送symbol,因为天线一次只能发送一个symbol呀~

image-20220509103538137

image-20220509103553174

信道

一.信道步骤一:加噪

这已经是老生常谈了

 Power_transmit = var(Frame_transmit);%发送信号功率
 N_noise = size(Frame_transmit,2);
 noise = NoiseGenerator(EbN0, Power_transmit, N_noise);
 Frame_noise = Frame_transmit+noise;
%-----------------------生成高斯噪声---------------------%
%-----------------------author:lzx--------------------------%
%-----------------------date:2022年5月6日22点43分-----------------%
function noise = NoiseGenerator(EbN0, Power_transmit, N_noise)
sigma = sqrt(Power_transmit/(2*EbN0));%标准差sigma
noise_real = randn(1,N_noise,2);
noise_real(:,:,1)=noise_real(:,:,1).*sigma(1);
noise_real(:,:,2)=noise_real(:,:,2).*sigma(2);
noise_imag = randn(1,N_noise,2);
noise_imag(:,:,1)=noise_imag(:,:,1).*sigma(1);
noise_imag(:,:,2)=noise_imag(:,:,2).*sigma(2);
noise=complex(noise_real,noise_imag);%复噪声序列

其实这个代码是比较愚蠢的,生成和发送信号等大的一个噪声矩阵,不详细说了

二.信道步骤二:信道

没有仿真的坐而论道,希望各位大佬指教

这个仿真里。。。。。居然没有信道模型,这其实是我最想知道的一个模型

目前对于信道的仿真,我们是割裂的。

在SISO-OFDM中,我们采用的是一个标准的TDL模型,他的大小是(1,Tau_max),然后进行卷积

在MIMO中,我们使用的是一个平坦信道,他是一个(N_Rx,NTx)的信道,直接进行相乘

那么在MIMO-OFDM中如何进行结合?因为每一个子载波都应该对应着一个子信道,信道的数量应该是(N_subcarrier, N_Tx*N_Rx);但这应该如何仿真呢?

接收机

首先要明确,接收机有N_user个

一. 步骤一:OFDM解调

%-----------------------OFDM的接收---------------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月5日16点35分-----------------%
function Frame_recieve = OFDMDemodulator(Frame_noise, N_sym, N_subcarrier,N_symbol,N_Rx, N_GI)
Frame_recieve = zeros(N_subcarrier,N_symbol,N_Rx);

for iant = 1:N_Rx
    Frame_symbol = reshape(Frame_noise(1,:,iant), N_sym, N_symbol);
    Frame_noGI = Frame_symbol(N_GI+1:end, :);
    % fft乘1/sqrt(N_subc)以保证变换前后能量不变
    % 我们假设频域的样点是在[-fs/2  fs/2]中的, fs是采样频率
    % fftshift目的是使得变换后的频域样点在[-fs/2  fs/2]中,而不是[0 fs]中
    Frame_recieve(:,:,iant) = fftshift(1/sqrt(N_subcarrier) * fft( Frame_noGI ), 1);
end
% Frame_recieve = Frame_FD(:, 1:N_symbol ,:);     % 数据OFDM符号, 包括导频符号

作为天线,自然应该按天线处理

接收到的信号重新串并转换为(N_sym, N_symbol),因为我们肯定是一个symbol一个symbol接收到的,去掉CP然后反做fft即可

最后得到的输出是(N_subcarrier,N_symbol),按天线接收后,得到的是(N_subcarrier,N_symbol,2)不过我这里存在疑问,接收端如何能分开发射端两根天线?我对此感到不解

二. 接收机二:STBC解码

%-----------------------STBC解码---------------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月7日10点07分-----------------%
function Frame_decoded = STBCDecoding(Frame_recieve, H, N_subcarrier, N_Tx,N_Rx, N_symbol)
Frame_decoded = zeros(N_subcarrier, N_symbol);
% 把H_freq转化为空时译码器的输入格式,为一个N_subc*N_ant_pair的矩阵,每列表示:
% 1-->1 ,1-->2,...,1-->N_Rx_ant, ... ,N_Tx_ant-->1, N_Tx_ant-->2,..., N_Tx_ant-->N_Rx_ant
if (N_Tx == 2)&&(N_Rx == 2)
    for ispace = 1:N_symbol/N_Tx
        % 构造输入进空时译码器的符号,用t表示时间号,a表示天线号,其格式为:
        % [Recv(t1,a1) Recv(t2,a1) Recv(t1,a2) Recv(t2,a2)].
        R = [];
        for iant = 1:N_Rx
            R = [R  Frame_recieve(:,(ispace-1)*N_Tx+1:ispace*N_Tx,iant) ];%对于2X2MIMO来说,R矩阵即为[Xe  -conj(Xo)  Xo  conj(Xe)]
        end
        H11=H(:,1);%1发送天线--》1接收天线信道参数
        H12=H(:,2);%1发送天线--》2接收天线信道参数
        H21=H(:,3);%2发送天线--》1接收天线信道参数
        H22=H(:,4);%2发送天线--》2接收天线信道参数
        R11=R(:,1);%1发送天线发送的第一个符号
        R12=R(:,2);%1发送天线发送的第二个符号
        R21=R(:,3);%2发送天线发送的第一个符号
        R22=R(:,4);%2发送天线发送的第二个符号
        for i=1:1:N_subcarrier
            X1(i,1)=(R11(i)*conj(H11(i))+conj(R12(i))*H21(i)+R21(i)*conj(H12(i))+conj(R22(i))*H22(i))/( H11(i)*conj(H11(i)) + H21(i)*conj(H21(i)) + H12(i)*conj(H12(i)) + H22(i)*conj(H22(i)));
            X2(i,1)=(R11(i)*conj(H21(i))-conj(R12(i))*H11(i)+R21(i)*conj(H22(i))-conj(R22(i))*H12(i))/( H11(i)*conj(H11(i)) + H21(i)*conj(H21(i)) + H12(i)*conj(H12(i)) + H22(i)*conj(H22(i)));
        end
        Sybmol_decoded = 2*[X1 X2]; % 非常扯淡,因为上面的分母是错误的
        Frame_decoded(:,(ispace-1)*N_Tx+1:ispace*N_Tx) = Sybmol_decoded;
    end
end

首先我要申明,我认为该仿真是错的Sybmol_decoded = 2*[X1 X2]; % 非常扯淡,因为上面的分母是错误的为什么莫名其妙的×2呢,因为上面( H11(i)*conj(H11(i)) + H21(i)*conj(H21(i)) + H12(i)*conj(H12(i)) + H22(i)*conj(H22(i)));是错误的,应该只选取其中两个。代码的实现更是丑陋,所以我也没有重写的欲望了。她也没有解决我想知道的信道的问题,

对于每两列进行一个操作,再加上两根天线,生成会原本的(N_subcarrier*2,2)的矩阵,然后解算。的总之他会把原本(N_subcarrier,N_symbol,2)的信号接收后得到N_subcarrier,N_symbol).

三.接收机步骤三:解调

%-----------------------QAM解调-----------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月7日11点51分-----------------%
function Frame_demod = Demodulator(Frame_decoded, index_data_iuser, N_mod, N_symbol)

Frame_demod = [];

for isymbol = 1:N_symbol
        Symbol_demod = SymbolDemodulator(Frame_decoded(index_data_iuser, isymbol).', N_mod);
        Frame_demod = [Frame_demod; Symbol_demod(:)];
end
%-----------------------每个符号的QAM解调-----------------%
%-----------------------author:lzx-------------------------%
%-----------------------date:2022年5月7日11点51分-----------------%
function Symbol_demod = SymbolDemodulator(Symbol, N_mod)

switch N_mod

    % BPSK解调
    case 1
        Symbol_demod = real(Symbol) > 0; 

    % QPSK解调
    case 2 
        % 由QPSK的星座图可以观察到
        bit0 = real(Symbol) ; 
        bit1 = imag(Symbol) ;
        
        % 得到2行, 列数为符号数的输出矩阵
        Symbol_demod(1,:) = bit0 > 0;
        Symbol_demod(2,:) = bit1 > 0;

    % 8PSK解调
    case 3
        % 参见8PSK的星座图
        bit0 = -imag( Symbol * exp(1j*pi/8)) ;
        % bit1和bit2解调,都需要进行星座旋转
        bit1 = -real(Symbol * exp(1j*pi/8)) ;
        
        bit2 = [];
        for isymbol = 1:length(Symbol)
            tmp = Symbol(isymbol) * exp(-1j*pi/8); 
            if ((real(tmp) <0) && (imag(tmp) >0)) || ((real(tmp) >0) && (imag(tmp) <0))
                bit2 = [bit2 0];
            else
                bit2 = [bit2 1];
            end   
        end
        
        Symbol_demod(1,:) = bit0 >0;
        Symbol_demod(2,:) = bit1 >0;
        Symbol_demod(3,:) = bit2 ;    % 已经硬判决
            % 16QAM解调   
    case    4
        bit0 = real(Symbol);
        bit2 = imag(Symbol);

        % 以bit1的生成来说明方法:
        % 2/sqrt(10) 为临界值, abs(real(sym))大于此, 则bit1为负,硬判决得到0 ; 反之为正
        bit1 = 2/sqrt(10)-(abs(real(Symbol)));
        bit3 = 2/sqrt(10)-(abs(imag(Symbol)));

        Symbol_demod(1,:) = bit0 > 0;
        Symbol_demod(2,:) = bit1 > 0;
        Symbol_demod(3,:) = bit2 > 0;
        Symbol_demod(4,:) = bit3 > 0;
            % 64QAM解调         
    case    6       
        bit0 = real(Symbol);
        bit3 = imag(Symbol);
        bit1 = 4/sqrt(42)-abs(real(Symbol));
        bit4 = 4/sqrt(42)-abs(imag(Symbol));
        for m=1:size(Symbol,2)
            for k=1:size(Symbol,1)
                if abs(4/sqrt(42)-abs(real(Symbol(k,m)))) <= 2/sqrt(42)  
                    bit2(k,m) = 2/sqrt(42) - abs(4/sqrt(42)-abs(real(Symbol(k,m))));
                elseif abs(real(Symbol(k,m))) <= 2/sqrt(42) 
                    bit2(k,m) = -2/sqrt(42) + abs(real(Symbol(k,m)));
                else
                    bit2(k,m) = 6/sqrt(42)-abs(real(Symbol(k,m)));
                end
      
                if abs(4/sqrt(42)-abs(imag(Symbol(k,m)))) <= 2/sqrt(42)  
                    bit5(k,m) = 2/sqrt(42) - abs(4/sqrt(42)-abs(imag(Symbol(k,m))));
                elseif abs(imag(Symbol(k,m))) <= 2/sqrt(42) 
                    bit5(k,m) = -2/sqrt(42) + abs(imag(Symbol(k,m)));
                else
                    bit5(k,m) = 6/sqrt(42)-abs(imag(Symbol(k,m)));
                end
            end
        end

        Symbol_demod(1,:) = bit0 > 0;
        Symbol_demod(2,:) = bit1 > 0;
        Symbol_demod(3,:) = bit2 > 0;
        Symbol_demod(4,:) = bit3 > 0;
        Symbol_demod(5,:) = bit4 > 0;
        Symbol_demod(6,:) = bit5 > 0;
end

这里对于QAM,我没有自己写过就不班门弄斧了,但我大致知道他的思路

解调时,也是对于每一个symbol的每一个用户的那一部分子载波进行解调,挑选出属于该接收机 子载波然后全部并联起来,最后得到一个(N_data*Nysmbol, 1)便于计算误码率。

总结

总的来说,这个仿真我觉得不行!而且我认为OFDM的发送解调和STBC的发送解调顺序可能有问题。如果后续看到更好的仿真,我会继续更新的!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐