FPGA对EEPROM驱动控制(I2C协议)
本文摘要:本文首先对I2C协议的通信模式和AT24C16-EEPROM芯片时序控制进行分析和理解,设计了一个i2c通信方案。人为按下写操作按键后,FPGA(Altera EP4CE10)对EEPROM指定地址写入字节数据,并接后按下读操作按键,读取该地址上的一个字节数据在数码管低两位显示出来。其中包括了对此方案的Modelsim仿真测试,并且接续完成板级验证。(过程笔记)
关键词:EEPROM、I2C协议、Verilog HDL、FPGA
框图设计:
输入端口包括系统时钟、复位信号、写操作和读操作按键,输出端口包括IIC通信接口、数码管段选和位选信号。

共计用到了四种子模块,分别是按键消抖、i2c通信数据处理、i2c通信控制、2khz时钟分配。按逻辑链接成体,完成本文设计的测试方案。在调试过程中,为方便观察操作反应,另外加入了两个LED灯接口,具体框图如下。
【I2C协议通信模式】
I2C协议(通常称为IIC协议)是一种串行、同步、半双工通信协议。I2C协议支持多主机功能,允许多个具备主控能力的设备在同一总线上竞争控制权,并通过硬件仲裁机制避免冲突。其使用串行数据线(SDA)和串行时钟线(SCL)进行通信,标准模式的传输速率为100 kbit/s(Standard-mode, Sm),快速模式下的I2C传输速率提高至400 kbit/s(Fast-mode,Fm),在高速模式下可达3.4Mbit/s。
发送到SDA线上的每个字节必须为8位,且每次传输可以发送的字节数量不受限制,每个字节后必须跟一个ACK响应,位首先传输的是数据的最高位MSB,SDA线上的数据必须在SCL时钟的高电平周期保持稳定,数据线的高或低电平状态只有在SCL时钟是低电平时才能改变。

传输信号类型:(见上图)
- 启动信号(START)(S条件):在
SCL线处于高电平,SDA上的数据由高向低转换,则表示启动IIC总线; - 应答信号(ACK):在接收到了8bit的信息后, 接收数据一方需要向发送信息的另一方传递默认的低电平脉冲作为信号,表明已经获取了数据;
 - 结束信号(STOP)(P条件):在
SCL线处于高电平,SDA上的数据由低向高转换,则表示停止IIC总线。 
起始(S)和停止(P)条件一般由主机产生,总线在起始条件后被认为处于忙的状态,在停止条件的某段时间后总线被认为再次处于空闲状态 。
由于I2C 总线没有中央控制,其控制只能由地址或主机码以及竞争主机发送的数据决定,总线也没有任何定制的优先权。所以,当发生多主机通信时,需要一个控制仲裁机制来决定通信优先。
仲裁过程中的时钟同步:
在I2C通信中,所有主机在SCL线上生成自己的时钟信号以传输数据。数据仅在时钟高电平时有效,因此需要同步时钟以实现逐位仲裁。时钟同步通过线与连接实现,即SCL线状态由所有设备共同决定。

①见上图中,当SCL线从高变低时,所有设备开始计数其低电平周期。若某设备时钟先变低,则它会保持SCL线在低电平直到其时钟再次变高。若此时有其他设备仍处于低电平周期,它们的时钟变化不会改变SCL线状态,直至最长低电平周期的设备完成计数。
②一旦所有设备完成低电平周期,SCL线释放并变为高电平,随后所有设备同步开始计数高电平周期。
③首先完成高电平周期的设备将再次拉低SCL线。因此,SCL时钟的低电平周期由最长低电平周期的设备决定,而高电平周期由最短高电平周期的设备决定。(巧记:保证最长低电平周期)
仲裁判定优先:
主机只能在总线空闲时启动传输,两个或多个主机可能在起始条件的最小持续时间内产生一个起始条件,结果在总线上产生一个规范的起始条件(如下框S条件内)。当SCL线是高电平时,仲裁在SDA线发生,在其他主机保持低电平状态时,首先拉高电平的主机将断开数据输出级,如下图DATA1失去了通信的优先权。

