数字设计---同步fifo
同步FIFO原理
FIFO
FIFO (First-In-First-Out) 是一种先进先出的数据交互方式,在数字ASIC设计中常常被使用。
FIFO 与普通存储器 RAM 的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。 FIFO 本质上是由 RAM (或者寄存器)加读写控制逻辑构成的一种先进先出的数据缓冲器。
FIFO按工作时钟域的不同又可以分为:同步FIFO和异步FIFO。
同步FIFO与异步FIFO的区别
同步FIFO的写时钟和读时钟为同一个时钟,FIFO内部所有逻辑都是同步逻辑,常常用于交互数据缓冲。
异步FIFO的写时钟和读时钟为异步时钟,FIFO内部的写逻辑和读逻辑的交互需要异步处理,异步FIFO常用于跨时钟域交互。
同步FIFO的端口
- FIFO 的宽度:即 FIFO 一次读写操作的数据位;
- FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)。
- 满标志:FIFO 已满或将要满时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向FIFO 中写数据而造成溢出(overflow)。
- 空标志:FIFO 已空或将要空时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从FIFO 中读出数据而造成无效数据的读出(underflow)。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
- 将满标志(almost full):FIFO将要满时由FIFO的状态电路送出的一个信号。
- 将空信号(almost empty):FIFO将要空时由FIFO的状态电路送出的一个信号。
同步FIFO的结构
读写指针
从这个图上我们可以看出,写指针总是指向下一个时钟要写的地址,读指针总是指向下一个时钟要读的地址。读指针等于写指针的时候有可能为空,有可能为满。(当然,读指针也可以指向当前正在读地址,但相应的根据地址读取数据的逻辑会有所不同,前面读指针指向下一个时钟要读的地址是用时序逻辑去读,指向当前正在读的地址用组合逻辑去读。)
当fifo没有写满并且写使能拉高时,写指针加一,当fifo没有读空并且读使能拉高时,读指针加一。由于fifo的读写地址变化是循环的,即fifo的写地址写到存储的最大地址时,写地址会回到0地址处再次按地址递增的顺序进行写操作,同理当fifo的读地址读到存储的最大地址时,读地址会回到0地址处再次按地址递增的顺序进行读操作。所以在对读写指针进行加一操作时还需要考虑到指针会回到0地址这一问题。
当然有可能在fifo既没有写满也没有读空的情况下,写使能和读使能同时拉高,这是将写指针和读指针同时加一,那会不会同时读写会对数据产生影响呢?是不会的,出现问题的情况是对同一地址进行读写,但当同时读写对同一个数据地址进行操作时,这时意味着fifo要么已经满要么已经空,这时对fifo进行读或者进行写都是违法的,所以写fifo逻辑部分要避免在fifo空的时候进行读操作,在fifo满的时候对fifo进行写操作。但上面提到的是读写fifo逻辑部分需要考虑的问题,这里考虑的是fifo内部的问题,在前面已经提到,同时读写是在fifo既没有写满又没有读空的条件下进行的,所以对于数据的读写没有影响。
空满检测
对于空满检测,前面我们提过,当读写指针地址相同时,fifo可能已经写满也可能已经读空,但是由于地址会在fifo最大深度处回到0地址,所以直接将读写地址进行比较来判断fifo的空满状态好像不是那么可行。
可以通过在fifo内部设立一个计数器用来计数fifo内的数据量,当fifo没有写满且写使能拉高时(或者写指针加一时),计数器加一,当fifo没有读空且读使能拉高时(或者读指针加一时),计数器减一,当fifo既没有读空有没有写满,且读写使能同时拉高有效时,这是的计数器不增也不减(由于我们讨论的时同步fifo,读写都在同一个时钟下,在相同时间内,如果读写使能同时有效,那么读的数据量和写的数据量相等,所以计数器不增也不减)。
如果fifo内部的计数器计数为0,表示fifo内没有数据,则空信号拉高,当fifo内部的计数器计数等于fifo的最大深度,表示fifo已经写满,则满信号拉高。
当然除了在fifo内部设立一个计数器,我们也可以采用在读写地址前增加一位的策略,如果fifo的深度为16,则地址需要4位二进制来表示,那么我们在表示fifo地址时用5位二进制数来表示。
当读写指针完全相同时,表明fifo已经读空了
当有数据进入时,写指针继续增大,当写指针为15时,继续增大来到了0地址处,这时第五位置为1,继续增大,当读指针与写指针低四位相同,最高为相反时,表示fifo已经写满。(最高为可以理解为写指针将读指针套圈的标志位,当最高位不同时,代表着写指针已经将读指针套圈,当低位完全相同时,表示写指针套圈后追上读指针,这时fifo已经写满)
我们也可以通过读写指针前后关系判断FIFO的空满,如果读指针落后于写指针一个地址,当读指针加一时,这时FIFO为空,当写指针落后于读指针一个地址时,当写指针加一时,这时FIFO为满。
读写数据
读数据可以通过组合逻辑根据读指针读取fifo中的数据,也可以根据时序逻辑根据读指针读取数据,采用时序逻辑读时序可能会错后一个时钟。
同步FIFO的verilog实现
采用计数器进行空满检测
sync_fifo_1.v
`timescale 1ns / 1ps module sync_fifo_1#( parameter DATA_WIDTH = 8, FIFO_DEPTH = 8, AFULL_DEPTH = 7, AEMPTY_DEPTH = 1, ADDR_WIDTH = 3, RDATA_MODE = 0 ) ( input clk, input rst_n, input wr_en, input [DATA_WIDTH-1:0] wr_data, output full, output almost_full, input rd_en, output reg [DATA_WIDTH-1:0] rd_data, output empty, output almost_empty, output reg overflow, output reg underflow ); reg [ADDR_WIDTH-1:0] wr_ptr; reg [ADDR_WIDTH-1:0] rd_ptr; reg [ADDR_WIDTH:0] fifo_cnt; reg [DATA_WIDTH-1:0] buf_mem[0:FIFO_DEPTH-1]; integer II; //- - - - -fifo_cnt- - - - - - - always@(posedge clk or negedge rst_n) begin if(!rst_n) fifo_cnt <= {(ADDR_WIDTH+1){1'b0}}; else begin if(wr_en && ~full && rd_en && ~empty) fifo_cnt <= fifo_cnt; else if(wr_en && ~full) fifo_cnt <= fifo_cnt + 1'b1; else if(rd_en && ~empty) fifo_cnt <= fifo_cnt - 1'b1; end end //- - - - - -wr_ptr- - - - - - - - - always @(posedge clk or negedge rst_n) begin if(!rst_n) wr_ptr <= {(ADDR_WIDTH+1){1'b0}}; else begin if(wr_ptr == FIFO_DEPTH-1)//注意这里,当写到fifo地址的最大值时需要从0开始写 wr_ptr <= {(ADDR_WIDTH+1){1'b0}}; else if(wr_en && ~full) wr_ptr <= wr_ptr + 1'b1; end end //- - - - - -rd_ptr- - - - - - - - - - - always @(posedge clk or negedge rst_n) begin if(!rst_n) rd_ptr <= {(ADDR_WIDTH+1){1'b0}}; else begin if(rd_ptr == FIFO_DEPTH-1) rd_ptr <= {(ADDR_WIDTH+1){1'b0}}; else if(rd_en && ~empty) rd_ptr <= rd_ptr + 1'b1; end end //- - - - - - buf_mem- - - - - - - - - - always @(posedge clk or negedge rst_n) begin if(!rst_n) for(II=0;II<FIFO_DEPTH;II=II+1)//初始化 buf_mem[II]<={(DATA_WIDTH){1'b0}}; else if(wr_en && ~full) buf_mem[wr_ptr] <= wr_data; end //- - - - - - - rd_data- - - - - - - - - generate if(RDATA_MODE == 1'b0) begin always @(*) rd_data = buf_mem[rd_ptr]; end else begin always @(posedge clk or negedge rst_n) begin if(!rst_n) rd_data <= {DATA_WIDTH{1'b0}}; else rd_data <= buf_mem[rd_ptr]; end end endgenerate //- - - - - - - - overflow,underflow- - - - - - - always @(posedge clk or negedge rst_n) begin if(!rst_n) begin overflow <= 1'b0; underflow <= 1'b0; end else if(wr_en && full) begin overflow <= 1'b1; end else if(rd_en && empty) begin underflow <= 1'b1; end end assign full = (fifo_cnt == FIFO_DEPTH) ? 1'b1:1'b0; assign empty = (fifo_cnt == {(ADDR_WIDTH+1){1'b0}}) ? 1'b1:1'b0; assign almost_full = (fifo_cnt >= AFULL_DEPTH) ? 1'b1:1'b0; assign almost_empty = (fifo_cnt <= AEMPTY_DEPTH) ? 1'b1:1'b0; endmodule
testbench
sync_fifo_test_1.v
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2022/03/11 21:59:47 // Design Name: // Module Name: sync_fifo_test_1 // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module sync_fifo_test_1( ); parameter DATA_WIDTH = 8; parameter FIFO_DEPTH = 8; parameter AFULL_DEPTH = FIFO_DEPTH -1 ; parameter AEMPTY_DEPTH = 1; parameter ADDR_WIDTH = 3; parameter RDATA_MODE = 0; reg clk; reg rst_n; reg wr_en; reg [DATA_WIDTH-1:0] wr_data; reg rd_en; wire [DATA_WIDTH-1:0] rd_data; wire full; wire almost_full; wire empty; wire almost_empty; wire overflow; wire underflow; integer II; sync_fifo_1 #( .DATA_WIDTH(DATA_WIDTH), .FIFO_DEPTH(FIFO_DEPTH), .ADDR_WIDTH(ADDR_WIDTH), .RDATA_MODE(RDATA_MODE) ) inst_sync_fio( .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .wr_data(wr_data), .rd_en(rd_en), .rd_data(rd_data), .full(full), .almost_full(almost_full), .empty(empty), .almost_empty(almost_empty), .overflow(overflow), .underflow(underflow) ); initial begin #0; clk = 0; rst_n = 0; #10; rst_n = 1; end always #5 clk = ~clk; initial begin #0; wr_en = 0; wr_data = 0; rd_en = 0; end initial begin #20; send_wr; end initial begin #100; send_rd; #1000; $finish; end task send_wr; begin for(II=0;II<8;II=II+1) begin @(posedge clk) begin wr_en <= 1'b1; wr_data <= II+1; end end @(posedge clk) begin wr_en <= 1'b0; wr_data <= 8'h0; end repeat(10) @(posedge clk); end endtask task send_rd; begin for(II=0;II<8;II=II+1) begin @(posedge clk) begin rd_en <= 1'b1; end end @(posedge clk) begin rd_en <= 1'b0; end end endtask endmodule
采用在最高位相同与否检测空满
sync_fifo_2.v
module sync_fifo_2#( parameter DEPTH = 16, //FIFO深度 parameter WIDTH = 8, //FIFO内数据位宽 parameter P_WIDTH = 5 //读写指针位宽 ) ( input rst_n ,input clk ,input [WIDTH-1:0] Data_Write //写入的数据值 ,input Write_Sig //写入数据使能 ,input Read_Sig //读取数据使能 ,output wire [WIDTH-1:0] Data_Read //读取的数据值 ,output wire Full_Sig //FIFO为空信号 ,output wire Empty_Sig //FIFO为满信号 ); reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1]; reg [P_WIDTH-1:0] Read_pointer; reg [P_WIDTH-1:0] Write_pointer; reg [WIDTH-1:0] rData_Read; assign Full_Sig = ((Read_pointer[P_WIDTH-2:0]==Write_pointer[P_WIDTH-2:0]) &&(Read_pointer[P_WIDTH-1] != Write_pointer[P_WIDTH-1]))? 1'd1:1'd0; assign Empty_Sig = (Read_pointer[P_WIDTH-1:0]==Write_pointer[P_WIDTH-1:0])? 1'd1:1'd0; always @(negedge rst_n or posedge clk) begin if (!rst_n) begin rData_Read <= 0; Read_pointer <= 0; Write_pointer <= 0; end else begin if (~Full_Sig && ~Empty_Sig && Read_Sig && Write_Sig ) //非空非满时同时读写 begin rData_Read <= RAM_MEM[Read_pointer[P_WIDTH-2:0]]; RAM_MEM[Write_pointer[P_WIDTH-2:0]] <= Data_Write; Read_pointer <= Read_pointer + 1'd1; Write_pointer <= Write_pointer + 1'd1; end // else if (~Empty_Sig && Read_Sig ) //非空时读数据 begin rData_Read <= RAM_MEM[Read_pointer[P_WIDTH-2:0]]; Read_pointer <= Read_pointer + 1'd1; end else if (~Full_Sig && Write_Sig) //非满时写数据 begin RAM_MEM[Write_pointer[P_WIDTH-2:0]] <= Data_Write; Write_pointer <= Write_pointer + 1'd1; end end // else end // always assign Data_Read = rData_Read; endmodule
testbench
sync_fifo_test_2.v
module sync_fifo_test_2( ); parameter DEPTH = 16; parameter WIDTH = 8; parameter P_WIDTH = 5; reg clk; reg rst_n; reg Write_Sig; reg Read_Sig; reg [7:0] Data_Write; wire [7:0] Data_Read; integer i; wire Full_Sig; wire Empty_Sig; sync_fifo_2 #( .DEPTH(DEPTH), .WIDTH(WIDTH), .P_WIDTH(P_WIDTH) ) inst_Syn_FIFO ( .rst_n (rst_n), .clk (clk), .Data_Write (Data_Write), .Write_Sig (Write_Sig), .Read_Sig (Read_Sig), .Data_Read (Data_Read), .Full_Sig (Full_Sig), .Empty_Sig (Empty_Sig) ); always #5 clk = ~clk; initial begin clk = 1'b0; rst_n = 1'b1; @(posedge clk) #5 rst_n = 1'b0; @(posedge clk) #5 rst_n = 1'b1; end // initial initial begin i = 4'd0; Write_Sig = 1'd0; Read_Sig = 1'd0; Data_Write = 8'd0; repeat(5) @(posedge clk); for(i=0; i<19; i=i+1) Write_Data(1'd1, 12*(i+1)); for(i=0; i<19; i=i+1) Read_Data(1'd1); for(i=0; i<19; i=i+1) Write_Data(1'd1, 12*(i+1)); for(i=0; i<8; i=i+1) Read_Data(1'd1); @(posedge clk) Read_Sig <= 1'b0; for(i=0; i<19; i=i+1) Write_Data(1'd1, 12*(i+1)); for(i=0; i<19; i=i+1) Read_Data(1'd1); @(posedge clk) Write_Sig <= 1'b1; Data_Write <= 8'd3; @(posedge clk) Write_Sig <= 1'b1; Data_Write <= 8'd5; @(posedge clk) Write_Sig <= 1'b1; Data_Write <= 8'd7; Read_Sig <= 1'b1; @(posedge clk) Write_Sig <= 1'b0; Data_Write <= 8'd0; Read_Sig <= 1'b1; @(posedge clk) Read_Sig <= 1'b1; @(posedge clk) Read_Sig <= 1'b1; @(posedge clk) Read_Sig <= 1'b0; #30 $stop; end // initial task Write_Data(input reg Write_en,input reg [7:0] Data_in); begin @(posedge clk) if(Full_Sig) begin Write_Sig <= 1'b0; Data_Write <= 8'd0; end else begin Write_Sig <= Write_en; Data_Write <= Data_in; end end endtask task Read_Data(input reg Read_en); begin @(posedge clk) if(Empty_Sig) Read_Sig <= 1'b0; else Read_Sig <= Read_en; end endtask endmodule
数据丢失问题
在运行上面最后一个例子的时候会发现如果根据FIFO的满信号来中止数据传输,就会存在数据丢失的问题,上面在传输192的时候就已经写满了,这时候写满信号拉高,但是对于数据输入端来说,这时又传输了一个204,只有下一个时钟,发送端才采样到写满信号,才终止输出的传输,所以传输的204就丢失了。
对于这种情况,数据发送端可以采用将满信号(almost_flag)来控制数据的传输。
贴一个解决办法
sync_fifo.v
module sync_fifo_z #( parameter DATA_WIDTH = 8, FIFO_DEPTH = 16, ALMOST_FULL_GAP = 15, ALMOST_EMPTY_GAP = 1, ADDR_WIDTH = $clog2(FIFO_DEPTH) ) ( input clk, input rst_n, input wr_en, input rd_en, input [DATA_WIDTH-1:0] wr_data, output full_flag, output empty_flag, output almost_full_flag, output almost_empty_flag, output [DATA_WIDTH-1:0] rd_data ); reg [ADDR_WIDTH-1:0] wr_ptr; reg [ADDR_WIDTH-1:0] rd_ptr; reg [ADDR_WIDTH:0] cnt; reg [DATA_WIDTH-1:0] mem [FIFO_DEPTH-1:0]; // cnt always @(posedge clk or negedge rst_n) begin if(!rst_n) begin cnt <= {(ADDR_WIDTH+1){1'b0}}; end else begin if(wr_en && !full_flag && rd_en && !empty_flag) begin cnt <= cnt; end else if(wr_en && !full_flag) begin cnt <= cnt + 1'b1; end else if(rd_en && !empty_flag) begin cnt <= cnt - 1'b1; end else begin cnt <= cnt; end end end //wr_ptr rd_ptr always @(posedge clk or negedge rst_n) begin if(!rst_n) begin wr_ptr <= {ADDR_WIDTH{1'b0}}; end else begin if(wr_en && !full_flag) begin if(wr_ptr == FIFO_DEPTH-1) wr_ptr <= {ADDR_WIDTH{1'b0}}; else wr_ptr <= wr_ptr + 1'b1; end else wr_ptr <= wr_ptr; end end always @(posedge clk or negedge rst_n) begin if(!rst_n) begin rd_ptr <= {ADDR_WIDTH{1'b0}}; end else begin if(rd_en && !empty_flag) begin if(rd_ptr == FIFO_DEPTH-1) rd_ptr <= {ADDR_WIDTH{1'b0}}; else rd_ptr <= rd_ptr + 1'b1; end else rd_ptr <= rd_ptr; end end //read write integer i; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin for(i=0;i<FIFO_DEPTH;i=i+1) begin mem[i] = {DATA_WIDTH{1'b0}}; end end else begin if(wr_en && !full_flag) mem[wr_ptr] <= wr_data; end end assign rd_data = mem[rd_ptr]; //full empty assign full_flag = (cnt == FIFO_DEPTH) ? 1'b1:1'b0; assign empty_flag = (cnt == {(ADDR_WIDTH+1){1'b0}})?1'b1:1'b0; assign almost_full_flag = (cnt >=ALMOST_FULL_GAP) ? 1'b1:1'b0; assign almost_empty_flag = (cnt <=ALMOST_EMPTY_GAP)?1'b1:1'b0; endmodule
testbench
module sync_fifo_test_z( ); parameter DATA_WIDTH = 8, FIFO_DEPTH = 16, ALMOST_FULL_GAP = 15, ALMOST_EMPTY_GAP = 1, ADDR_WIDTH = $clog2(FIFO_DEPTH); reg wr_en; reg rd_en; reg [DATA_WIDTH-1:0] wr_data; wire [DATA_WIDTH-1:0] rd_data; wire full_flag; wire empty_flag; wire almost_full_flag; wire almost_empty_flag; reg clk; reg rst_n; sync_fifo_z #( .DATA_WIDTH(DATA_WIDTH), .FIFO_DEPTH(FIFO_DEPTH) ) inst1( .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en), .wr_data(wr_data), .full_flag(full_flag), .empty_flag(empty_flag), .almost_full_flag(almost_full_flag), .almost_empty_flag(almost_empty_flag), .rd_data(rd_data) ); initial begin clk =0; forever #5 clk = ~clk; end initial begin rst_n = 1'b1; #20 rst_n = 1'b0; #10 rst_n = 1'b1; end always@(posedge clk or negedge rst_n) begin if(!rst_n) begin wr_en <= 1'b0; wr_data <= {DATA_WIDTH{1'b0}}; end else begin if(!almost_full_flag ) begin wr_en <= 1'b1; wr_data <= wr_data + 1'b1; end else begin wr_en <= 1'b0; wr_data <= wr_data; end end end always @(posedge clk or negedge rst_n) begin if(!rst_n) rd_en <= 1'b1; else begin if(almost_full_flag && !almost_empty_flag) rd_en <= 1'b1; else if(almost_empty_flag) rd_en <= 1'b0; else rd_en <= rd_en; end end endmodule
从图上可以看出,采用almost_full标志位可以解决数据丢失的位置,数据从17开始没有丢失。
小富哥(一个很牛逼的人)代码
module sync_fifo( input wire clk, input wire rst_n, input wire [7:0] data_in, input wire wr_en, input wire rd_en, output wire full, output wire empty, output reg [7:0] data_out ); reg [7:0] fifo_mem [15:0]; wire [3:0] wr_addr; wire [3:0] rd_addr; reg [4:0] wr_addr_ptr;//写指针 reg [4:0] rd_addr_ptr;//读指针 assign wr_addr = wr_addr_ptr[3:0]; assign rd_addr = rd_addr_ptr[3:0]; always @(posedge clk) begin if(wr_en && !full) fifo_mem[wr_addr] <= data_in; else fifo_mem[wr_addr] <= fifo_mem[wr_addr]; end always @(posedge clk) begin if(rd_en && !empty) data_out <= fifo_mem[rd_addr]; else data_out <= 8'd0; end always @(posedge clk or negedge rst_n) begin if(!rst_n) begin wr_addr_ptr <= 5'd0; end else begin if(wr_en && !full) wr_addr_ptr <= wr_addr_ptr + 1'b1; else wr_addr_ptr <= wr_addr_ptr; end end always @(posedge clk or negedge rst_n) begin if(!rst_n) begin rd_addr_ptr <= 5'd0; end else begin if(rd_en && !empty) rd_addr_ptr <= rd_addr_ptr + 1'b1; else rd_addr_ptr <= rd_addr_ptr; end end assign full = ({!wr_addr_ptr[4],wr_addr_ptr[3:0]}==rd_addr_ptr) ? 1'b1 : 1'b0; assign empty = (wr_addr_ptr == rd_addr_ptr) ? 1'b1 : 1'b0; endmodule
testbench.v
module sync_fifo_sim(); reg clk; reg rst_n; reg [7:0] data_in; reg wr_en; reg rd_en; wire full; wire empty; wire [7:0] data_out; sync_fifo sync_fifo( .clk(clk), .rst_n(rst_n), .data_in(data_in), .wr_en(wr_en), .rd_en(rd_en), .full(full), .empty(empty), .data_out(data_out) ); initial begin clk = 1'b0; wr_en = 1'b0; rd_en = 1'b0; rst_n = 1'b1; #20 rst_n = 1'b0; #20 rst_n = 1'b1; data_in = 8'd1; #20 data_in = 8'd2; #40 wr_en = 1'b1; #20 data_in = 8'd3; #20 data_in = 8'd2; #20 data_in = 8'd2; #260 wr_en = 1'b0; #10 rd_en = 1'b1; end always #10 clk = ~clk; endmodule
参考
同步FIFO学习 - IC新手 - 博客园 (cnblogs.com)
(13条消息) FPGA基础知识极简教程(3)从FIFO设计讲起之同步FIFO篇_Reborn Lee-CSDN博客_fifo缓冲区作用
同步FIFO的实现(从verilog代码到波形) - 知乎 (zhihu.com)
手写同步FIFO - 知乎 (zhihu.com)
Register based FIFO in VHDL (nandland.com)//vhdl版本
同步FIFO笔记 - 知乎 (zhihu.com)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 百万级群聊的设计实践
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期