Цифровая обработка сигналов для FPGA: Verilog реализует простой FIR-фильтр.
Цифровая обработка сигналов для FPGA: Verilog реализует простой FIR-фильтр.

В этом проекте показано, как реализовать простой КИХ-фильтр с заранее сгенерированными коэффициентами с помощью Verilog.

введение

Скромный КИХ-фильтр является одним из самых основных блоков цифровой обработки сигналов FPGA, поэтому важно понимать, как собрать воедино базовые блоки с заданным количеством отводов и соответствующими значениями коэффициентов. Поэтому в этом уроке по практическому введению в основы DSP на FPGA вы начнете с простого 15-отводного фильтра нижних частот FIR, сгенерируете для него начальные значения коэффициентов в Matlab, а затем преобразуете эти значения для написания модуля Verilog.

Конечная импульсная характеристика или КИХ-фильтр определяется как фильтр, импульсная характеристика которого устанавливается на нулевое значение в течение определенного периода времени. Время, необходимое для установления нуля импульсной характеристикой, напрямую связано с порядком фильтра (количеством отводов), который является порядком полинома базовой передаточной функции КИХ. Передаточная функция КИХ не включает обратную связь, поэтому, если вы вводите импульс со значением 1, а затем строку нулей, на выходе будут просто значения коэффициентов фильтра.

Функция фильтра в основном предназначена для формирования сигнала, в основном сосредотачиваясь на выборе частот, которые следует отфильтровать или пропустить. Одним из самых простых примеров является фильтр нижних частот, который пропускает частоты ниже определенного порога (частоты среза), одновременно значительно ослабляя частоты выше этого порога, как показано на рисунке ниже.

Основное внимание в этом проекте уделяется реализации FIR в HDL (в частности, Verilog), который можно разбить на три основных логических компонента: циклический буфер для синхронизации каждой выборки для надлежащего учета задержки последовательного ввода, множитель для каждого значение коэффициента отвода и аккумулятор для суммированного результата каждого выхода отвода.

Поскольку этот проект фокусируется на механизме проектирования FIR в логике FPGA, он использует инструмент FDA в Simulink и Matlab только для вставки некоторых простых параметров для фильтра нижних частот, а затем использует сгенерированные значения коэффициентов, чтобы поместить их в модуль Verilog, чтобы завершить проектирование фильтра (выполняется на следующем этапе).

Выбор реализации простого КИХ-фильтра нижних частот с 15 отводами и частотой дискретизации 1 Мс/с, частотой полосы пропускания 200 к Гц и частотой полосы задерживания 355 к Гц приводит к получению следующих коэффициентов:

Язык кода:javascript
копировать
-0.0265 
0 
0.0441 
0 
-0.0934 
0 
0.3139 
0.5000 
0.3139 
0 
-0.0934 
0 
0.0441 
0 
-0.0265

Создание файлов дизайна для модулей FIR.

Добавьте исходные файлы в проект Vivado.

После определения порядка КИХ (количества отводов) и получения значений коэффициентов следующий набор параметров, которые необходимо определить, — это разрядность входных выборок, выходных выборок и самих коэффициентов.

Для этого FIR выберите установку регистров входной выборки и коэффициентов шириной 16 бит и установите регистр выходной выборки на 32 бита, поскольку произведение двух 16-битных значений представляет собой 32-битное значение (умножение ширины два значения дают ширину продукта, поэтому, если выбраны 16-битные входные выборки с 8-битными отводами, выходные выборки будут иметь ширину 24 бита).

Эти значения также имеют знак, поэтому в качестве знакового бита используется старший бит, что важно помнить при выборе начальной ширины регистра входной выборки. Чтобы установить эти значения для подписанных типов данных в Verilog, используйте ключевое слово Signed:

Язык кода:javascript
копировать
reg signed [15:0] register_name;