仲裁可以持续多位,第一个阶段是比较地址位。 如果各主机都尝试寻址同一器件,仲裁机制会持续到数据位。在仲裁过程中不会丢失信息(仲裁区间有效信息一致),丢失仲裁的主机可以产生时钟脉冲直到丢失仲裁的该字节末尾 。
补充:如果主机也结合了从机功能,并且在寻址阶段丢失仲裁,由于它存在是赢得仲裁的主机所寻址的器件。因此,丢失仲裁的主机必须立即切换到从机模式。
【AT24C16时序分析】
AT24C16 EEPROM存储芯片的相关信息:存储容量为16Kbit,即2048字节。芯片内部分成128页,每页16字节,读写操作都是以字节为基本单位。AT24C16具有低电压工作、高速串行通信和硬件写保护等特点。(下图为芯片引脚说明)

- 地址组成:AT24C16的器件地址包括高4位固定地址(1010)和用户需设置的低3位地址(
A0、A1、A2); - 地址设置:通过连接芯片的
A0、A1、A2这3个引脚到VCC或GND来实现地址的低3位设置。例如这3个引脚均连接到VCC,则器件地址为1010_111。由于该 3 位只能组合出 8 种情况,因此,一个主机最多可以连接8个AT24C16存储芯片。 
1、EEPROM驱动写时序进行分析

字节写入时序 (Byte Write):通信开始,由主机发送一个起始条件。紧接着,主机发送EEPROM的设备地址,选择目标EEPROM芯片。如果EEPROM支持字地址寻址,主机将接续发送一个或多个字节的字地址来指定要写入数据的内存位置。然后,主机写入数据字节,最高有效位(MSB)首先发送。 EEPROM在接收到每个字节后,返回一个应答信号(ACK),应答信号是低电平脉冲,表示已成功接收字节并准备接收下一个字节或停止信号。待所有数据字节都发送完毕后,主机发送停止信号(STOP)来结束目前的通信。
页写入时序 (Page Write):与字节写入时序相比,页写入时序类似,但数据被分组为多个字节,作为一个连续的数据块发送。 发送起始设备地址后,接续发送一系列的数据字节(DATA (n), DATA (n + 1),......, DATA (n + x))单个字节发送完成,EEPROM都会返回一个应答信号(ACK)。
补充:所有I2C设备均支持单字节数据写入操作,但只有部分I2C设备支持页写操作;且支持页写操作的设备,一次页写操作写入的字节数不能超过设备单页包含的存储单元数。
2、读时序进行分析
IIC协议支持三种EEPROM读时序。首先是指定地址单字节读取方式:操作时序和写时序类似,不同的是,在写入目标地址后,主机的操作方式换为读取。

顺序读取时序:主机发送一个起始地址后,EEPROM开始连续发送数据(DATA n, DATA n+1, DATA n+2, ... DATA n+X)。在每个数据字节的末尾,EEPROM同样等待主机的应答信号(ACK)。主机在接收到每个数据字节后发送ACK信号,直到所有数据都被接收。当所有数据发送完毕或主机决定停止时,它会发送一个停止信号(STOP),结束顺序读取操作。
随机读取时序:起始条件后,发送设备地址和读写位(R/W=1),接着发送随机地址。EEPROM发送数据,主机接收后发送ACK。所有数据发送完毕后,发送停止信号。EEPROM在接收到有效地址后,通过SDA线连续发送数据(DATA n, DATA n+1, ...)。在每个数据字节的末尾,EEPROM等待主机的应答信号(ACK)。主机待接收到数据字节后,通过发送ACK信号来确认接收,并告知EEPROM可以继续发送下一个数据字节。
【时序逻辑设计方案】
1、时钟处理与i2c通信启动
系统时钟频率为50Mhz,频率很高,这里首先需要从系统时钟分频提供一个1Mhz的i2c_clk时钟用于i2c通信处理。下图中cnt_clk为一个时钟分频计数器。

以写操作为例,待写触发信号write发生,拉高写有效信号write_valid,并且为使i2c_clk时钟信号要在上升沿检测到其高电平,write触发要保持≥2个时钟周期,对应100个系统时钟周期。这里设定有效计数器cnt_wr,计数150个系统周期,即300ns,满足要求。计数完成,拉低写有效信号write_valid完成触发操作,具体时序见下图,当然,写有效时序也是如此。

写有效信号write_valid触发,在其拉高的下一个i2c_clk时钟上升沿,触发写使能wr_en信号,并且,启动cnt_start计数器,计数值为5000(1Mhz ->1us)≈ 5ms。这是因为AT64C16单次操作间隔周期需要保持5ms空闲状态。完成计数后,启动i2c_start起始信号触发,后面就是i2c通信的具体流程。当完成操作后,i2c_end结束信号标志通信完成,从而拉低写使能wr_en信号。过程中,设定了写操作的目标地址为16'H00_4D,写入数据为8'H8A。

注意,大部分信号时钟触发源都是i2c_clk,而不是系统时钟。当i2c_start起始信号触发,i2c_clk启动,作为i2c_scl通信时钟的触发源。同时,根据下图逻辑,状态机状态由IDLE转到START1。起始信号仅占一个i2c_clk周期。cnt_i2c_clk是i2c_scl通信时钟线的计数器,计数值范围0-3,使得i2c_scl周期为250khz。

完成后,进入SEND_ADDR状态,向IIC总线发生器件地址1010011+控制位0,表示写入。比特位计数器cnt_bit用于比特位0-7的计数。在ACK1状态,等待总线回应。ACK是从机,即EEPROM,传回来的低电平信号。对于主机来说SDA线此时是高阻态,总线的上拉电阻将电位钳位在高电位(sda为inout类型)。所以在后面,可以看到这里是处于高阻态的。通信后面的流程根据状态机的跳转,在时序图表现得明显。在STOP停止状态,FPGA向EEPROM发送停止信号,一次单字节数据写操作完成,拉低使能信号i2c_clk_en和we_en,并且及时触发i2c_end,随后状态机跳回IDLE初始状态。

读控制时序处理与写时序处理流程类似,具体见上图,不同在ACK3状态的跳转,接收一个周期回应信号后,进入的是起始状态START2,从而触发第二次起始信号,再次写入器件地址,读取一字节数据。rd_data_reg作为一个暂存器,存储读取到的字节数据,待完成后,转录到rd_data。在N_ACK等待回应一个高电平回应信号,将i2c_scl由地拉高产生一个停止信号,同时触发i2c_end。两个操作过程具体状态判断条件,需具体分析,但都类似。
2、STATE状态机转换逻辑
i2c整个通信的过程可以分为很多阶段,将它们细分开:空闲、起始、器件地址写入、字/字节地址写入、各级响应、字节数据写入、读取字节数据、停止。主机视角对从设备操作过程,具体可见如下。