Следующее, что нужно решить, — как обрабатывать значения коэффициентов в Verilog. Значения десятичной точки необходимо преобразовать в значения с фиксированной точкой. Поскольку все значения коэффициентов меньше 1, для десятичного знака доступны все 15 бит регистра (всего 16 бит, старший бит — знаковый бит). Часто вам приходится решать, сколько бит в регистре вы хотите использовать для целой части числа, а не для дробной части числа. Таким образом, математические методы преобразования дробных значений: (значение дробного коэффициента)*(2^(15)) Дробное значение произведения округляется, и если коэффициент отрицательный, вычисляется дополнение до двух значений:

Язык кода:javascript
копировать
tap0 = twos(-0.0265 * 32768) = 0xFC9C
tap1 = 0
tap2 = 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5
tap3 = 0
tap4 = twos(-0.0934 * 32768) = 0xF40C
tap5 = 0
tap6 = 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D
tap7 = 0.5000 * 32768 = 16384 = 0x4000
tap8 = 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D
tap9 =  0
tap10 = twos(-0.0934 * 32768) = 0xF40C
tap11 = 0
tap12 = 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5
tap13 = 0
tap14 = twos(-0.0265 * 32768) = 0xFC9C

Теперь мы, наконец, готовы сосредоточиться на логике модуля FIR. Первый из них — это кольцевой буфер, который принимает последовательный поток входных выборок и создает массив из 15 входных выборок для 15 отводов фильтра.