设备启动,主机处于空闲状态IDLE,待检测到写/读操作信号后,程序转入到第一个起始状态START_1,表现为SDA线产生一个由高到低的电平转换信号。接续进入器件地址写入状态SEND_ADDR,根据硬件图,这里预先设定器件是1010_011。器件确认后,向主机发生一个低电平回应ACK_1。寄存器存储地址类型分为字地址和字节地址,分别对应不同的状态切换。完成后ACK_3响应后,写操作,进入字节数据写入状态WR_DATA,写入一个预先数据,并回应ACK_4进入结束停止状态STOP。而ACK_3响应后,进入读操作流程,需再次发生起始START_2,接后写入读取目标地址,等待从机完成数据发送后,进入结束停止状态STOP。最终保持一定周期,再次转换至空闲状态IDLE。
根据时序图逻辑,确定状态机状态转换条件:
| 当前状态 | 目标跳转状态 | 跳转条件 | 操作类型 | 
|---|---|---|---|
| IDLE | START1 | i2c_start拉高 | 读/写 | 
| START1 | SEND_ADDR | cnt_i2c_clk == 2'd3(一个i2c_clk周期 ) | 读/写 | 
| SEND_ADDR | ACK1 | (cnt_i2c_clk == 2'd3)&&(!ack) | 读/写 | 
| ACK1 | SEND_BH | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 读/写 | 
| SEND_BH | ACK2 | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 读/写 | 
| ACK2 | SEND_BL | (cnt_i2c_clk == 2'd3)&&(!ack) | 读/写 | 
| SEND_BL | ACK3 | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 读/写 | 
| ACK3 | WR_DATA | (cnt_i2c_clk == 2'd3)&&(!ack) | 读/写 | 
| WR_DATA | ACK4 | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 写 | 
| ACK4 | STOP | (cnt_i2c_clk == 2'd3)&&(!ack) | 写 | 
| ACK3 | START2 | rd_en拉高 | 读 | 
| START2 | SEND_RA | cnt_i2c_clk == 2'd3 | 读 | 
| SEND_RA | ACK5 | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 读 | 
| ACK5 | RD_DATA | (cnt_i2c_clk == 2'd3)&&(!ack) | 读 | 
| RD_DATA | N_ACK | (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) | 读 | 
| N_ACK | STOP | cnt_i2c_clk == 2'd3 | 读 | 
| STOP | IDLE | (cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3) | 读/写 | 
对上面的表格可以总结,ACK1 - ACK5条件都是相同的,只是跳转目标不同,单字节读/写结束后状态跳转判断条件相同,两个起始状态跳转判断条件也是相同的 ,N_ACK和STOP的跳转注意区别。跳转机Verilog HDL具体程序如下:
//dispose state condition
always @(posedge i2c_clk or negedge sys_rst)begin
    if(!sys_rst) state <= IDLE;
    else case(state)
        IDLE:   if(i2c_start) state <= START1;else state<= state;
        START1:  if(cnt_i2c_clk == 2'd3) state <= SEND_ADDR;
                else state <= state;
        SEND_ADDR:
                if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                        state <= ACK1;
                else state <= state;
        ACK1:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(addr_num)state <= SEND_BH;
                    else state<= SEND_BL;
                end else state <= state;
        SEND_BH:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK2;
                else state<= state;
        ACK2:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= SEND_BL;
                else state <= state;
        SEND_BL:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK3;
                else state <= state;
        ACK3:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(wr_en) state <= WR_DATA;
                    else if(rd_en) state <= START2;
                end
                else state <= state;
        WR_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK4;
                else state <= state;
        ACK4:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= STOP;
                else state <= state;
        START2: if(cnt_i2c_clk == 2'd3) state <= SEND_RA;
                else state <= state;
        SEND_RA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK5;
                else state <= state;
        ACK5:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= RD_DATA;
                else state <= state;
        RD_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= N_ACK;
                else state <= state;
        N_ACK:  if(cnt_i2c_clk == 2'd3)
                    state <= STOP;
                else state <= state;
        STOP:   if((cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3))
                    state <= IDLE;
                else state <= state;
    endcase
end
STATE状态机在整个程序过程中十分重要,i2c_scl时钟电平和i2c_sda输出电平情况,也要根据其状态的不同做出具体分析。
//dispose i2c_scl sequence
always@(*)begin
    case(state)
        IDLE: i2c_scl <= 1'b1;
        START1:
            if(cnt_i2c_clk == 2'd3) i2c_scl <= 1'b0;
            else i2c_scl <= 1'b1;
        SEND_ADDR,ACK1,SEND_BH,ACK2,SEND_BL,
        ACK3,WR_DATA,ACK4,START2,SEND_RA,ACK5,RD_DATA,N_ACK:
            if((cnt_i2c_clk == 2'd1) || (cnt_i2c_clk == 2'd2)) i2c_scl <= 1'b1;
            else i2c_scl <= 1'b0;
        STOP:
            if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0)) i2c_scl <=  1'b0;
            else i2c_scl <=  1'b1;
        default:    i2c_scl <=  1'b1;
    endcase
end
//dispose i2c_sda_reg & rd_data_reg sequence
always @(*)begin
    case(state)
        IDLE:       begin
                        i2c_sda_reg <= 1'b1;
                        rd_data_reg  <= 8'd0;
                    end
        START1:     if(cnt_i2c_clk == 2'd0) i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_ADDR:  if(cnt_bit <= 3'd6) i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b0;
        ACK1:       i2c_sda_reg<= 1'b1;
        SEND_BH:    i2c_sda_reg<= byte_addr[15-cnt_bit];
        ACK2:       i2c_sda_reg<= 1'b1;
        SEND_BL:    i2c_sda_reg<= byte_addr[7-cnt_bit];
        ACK3:       i2c_sda_reg<= 1'b1;
        WR_DATA:    i2c_sda_reg<= wr_data[7-cnt_bit];
        ACK4:       i2c_sda_reg<= 1'b1;
        START2:     if(cnt_i2c_clk <= 2'd1)i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_RA:    if(cnt_bit <= 3'd6)i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b1;
        ACK5:       i2c_sda_reg<= 1'b1;
        RD_DATA:    if(cnt_i2c_clk  == 2'd2) rd_data_reg[7-cnt_bit] <= sda_in;
                    else rd_data_reg <= rd_data_reg;
        N_ACK:      i2c_sda_reg<= 1'b1;
        STOP:       if((cnt_bit==3'd0)&&(cnt_i2c_clk<2'd3))i2c_sda_reg <= 1'b0;
                    else i2c_sda_reg<= 1'b1;
        default:    begin
                        i2c_sda_reg<= 1'b1;
                        rd_data_reg <=  rd_data_reg;
                    end
    endcase
end
其他信号在此不再列举,它们的判断条件相对状态机来说处理起来比较简单,逻辑也很清晰。
【阶段仿真验证】
下面看下程序的仿真结果,查看四个:读/写操作启动时刻、写操作整体时序、读操作整体时序、读/写操作结束时序。
操作启动:write信号发生,write_valid拉高,并在下一i2c_clk上升沿拉高wr_en使能信号,等待cnt_wr完成计数150后,放低write_valid。cnt_start待wr_en拉高后,开始计数。

写操作(这里器件地址设定1010_011):cnt_start计数到4900处,发起i2c_start触发,state值变为4'h01,表示IDLE进入START1状态,依次完成后续的状态切换。STOP状态下,待cnt_bit保持到3‘h3,结束停止状态返回IDLE。wr_en拉低,i2c_end拉高一个周期后放下。从下面的仿真图可以看到,整个写操作的时序表现正常。

读操作及读操作结束仿真图如下:


【最终上机验证】

Quartus II生成框图如下,两个按键做为输入触发,输出包括IIC通信总线SDA和SCL,数码管段选SEG_SEL和位选SEG_LED,以及外部挂载的两个LED。程序烧录后,设备低二位数码管显示00,LED均处于熄灭状态,按下key_wr后,led_wr点亮,表示写操作启动;接续,按下key_rd后,led_rd点亮,表示读操作启动,并且数码管显示读取数据8A。最终得到的现象与预期一致。

文献参考:
[1] I2C总线规范 (https://sumcu.suda.edu.cn/_upload/article/files/74/e5/d4eb93de45808d71ad8aad542ede/a3cb5873-aaf4-4af0-9e5f-521793fbba46.pdf);
[2] 基于I2C协议的EEPROM驱动控制(https://doc.embedfire.com/fpga/altera/ep4ce10_mini/zh/latest/fpga/I2C.html);
[3] 王荣华. 可配置的IIC协议控制器IP核的设计[D]. 黑龙江:哈尔滨理工大学,2011.( DOI:10.7666/d.y2012472);
本篇文章中使用的Verilog程序模块,若有需见网页左栏Gitee仓库链接:https://gitee.com/silly-big-head/little-mouse-funnyhouse/tree/FPGA-Verilog/
FPGA对EEPROM驱动控制(I2C协议)的更多相关文章
- 硬件和软件兼容i2c协议的24Cxx系列EEPROM存储器(转)
		
源:硬件和软件兼容i2c协议的24Cxx系列EEPROM存储器 硬件上由于24c01的A0A1A2管脚不允许悬空,故暂时的想法是兼容24c02 ---24c16 使用一个dip8封装的芯片插座,A0 ...
 - 模拟I2C协议学习点滴之原理框架
		
I2C是一种串行总线协议. 目前几种常用的串行总线有UART.SPI和I2C协议.UART协议的总线只有两条,发送(Transmit:TX)和接收(Receive:RX),没有时钟信号,这就要求两位数 ...
 - 和菜鸟一起学linux总线驱动之i2c死锁问题
		
不知不觉中已经有好几个月没有写点东西了,懒了就是懒了,说是忙着想把产品做得更好,都是借口,每天花一点时间来写点东西确实很不错,自己也坚持了很久很久,只不过今年以来,发现提高不是很大,能写的东西好少好少 ...
 - GPIO实现I2C协议模拟(1)
		
最近需要用GPIO模拟I2C协议,如果是在Linux下面比较简单,但在Windows下面,是否有没Linux那么简单了. 索性自己对I2C协议还有一些了解,翻了SPEC并结合示波器量出的实际信号分析, ...
 - I2C协议简介
		
主从芯片如何传输数据 AT24C02是一个存储芯片,需要把数据从ARM板发给AT24C02,也需要从AT24C02读取数据. I2C是一个主从结构,Master发起传输,slave接收或回应 一主多从 ...
 - Linux 驱动框架---i2c驱动框架
		
i2c驱动在Linux通过一个周的学习后发现i2c总线的驱动框架还是和Linux整体的驱动框架是相同的,思想并不特殊比较复杂的内容如i2c核心的内容都是内核驱动框架实现完成的,今天我们暂时只分析驱动开 ...
 - i2c协议
		
i2c协议 http://blog.csdn.net/g_salamander/article/details/8016698 总线设备驱动模型 http://blog.csdn.net/u01395 ...
 - i2c协议简要分析(转载)
		
声明 本文大部分内容为转载,因此标定为转载 源地址: http://www.cnblogs.com/zym0805/archive/2011/07/31/2122890.html http://blo ...
 - (原创) 巩固理解I2C协议(MCU,经验)
		
题外话:这几天天气突然转冷了.今天已是11月23日了,查查黄历,昨天(11月22日)刚好是小雪,一夜温度骤降,果然老祖先的经验有灵验!冬天来了,还是多加加衣服,注意保暖! 1.Abstract ...
 - I2C协议(转)
		
1.I2C协议 2条双向串行线,一条数据线SDA,一条时钟线SCL. SDA传输数据是大端传输,每次传输8bit,即一字节. 支持多主控(multimastering),任何时间点只能有一 ...
 
随机推荐
- C#的基于.net framework的Dll模块编程(四) - 编程手把手系列文章
			
这次继续这个系列的介绍: 一.命名空间的起名: 对于C#来说,一般命名空间的建议是:公司名(或个人名称).产品名.分类名,比如我这边是用的这个:Lzhdim.LPF.Helper,意思是个人名Lzhd ...
 - CRAPS赌博小游戏
			
游戏规则 代码实现 首先把这个规则用代码写出来 再在它基础上进行简单的可视化(主要是利用Easygui的界面) 最后查缺补漏,看看有没有什么Bug 利用pyinstaller -F -w -i xx. ...
 - 【项目学习】Timeswap:第一个完全去中心化的基于 AMM 的货币市场协议
			
总览 Timeswap 是世界上第一个完全去中心化的基于 AMM 的货币市场协议,无需预言机或清算人即可工作. Timeswap 采用 3 变量来维持 AMM 的运作.它通过允许用户决定他们的风险状况 ...
 - 企业微信群机器人发送消息(三)java端如何控制
			
1.先在群里添加机器人,然后获取机器人的webhook地址: 假设webhook是:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693a9 ...
 - iframe 高度设置为0时还有占位_iframe占位
			
iframe是一个内联元素,默认是跟baseline对齐的,iframe后边有个看不见.摸不着的行内空白节点,空白节点占据着高度,iframe与空白节点的基线对齐,导致了div被撑开,从而出现滚动条, ...
 - CF933-Div3 大致思路+题解
			
\(Rank\) A - Rudolf and the Ticket 纯水题 暴力枚举直接过 $code$ #include<bits/stdc++.h> #define fo(x,y,z ...
 - 网络拓扑—DNS服务搭建
			
目录 DNS服务搭建 网络拓扑 配置网络 DNS PC 安装DNS服务 配置DNS服务 创建正向查找区域 创建反向查找区域 创建子域名 PC机DNS域名解析 DNS服务搭建 网络拓扑 为了节省我的U盘 ...
 - 网络拓扑—NAT内外网映射
			
使用Windows Server 2003 网络拓扑 Router 外网:NATIP 网段 = 192.168.17.0/24 内网:仅主机模式IP = 172.16.29.4 Client1:仅主机 ...
 - WPF使用Shape实现复杂线条动画
			
看到巧用 CSS/SVG 实现复杂线条光效动画的文章,便也想尝试用WPF的Shape配合动画实现同样的效果.ChokCoco大佬的文章中介绍了基于SVG的线条动画效果和通过角向渐变配合 MASK 实现 ...
 - 推荐几款卓越的 .NET 开源搜索组件
			
前言 在当今日益数据化的世界中,信息的检索和搜索功能对于各种应用来说变得至关重要. 无论是电子商务网站.企业资源规划系统.还是内容管理系统,高效的搜索功能都是提升用户体验.促进业务发展的关键. 因此, ...