Язык кода:javascript
копировать
    always @ (posedge clk)
        begin
            if(enable_buff == 1'b1)
                begin
                    buff0 <= in_sample;
                    buff1 <= buff0;        
                    buff2 <= buff1;         
                    buff3 <= buff2;      
                    buff4 <= buff3;      
                    buff5 <= buff4;       
                    buff6 <= buff5;    
                    buff7 <= buff6;       
                    buff8 <= buff7;       
                    buff9 <= buff8;       
                    buff10 <= buff9;        
                    buff11 <= buff10;       
                    buff12 <= buff11;       
                    buff13 <= buff12;       
                    buff14 <= buff13;    
                end
        end

Затем на этапе умножения каждая выборка умножается на каждое значение коэффициента:

Язык кода:javascript
копировать
    /* Multiply stage of FIR */
    always @ (posedge clk)
        begin
            if (enable_fir == 1'b1)
                begin
                    acc0 <= tap0 * buff0;
                    acc1 <= tap1 * buff1;
                    acc2 <= tap2 * buff2;
                    acc3 <= tap3 * buff3;
                    acc4 <= tap4 * buff4;
                    acc5 <= tap5 * buff5;
                    acc6 <= tap6 * buff6;
                    acc7 <= tap7 * buff7;
                    acc8 <= tap8 * buff8;
                    acc9 <= tap9 * buff9;
                    acc10 <= tap10 * buff10;
                    acc11 <= tap11 * buff11;
                    acc12 <= tap12 * buff12;
                    acc13 <= tap13 * buff13;
                    acc14 <= tap14 * buff14;
                end
        end

Значение результата этапа умножения накапливается в регистре путем сложения и, наконец, становится потоком выходных данных фильтра.

Язык кода:javascript
копировать
     /* Accumulate stage of FIR */   
    always @ (posedge clk) 
        begin
            if (enable_fir == 1'b1)
                begin
                    m_axis_fir_tdata <= acc0 + acc1 + acc2 + acc3 + acc4 + acc5 + acc6 + acc7 + acc8 + acc9 + acc10 + acc11 + acc12 + acc13 + acc14;
                end
        end

Наконец, последняя часть логики — это интерфейс, который передает данные в модуль FIR и из него. Интерфейс AXI Stream — один из наиболее распространенных интерфейсов. Ключевыми аспектами являются сигналы tready и tvalid, которые позволяют управлять потоком данных между вышестоящими и нижестоящими устройствами. Это означает, что модуль FIR должен предоставить сигнал tvalid своему нисходящему устройству, чтобы указать, что его выходные данные являются действительными данными, и иметь возможность приостановить (но при этом сохранить) свой вывод, если нисходящее устройство снимет свой сигнал готовности. Модуль FIR также должен иметь возможность работать так же, как вышестоящее устройство на своем первичном интерфейсе.

Ниже приводится обзор логической конструкции модуля FIR:

Обратите внимание, как сигналы tready и tvalid устанавливают значение включения входного кольцевого буфера и этап умножения FIR, а также как каждый регистр, через который проходят данные или коэффициенты, объявляется как подписанный.

Код Verilog модуля FIR:

Язык кода:javascript
копировать
`timescale 1ns / 1ps

module FIR(
    input clk,
    input reset,
    input signed [15:0] s_axis_fir_tdata, 
    input [3:0] s_axis_fir_tkeep,
    input s_axis_fir_tlast,
    input s_axis_fir_tvalid,
    input m_axis_fir_tready,
    output reg m_axis_fir_tvalid,
    output reg s_axis_fir_tready,
    output reg m_axis_fir_tlast,
    output reg [3:0] m_axis_fir_tkeep,
    output reg signed [31:0] m_axis_fir_tdata
    );


    always @ (posedge clk)
        begin
            m_axis_fir_tkeep <= 4'hf;
        end
        
    always @ (posedge clk)
        begin
            if (s_axis_fir_tlast == 1'b1)
                begin
                    m_axis_fir_tlast <= 1'b1;
                end
            else
                begin
                    m_axis_fir_tlast <= 1'b0;
                end
        end
    
    // 15-tap FIR 
    reg enable_fir, enable_buff;
    reg [3:0] buff_cnt;
    reg signed [15:0] in_sample; 
    reg signed [15:0] buff0, buff1, buff2, buff3, buff4, buff5, buff6, buff7, buff8, buff9, buff10, buff11, buff12, buff13, buff14; 
    wire signed [15:0] tap0, tap1, tap2, tap3, tap4, tap5, tap6, tap7, tap8, tap9, tap10, tap11, tap12, tap13, tap14; 
    reg signed [31:0] acc0, acc1, acc2, acc3, acc4, acc5, acc6, acc7, acc8, acc9, acc10, acc11, acc12, acc13, acc14; 

    
    /* Taps for LPF running @ 1MSps with a cutoff freq of 400kHz*/
    assign tap0 = 16'hFC9C;  // twos(-0.0265 * 32768) = 0xFC9C
    assign tap1 = 16'h0000;  // 0
    assign tap2 = 16'h05A5;  // 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5
    assign tap3 = 16'h0000;  // 0
    assign tap4 = 16'hF40C;  // twos(-0.0934 * 32768) = 0xF40C
    assign tap5 = 16'h0000;  // 0
    assign tap6 = 16'h282D;  // 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D
    assign tap7 = 16'h4000;  // 0.5000 * 32768 = 16384 = 0x4000
    assign tap8 = 16'h282D;  // 0.3139 * 32768 = 10285.8752 = 10285 = 0x282D
    assign tap9 = 16'h0000;  // 0
    assign tap10 = 16'hF40C; // twos(-0.0934 * 32768) = 0xF40C
    assign tap11 = 16'h0000; // 0
    assign tap12 = 16'h05A5; // 0.0441 * 32768 = 1445.0688 = 1445 = 0x05A5
    assign tap13 = 16'h0000; // 0
    assign tap14 = 16'hFC9C; // twos(-0.0265 * 32768) = 0xFC9C
    
    /* This loop sets the tvalid flag on the output of the FIR high once 
     * the circular buffer has been filled with input samples for the 
     * first time after a reset condition. */
    always @ (posedge clk or negedge reset)
        begin
            if (reset == 1'b0) //if (reset == 1'b0 || tvalid_in == 1'b0)
                begin
                    buff_cnt <= 4'd0;
                    enable_fir <= 1'b0;
                    in_sample <= 8'd0;
                end
            else if (m_axis_fir_tready == 1'b0 || s_axis_fir_tvalid == 1'b0)
                begin
                    enable_fir <= 1'b0;
                    buff_cnt <= 4'd15;
                    in_sample <= in_sample;
                end
            else if (buff_cnt == 4'd15)
                begin
                    buff_cnt <= 4'd0;
                    enable_fir <= 1'b1;
                    in_sample <= s_axis_fir_tdata;
                end
            else
                begin
                    buff_cnt <= buff_cnt + 1;
                    in_sample <= s_axis_fir_tdata;
                end
        end   

    always @ (posedge clk)
        begin
            if(reset == 1'b0 || m_axis_fir_tready == 1'b0 || s_axis_fir_tvalid == 1'b0)
                begin
                    s_axis_fir_tready <= 1'b0;
                    m_axis_fir_tvalid <= 1'b0;
                    enable_buff <= 1'b0;
                end
            else
                begin
                    s_axis_fir_tready <= 1'b1;
                    m_axis_fir_tvalid <= 1'b1;
                    enable_buff <= 1'b1;
                end
        end
    
    /* Circular buffer bring in a serial input sample stream that 
     * creates an array of 15 input samples for the 15 taps of the filter. */
    always @ (posedge clk)
        begin
            if(enable_buff == 1'b1)
                begin
                    buff0 <= in_sample;
                    buff1 <= buff0;        
                    buff2 <= buff1;         
                    buff3 <= buff2;      
                    buff4 <= buff3;      
                    buff5 <= buff4;       
                    buff6 <= buff5;    
                    buff7 <= buff6;       
                    buff8 <= buff7;       
                    buff9 <= buff8;       
                    buff10 <= buff9;        
                    buff11 <= buff10;       
                    buff12 <= buff11;       
                    buff13 <= buff12;       
                    buff14 <= buff13;    
                end
            else
                begin
                    buff0 <= buff0;
                    buff1 <= buff1;        
                    buff2 <= buff2;         
                    buff3 <= buff3;      
                    buff4 <= buff4;      
                    buff5 <= buff5;       
                    buff6 <= buff6;    
                    buff7 <= buff7;       
                    buff8 <= buff8;       
                    buff9 <= buff9;       
                    buff10 <= buff10;        
                    buff11 <= buff11;       
                    buff12 <= buff12;       
                    buff13 <= buff13;       
                    buff14 <= buff14;
                end
        end
        
    /* Multiply stage of FIR */
    always @ (posedge clk)
        begin
            if (enable_fir == 1'b1)
                begin
                    acc0 <= tap0 * buff0;
                    acc1 <= tap1 * buff1;
                    acc2 <= tap2 * buff2;
                    acc3 <= tap3 * buff3;
                    acc4 <= tap4 * buff4;
                    acc5 <= tap5 * buff5;
                    acc6 <= tap6 * buff6;
                    acc7 <= tap7 * buff7;
                    acc8 <= tap8 * buff8;
                    acc9 <= tap9 * buff9;
                    acc10 <= tap10 * buff10;
                    acc11 <= tap11 * buff11;
                    acc12 <= tap12 * buff12;
                    acc13 <= tap13 * buff13;
                    acc14 <= tap14 * buff14;
                end
        end    
        
     /* Accumulate stage of FIR */   
    always @ (posedge clk) 
        begin
            if (enable_fir == 1'b1)
                begin
                    m_axis_fir_tdata <= acc0 + acc1 + acc2 + acc3 + acc4 + acc5 + acc6 + acc7 + acc8 + acc9 + acc10 + acc11 + acc12 + acc13 + acc14;
                end
        end     

    
    
endmodule

Создание файлов моделирования

Чтобы протестировать модуль FIR, вам необходимо создать тестовый стенд в качестве источника моделирования:

В модуле FIR необходимо протестировать две основные вещи: алгоритм фильтра и интерфейс потока AXI. Для достижения этой цели на испытательном стенде был создан конечный автомат, который генерирует простую синусоидальную волну частотой 200 к Гц и переключает активный сигнал на ведомой стороне и сигнал готовности на ведущей стороне интерфейса FIR.

Тестовая платформа для модуля FIR:

Язык кода:javascript
копировать
`timescale 1ns / 1ps

module tb_FIR;

    reg clk, reset, s_axis_fir_tvalid, m_axis_fir_tready;
    reg signed [15:0] s_axis_fir_tdata;
    wire m_axis_fir_tvalid;
    wire [3:0] m_axis_fir_tkeep;
    wire [31:0] m_axis_fir_tdata;
    
    /*
     * 100Mhz (10ns) clock 
     */
    always begin
        clk = 1; #5;
        clk = 0; #5;
    end
    
    always begin
        reset = 1; #20;
        reset = 0; #50;
        reset = 1; #1000000;
    end
    
    always begin
        s_axis_fir_tvalid = 0; #100;
        s_axis_fir_tvalid = 1; #1000;
        s_axis_fir_tvalid = 0; #50;
        s_axis_fir_tvalid = 1; #998920;
    end
    
    always begin
        m_axis_fir_tready = 1; #1500;
        m_axis_fir_tready = 0; #100;
        m_axis_fir_tready = 1; #998400;
    end
    
    /* Instantiate FIR module to test. */
    FIR FIR_i(
        .clk(clk),
        .reset(reset),
        .s_axis_fir_tdata(s_axis_fir_tdata),   
        .s_axis_fir_tkeep(s_axis_fir_tkeep),   
        .s_axis_fir_tlast(s_axis_fir_tlast),   
        .s_axis_fir_tvalid(s_axis_fir_tvalid), 
        .m_axis_fir_tready(m_axis_fir_tready),
        .m_axis_fir_tvalid(m_axis_fir_tvalid), 
        .s_axis_fir_tready(s_axis_fir_tready), 
        .m_axis_fir_tlast(m_axis_fir_tlast),   
        .m_axis_fir_tkeep(m_axis_fir_tkeep),   
        .m_axis_fir_tdata(m_axis_fir_tdata));  
        
    reg [4:0] state_reg;
    reg [3:0] cntr;
    
    parameter wvfm_period = 4'd4;
    
    parameter init               = 5'd0;
    parameter sendSample0        = 5'd1;
    parameter sendSample1        = 5'd2;
    parameter sendSample2        = 5'd3;
    parameter sendSample3        = 5'd4;
    parameter sendSample4        = 5'd5;
    parameter sendSample5        = 5'd6;
    parameter sendSample6        = 5'd7;
    parameter sendSample7        = 5'd8;
    
    /* This state machine generates a 200kHz sinusoid. */
    always @ (posedge clk or posedge reset)
        begin
            if (reset == 1'b0)
                begin
                    cntr <= 4'd0;
                    s_axis_fir_tdata <= 16'd0;
                    state_reg <= init;
                end
            else
                begin
                    case (state_reg)
                        init : //0
                            begin
                                cntr <= 4'd0;
                                s_axis_fir_tdata <= 16'h0000;
                                state_reg <= sendSample0;
                            end
                            
                        sendSample0 : //1
                            begin
                                s_axis_fir_tdata <= 16'h0000;
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample1;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample0;
                                    end
                            end 
                        
                        sendSample1 : //2
                            begin
                                s_axis_fir_tdata <= 16'h5A7E; 
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample2;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample1;
                                    end
                            end 
                        
                        sendSample2 : //3
                            begin
                                s_axis_fir_tdata <= 16'h7FFF;
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample3;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample2;
                                    end
                            end 
                        
                        sendSample3 : //4
                            begin
                                s_axis_fir_tdata <= 16'h5A7E;
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample4;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample3;
                                    end
                            end 
                        
                        sendSample4 : //5
                            begin
                                s_axis_fir_tdata <= 16'h0000;
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample5;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample4;
                                    end
                            end 
                        
                        sendSample5 : //6
                            begin
                                s_axis_fir_tdata <= 16'hA582; 
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample6;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample5;
                                    end
                            end 
                        
                        sendSample6 : //6
                            begin
                                s_axis_fir_tdata <= 16'h8000; 
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample7;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample6;
                                    end
                            end 
                        
                        sendSample7 : //6
                            begin
                                s_axis_fir_tdata <= 16'hA582; 
                                
                                if (cntr == wvfm_period)
                                    begin
                                        cntr <= 4'd0;
                                        state_reg <= sendSample0;
                                    end
                                else
                                    begin 
                                        cntr <= cntr + 1;
                                        state_reg <= sendSample7;
                                    end
                            end                     
                    
                    endcase
                end
        end
    
endmodule

Запустите поведенческую симуляцию

Установив модуль FIR и файлы его тестового стенда, запустите симулятор в Vivado из окна Flow Navigator, выбрав опцию «Запустить поведенческое моделирование».

Как показано в поведенческом моделировании, FIR правильно фильтрует сигналы и правильно реагирует на сигналы потока AXI.

Подвести итог

Код приведен выше. Если вам интересно, вы можете запустить его самостоятельно. Однако вы можете заметить, что время работы этого модуля FIR не должно спадать при запуске синтеза и реализации проекта. В следующей статье мы подробно расскажем, как перепроектировать ваш дизайн, если требования по времени не могут быть соблюдены~

boy illustration
[Спецификация] Результаты и исключения возврата интерфейса SpringBoot обрабатываются единообразно, поэтому инкапсуляция является элегантной.
boy illustration
Интерпретация каталога веб-проекта Flask
boy illustration
Что такое подробное объяснение файла WSDL_wsdl
boy illustration
Как запустить большую модель ИИ локально
boy illustration
Подведение итогов десяти самых популярных веб-фреймворков для Go
boy illustration
5 рекомендуемых проектов CMS с открытым исходным кодом на базе .Net Core
boy illustration
Java использует httpclient для отправки запросов HttpPost (отправка формы, загрузка файлов и передача данных Json)
boy illustration
Руководство по развертыванию Nginx в Linux (Centos)
boy illustration
Интервью с Alibaba по Java: можно ли использовать @Transactional и @Async вместе?
boy illustration
Облачный шлюз Spring реализует примеры балансировки нагрузки и проверки входа в систему.
boy illustration
Используйте Nginx для решения междоменных проблем
boy illustration
Произошла ошибка, когда сервер веб-сайта установил соединение с базой данных. WordPress предложил решение проблемы с установкой соединения с базой данных... [Легко понять]
boy illustration
Новый адрес java-библиотеки_16 топовых Java-проектов с открытым исходным кодом, достойных вашего внимания! Обязательно к просмотру новичкам
boy illustration
Лучшие практики Kubernetes для устранения несоответствий часовых поясов внутри контейнеров
boy illustration
Введение в проект удаления водяных знаков из коротких видео на GitHub Douyin_TikTok_Download_API
boy illustration
Весенние аннотации: подробное объяснение @Service!
boy illustration
Пожалуйста, не используйте foreach для пакетной вставки в MyBatis. Для 5000 фрагментов данных потребовалось 14 минут. .
boy illustration
Как создать проект Node.js с помощью npm?
boy illustration
Mybatis-plus использует typeHandler для преобразования объединенных строк String в списки списков.
boy illustration
Не удалось установить программное обеспечение Mitsubishi. Возможно, возникла проблема с реестром.
boy illustration
Разрешение ошибок проекта SpringBoot 3 mybatis-plus: org.apache.ibatis.binding.BindingException: неверный оператор привязки
boy illustration
Более краткая проверка параметров. Для проверки параметров используйте SpringBoot Validation.
boy illustration
Поиграйтесь с интеграцией Spring Boot (платформа запланированных задач Quartz)
boy illustration
Несколько популярных режимов интерфейса API: RESTful, GraphQL, gRPC, WebSocket, Webhook.
boy illustration
Redis: практика публикации (pub) и подписки (sub)
boy illustration
Подробное объяснение пакета Golang Context
boy illustration
Краткое руководство: создайте свое первое приложение .NET Aspire
boy illustration
Краткое обсуждение метода пакетной вставки MyBatis: обработка 100 000 фрагментов данных занимает всего 2 секунды.
boy illustration
[Инструмент] Используйте nvm для управления переключением версий nodejs, это так здорово!
boy illustration
HTML можно преобразовать в word_html для отображения текстовых документов.