基于FPGA的模拟I²C协议系统设计

描述

导读

I²C(Inter-Integrated Circuit),其实是 I²C Bus 简称,中文就是集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年10月1日起,使用 I²C 协议已经不需要支付专利费,但制造商仍然需要付费以获取 I²C 从属设备地址。

I²C 简单来说,就是一种串行通信协议,I²C的通信协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 I²C 协议占用的 IO 资源特别少,连接方便,所以工程中也常选用 I²C 接口做为不同芯片间的通信协议。I²C 串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到 I²C 总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。

在现代电子系统中,有为数众多的 IC 需要进行相互之间以及与外界的通信。为了简化电路的设计,Philips 公司开发了一种用于内部 IC 控制的简单的双向两线串行总线 I²C(Intel-Integrated Circuit bus)。1998 年当推出 I²C 总线协议 2.0 版本时,I²C 协议实际上已经成为一个国际标准。

在进行 FPGA 设计时,经常需要和外围提供 I²C 接口的芯片通信。例如低功耗的 CMOS 实时时钟/日历芯片 PCF8563、LCD 驱动芯片 PCF8562、并行口扩展芯片 PCF8574、键盘/LED 驱动器 ZLG7290 等都提供 I²C 接口。因此在 FPGA 中模拟 I²C 接口已成为 FPGA 开发必要的步骤。

本篇将详细讲解在 FPGA 芯片中使用 VHDL/Verilog HDL 模拟 I²C 协议,以及编写 TestBench仿真和测试程序的方法。

第三篇内容摘要:本篇会介绍程序的仿真与测试,包括主节点的仿真、从节点的仿真、仿真主程序、仿真结果以及总结等相关内容。

四、程序的仿真与测试

I²C 协议的模拟程序完成后,还需要通过仿真程序对程序的功能进行测试。对本程序的仿真包括 3 个部分:第一部分是主节点的仿真,模拟数据读/写;第二部分是从节点的仿真,模拟数据的接收和应答;第三部分是仿真主程序,负责整个仿真过程的控制。

4.1 主节点的仿真

主节点仿真的内容包括读数据、写数据和比较数据 3 部分,代码如下:

 

`include "timescale.v"
//模块定义
module wb_master_model(clk, rst, adr, din, dout, cyc, stb, we, sel, ack, err, rty);
    //参数
    parameter dwidth = 32;
    parameter awidth = 32;
    
    //输入、输出
    input clk, rst;
    output [awidth -1:0] adr;
    input [dwidth -1:0] din;
    output [dwidth -1:0] dout;
    output cyc, stb;
    output we;
    output [dwidth/8 -1:0] sel;
    input ack, err, rty;
    
    //WIRE 定义
    reg [awidth -1:0] adr;
    reg [dwidth -1:0] dout;
    reg cyc, stb;
    reg we;
    reg [dwidth/8 -1:0] sel;
    reg [dwidth -1:0] q;
    
    // 存储逻辑
    //初始化
    initial
        begin
            adr = {awidth{1'bx}};
            dout = {dwidth{1'bx}};
            cyc = 1'b0;
            stb = 1'bx;
            we = 1'hx;
            sel = {dwidth/8{1'bx}};
            #1;
        end
        
    // 写数据周期
    task wb_write;
        input delay;
        integer delay;
        input [awidth -1:0] a;
        input [dwidth -1:0] d;
        begin
            // 延迟
            repeat(delay) @(posedge clk);
            // 设置信号值
            #1;
            adr = a;
            dout = d;
            cyc = 1'b1;
            stb = 1'b1;
            we = 1'b1;
            sel = {dwidth/8{1'b1}};
            @(posedge clk);
            // 等待从节点的应答信号
            while(~ack) @(posedge clk);
            #1;
            cyc = 1'b0;
            stb = 1'bx;
            adr = {awidth{1'bx}};
            dout = {dwidth{1'bx}};
            we = 1'hx;
            sel = {dwidth/8{1'bx}};
        end
    endtask
    
    // 读数据周期
    task wb_read;
        input delay;
        integer delay;
        input [awidth -1:0]a;
        output [dwidth -1:0] d;
        begin
            // 延迟
            repeat(delay) @(posedge clk);
            // 设置信号值
            #1;
            adr = a;
            dout = {dwidth{1'bx}};
            cyc = 1'b1;
            stb = 1'b1;
            we = 1'b0;
            sel = {dwidth/8{1'b1}};
            @(posedge clk);
            // 等待从节点应答信号
            while(~ack) @(posedge clk);
            #1;
            cyc = 1'b0;
            stb = 1'bx;
            adr = {awidth{1'bx}};
            dout = {dwidth{1'bx}};
            we = 1'hx;
            sel = {dwidth/8{1'bx}};
            d = din;
        end
    endtask
    
    // 比较数据
    task wb_cmp;
        input delay;
        integer delay;
        input [awidth -1:0] a;
        input [dwidth -1:0] d_exp;
        begin
            wb_read (delay, a, q);
            if (d_exp !== q)
            $display("Data compare error. Received %h, expected %h at time %t", q, d_exp,$time);
        end
    endtask
endmodule

 

4.2 从节点的仿真

从节点仿真程序需要模拟从主节点接收数据,并发出应答信号,代码如下:

 

`include "timescale.v"
//模块定义
module i2c_slave_model (scl, sda);
    // 参数
    // 地址
    parameter I2C_ADR = 7'b001_0000;
    
    // 输入、输出
    input scl;
    inout sda;
    
    // 变量申明
    wire debug = 1'b1;
    reg [7:0] mem [3:0]; // 初始化内存
    reg [7:0] mem_adr; // 内存地址
    reg [7:0] mem_do; // 内存数据输出
    reg sta, d_sta;
    reg sto, d_sto;
    reg [7:0] sr; // 8 位移位寄存器
    reg rw; // 读写方向
    wire my_adr; // 地址
    wire i2c_reset; // RESET 信号
    reg [2:0] bit_cnt;
    wire acc_done; // 传输完成
    reg ld;
    reg sda_o;
    wire sda_dly;
    
    // 状态机的状态定义
    parameter idle = 3'b000;
    parameter slave_ack = 3'b001;
    parameter get_mem_adr = 3'b010;
    parameter gma_ack = 3'b011;
    parameter data = 3'b100;
    parameter data_ack = 3'b101;
    reg [2:0] state;
    
    // 模块主体
    //初始化
    initial
        begin
            sda_o = 1'b1;
            state = idle;
        end
        
    // 产生移位寄存器
    always @(posedge scl)
        sr <= #1 {sr[6:0],sda};
        
    //检测到访问地址与从节点一致
    assign my_adr = (sr[7:1] == I2C_ADR);
    
    //产生位寄存器
    always @(posedge scl)
        if(ld)
            bit_cnt <= #1 3'b111;
        else
            bit_cnt <= #1 bit_cnt - 3'h1;
            
    //产生访问结束标志
    assign acc_done = !(|bit_cnt);
    
    // sda 延迟
    assign #1 sda_dly = sda;
    
    //检测到开始状态
    always @(negedge sda)
        if(scl)
            begin
                sta <= #1 1'b1;
                    if(debug)
                        $display("DEBUG i2c_slave; start condition detected at %t", $time);
            end
        else
            sta <= #1 1'b0;
            
    always @(posedge scl)
        d_sta <= #1 sta;
    
    // 检测到停止状态信号
    always @(posedge sda)
        if(scl)
            begin
                sto <= #1 1'b1;
                    if(debug)
                        $display("DEBUG i2c_slave; stop condition detected at %t", $time);
            end
        else
            sto <= #1 1'b0;
            
    //产生 I2C 的 RESET 信号
    assign i2c_reset = sta || sto;
    
    // 状态机
    always @(negedge scl or posedge sto)
        if (sto || (sta && !d_sta) )
            begin
                state <= #1 idle; // reset 状态机
                sda_o <= #1 1'b1;
                ld <= #1 1'b1;
            end
        else
            begin
            // 初始化
            sda_o <= #1 1'b1;
            ld <= #1 1'b0;
            case(state)
                idle: // idle 状态
                    if (acc_done && my_adr)
                        begin
                            state <= #1 slave_ack;
                            rw <= #1 sr[0];
                            sda_o <= #1 1'b0; // 产生应答信号
                            #2;
                            if(debug && rw)
                                $display("DEBUG i2c_slave; command byte received (read) at %t",$time);
                            if(debug && !rw)
                                $display("DEBUG i2c_slave; command byte received (write) at %t",$time);
                            if(rw)
                                begin
                                    mem_do <= #1 mem[mem_adr];
                                        if(debug)
                                            begin
                                                #2 $display("DEBUG i2c_slave; data block read %x from address %x (1)", mem_do, mem_adr);
                                                #2 $display("DEBUG i2c_slave; memcheck [0]=%x, [1]=%x, [2]=%x", mem[4'h0], mem[4'h1], mem[4'h2]);
                                            end
                                end
                        end
                    slave_ack:
                        begin
                            if(rw)
                                begin
                                    state <= #1 data;
                                    sda_o <= #1 mem_do[7];
                                end
                            else
                                state <= #1 get_mem_adr;
                                ld <= #1 1'b1;
                         end    
                         
                    get_mem_adr: // 等待内存地址
                        if(acc_done)
                            begin
                                state <= #1 gma_ack;
                                mem_adr <= #1 sr; // 保存内存地址
                                sda_o <= #1 !(sr <= 15); // 收到合法地址信号后发出应答信号
                            if(debug)
                                #1 $display("DEBUG i2c_slave; address received. adr=%x, ack=%b",sr, sda_o);
                            end
                            
                    gma_ack:
                        begin
                            state <= #1 data;
                            ld <= #1 1'b1;
                        end
                   
                    data: // 接收数据
                        begin
                            if(rw)
                                sda_o <= #1 mem_do[7];
                            if(acc_done)
                                begin
                                    state <= #1 data_ack;
                                    mem_adr <= #2 mem_adr + 8'h1;
                                    sda_o <= #1 (rw && (mem_adr <= 15) );
                                if(rw)
                                    begin
                                        #3 mem_do <= mem[mem_adr];
                                        if(debug)
                                            #5 $display("DEBUG i2c_slave; data block read %x from address %x (2)", mem_do, mem_adr);                                    
                                    end
                                if(!rw)
                                    begin
                                        mem[ mem_adr[3:0] ] <= #1 sr; // store data in memory
                                        if(debug)
                                            #2 $display("DEBUG i2c_slave; data block write %x to address %x", sr, mem_adr);
                                    end
                                end
                            end
                            
                        data_ack:
                            begin
                                ld <= #1 1'b1;
                                if(rw)
                                    if(sda) //
                                        begin
                                            state <= #1 idle;
                                            sda_o <= #1 1'b1;
                                        end
                                    else
                                        begin
                                            state <= #1 data;
                                            sda_o <= #1 mem_do[7];
                                        end
                                    else
                                        begin
                                            state <= #1 data;
                                            sda_o <= #1 1'b1;
                                        end
                                end
                            endcase
                        end
                        
    // 从内存读数据
    always @(posedge scl)
    if(!acc_done && rw)
    mem_do <= #1 {mem_do[6:0], 1'b1};
    
    // 产生三态
    assign sda = sda_o ? 1'bz : 1'b0;
    
    // 检查时序
    wire tst_sto = sto;
    wire tst_sta = sta;
    wire tst_scl = scl;
    
    //指定各个信号的上升沿和下降沿
    specify
        specparam normal_scl_low = 4700,
            normal_scl_high = 4000,
            normal_tsu_sta = 4700,
            normal_tsu_sto = 4000,
            normal_sta_sto = 4700,
            fast_scl_low = 1300,
            fast_scl_high = 600,
            fast_tsu_sta = 1300,
            fast_tsu_sto = 600,
            fast_sta_sto = 1300;
        $width(negedge scl, normal_scl_low);
        $width(posedge scl, normal_scl_high);
        $setup(negedge sda &&& scl, negedge scl, normal_tsu_sta); // 开始状态信号
        $setup(posedge scl, posedge sda &&& scl, normal_tsu_sto); // 停止状态信号
        $setup(posedge tst_sta, posedge tst_scl, normal_sta_sto);
    endspecify
    
endmodule

 

4.3 仿真主程序

仿真主程序完成主节点数据到从节点的控制,代码如下:

 

`include "timescale.v"
//模块定义
module tst_bench_top();
    //连线和寄存器
    reg clk;
    reg rstn;
    wire [31:0] adr;
    wire [ 7:0] dat_i, dat_o;
    wire we;
    wire stb;
    wire cyc;
    wire ack;
    wire inta;
    
    //q 保存状态寄存器内容
    reg [7:0] q, qq;
    wire scl, scl_o, scl_oen;
    wire sda, sda_o, sda_oen;
    
    //寄存器地址
    parameter PRER_LO = 3'b000; //分频寄存器低位地址
    parameter PRER_HI = 3'b001; //高位地址
    parameter CTR = 3'b010; //控制寄存器地址,(7)使能位|6 中断使能位|5-0其余保留位
    parameter RXR = 3'b011; //接收寄存器地址,(7)接收到的最后一个字节的数据
    parameter TXR = 3'b011; //传输寄存器地址,(7)传输地址时最后一位为读写位,1 为读
    parameter CR = 3'b100; //命令寄存器地址,
   
    //(7)开始|6 结束|5 读|4 写|3 应答(作为接收方时,发送应答信号,“0”为应答,“1”为不应答)|2 保留位|1 保留位|0 中断应答位,这八位自动清除
    parameter SR = 3'b100; //状态寄存器地址,(7)接收应答位(“0”为接收到应答)|6 忙位(产生开始信号后变为 1,结束信号后变为 0)|5 仲裁位|4-2 保留位|1 传输中位(1 表示正在传输数据,0 表示传输结束)|中断标志位
    parameter TXR_R = 3'b101;
    parameter CR_R = 3'b110;
    
    // 产生时钟信号,一个时间单位为 1ns,周期为 10ns,频率为 100MHz。
    always #5 clk = ~clk;
    
    //连接 master 模拟模块
    wb_master_model #(8, 32) u0 (
            .clk(clk), //时钟
            .rst(rstn), //重起
            .adr(adr), //地址
            .din(dat_i), //输入的数据
            .dout(dat_o), //输出的数据
            .cyc(cyc),
            .stb(stb),
            .we(we),
            .sel(),
            .ack(ack), //应答
            .err(1'b0),
            .rty(1'b0)
        );
    
    //连接 i2c 接口
    i2c_master_top i2c_top (
            //连接到 master 模拟模块部分
            .wb_clk_i(clk), //时钟
            .wb_rst_i(1'b0), //同步重起位
            .arst_i(rstn), //异步重起
            .wb_adr_i(adr[2:0]), //地址输入
            .wb_dat_i(dat_o), //数据输入接口
            .wb_dat_o(dat_i), //数据从接口输出
            .wb_we_i(we), //写使能信号
            .wb_stb_i(stb), //片选信号,应该一直为高
            .wb_cyc_i(cyc),
            .wb_ack_o(ack), //应答信号输出到 master 模拟模块
            .wb_inta_o(inta), //中断信号输出到 master 模拟模块
            
            //输出的 i2c 信号,连接到 slave 模拟模块
            .scl_pad_i(scl),
            .scl_pad_o(scl_o),
            .scl_padoen_o(scl_oen),
            .sda_pad_i(sda),
            .sda_pad_o(sda_o),
            .sda_padoen_o(sda_oen)
        );
    
    //连接到 slave 模拟模块
    i2c_slave_model #(7'b1010_000) i2c_slave (
            .scl(scl),
            .sda(sda)
        );
        
    //为 master 模拟模块产生 scl 和 sda 的三态缓冲
    assign scl = scl_oen ? 1'bz : scl_o; // create tri-state buffer for i2c_master scl line
    assign sda = sda_oen ? 1'bz : sda_o; // create tri-state buffer for i2c_master sda line
    
    //上拉
    pullup p1(scl); // pullup scl line
    pullup p2(sda); // pullup sda line
    
    //初始化
    initial
        begin
            $display("
 状态: %t I2C 接口测试开始!

", $time);
            // 初始值
            clk = 0;
            //重起系统
            rstn = 1'b1; // negate reset
            #2;
            rstn = 1'b0; // assert reset
            repeat(20) @(posedge clk);
            rstn = 1'b1; // negate reset
            $display("状态: %t 完成系统重起!", $time);
            @(posedge clk);
            // 对接口编程
            // 写内部寄存器
            // 分频 100M/100K*5=O'200=h'C8
            u0.wb_write(1, PRER_LO, 8'hc7);
            u0.wb_write(1, PRER_HI, 8'h00);
            $display("状态: %t 完成分频寄存器操作!", $time);
            //读分频寄存器内容
            u0.wb_cmp(0, PRER_LO, 8'hc8);
            u0.wb_cmp(0, PRER_HI, 8'h00);
            $display("状态: %t 完成分频寄存器确认操作!", $time);
            //接口使能
            u0.wb_write(1, CTR, 8'h80);
            $display("状态: %t 完成接口使能!", $time);
            // 驱动 slave 地址
            // h'a0=b'1010_0000,地址+写状态,写入的地址为 h'50
            u0.wb_write(1, TXR, 8'ha0);
            //命令内容为 b'1001_0000,产生开始位,并设置写状态
            u0.wb_write(0, CR, 8'h90);
            $display("状态: %t 产生开始位, 然后写命令 a0(地址+写),命令开始!", $time);
            // 检查状态位信息
            // 检查传输是否结束
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(0, SR, q);
            $display("状态: %t 地址驱动写操作完成!", $time);
            // 待写的地址为 h'01
            u0.wb_write(1, TXR, 8'h01);
            // 产生写命令 b'0001_0000
            u0.wb_write(0, CR, 8'h10);
            $display("状态: %t 待写地址为 01,命令开始!", $time);
            // 检查状态位
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(0, SR, q);
            $display("状态: %t 写操作完成!", $time);
            // 写入内容
            u0.wb_write(1, TXR, 8'ha5);
            u0.wb_write(0, CR, 8'h10);
            $display("状态: %t 写入内容为 a5,开始写入过程!", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 写 a5 到地址 h'01 中完成!", $time);
            // 写入下一个地址 5a
            u0.wb_write(1, TXR, 8'h5a); // present data
            // 写入并停止
            u0.wb_write(0, CR, 8'h50); // set command (stop, write)
            $display("状态: %t 写 5a 到下一个地址,产生停止位!", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q); // poll it until it is zero
            $display("状态: %t 写第二个地址结束!", $time);
            // 读
            // 驱动 slave 地址
            u0.wb_write(1, TXR, 8'ha0);
            u0.wb_write(0, CR, 8'h90);
            $display("状态: %t 产生开始位,写命令 a0 (slave 地址+write)", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q); // poll it until it is zero
            $display("状态: %t slave 地址驱动完成!", $time);
            // 发送地址
            u0.wb_write(1, TXR, 8'h01);
            u0.wb_write(0, CR, 8'h10);
            $display("状态: %t 发送地址 01!", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 地址发送完成!", $time);
            // 驱动 slave 地址,1010_0001,h'50+read
            u0.wb_write(1, TXR, 8'ha1);
            u0.wb_write(0, CR, 8'h90);
            $display("状态: %t 产生重复开始位, 读地址+开始位", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 命令结束!", $time);
            // 读数据
            u0.wb_write(1, CR, 8'h20);
            $display("状态: %t 读+应答命令", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 读结束!", $time);
            // 检查读的内容
            u0.wb_read(1, RXR, qq);
            if(qq !== 8'ha5)
                $display("
 错误: 需要的是 a5, received %x at time %t", qq, $time);
            // 读下一个地址内容
            u0.wb_write(1, CR, 8'h20);
            $display("状态: %t 读+ 应答", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 第二个地址读结束!", $time);
            u0.wb_read(1, RXR, qq);
            if(qq !== 8'h5a)
            $display("
 错误: 需要的是 5a, received %x at time %t", qq, $time);
            // 读
            u0.wb_write(1, CR, 8'h20);
            $display("状态: %t 读 + 应答", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 第三个地址读完成!", $time);
            u0.wb_read(1, RXR, qq);
            $display("状态: %t 第三个地址内容是 %x !", $time, qq);
            // 读
            u0.wb_write(1, CR, 8'h28);
            $display("状态: %t 读 + 不应答!", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 第四个地址读完成!", $time);
            u0.wb_read(1, RXR, qq);
            $display("状态: %t 第四个地址内容为 %x !", $time, qq);
            // 检查不存在的 slave 地址
            // drive slave address
            u0.wb_write(1, TXR, 8'ha0);
            u0.wb_write(0, CR, 8'h90);
            $display("状态: %t 产生开始位, 发送命令 a0 (slave 地址+写). 检查非法地址!",$time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q); // poll it until it is zero
            $display("状态: %t 命令结束!", $time);
            // 发送内存地址
            u0.wb_write(1, TXR, 8'h10);
            u0.wb_write(0, CR, 8'h10);
            $display("状态: %t 发送 slave 内存地址 10!", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q);
            $display("状态: %t 地址发送完毕!", $time);
            // slave 发送不应答
            $display("状态: %t 检查不应答位!", $time);
            if(!q[7])
            $display("
 错误: 需要 NACK, 接收到 ACK
");
            // 从 slave 读数据
            u0.wb_write(1, CR, 8'h40);
            $display("状态: %t 产生'stop'位", $time);
            u0.wb_read(1, SR, q);
            while(q[1])
                u0.wb_read(1, SR, q); // poll it until it is zero
            $display("状态: %t 结束!", $time);
            #25000; // wait 25us
            $display("

 状态: %t 测试结束!", $time);
            $finish;
        end
endmodule

 

4.4 仿真结果

在 ModelSim 中可以看到仿真的结果。如图 7 所示是发送开始状态并写地址“a0”时的图形,此时在图上表示为 SCL 处于高时 SDA 的一个下降沿,然后是数据“1010,0000”。

FPGA

图 7 发送开始信号并写地址 a0   如图 8 所示为发送数据“01”和“a5”时的图形,在图上表示为:数据“0000,0001”和“1010,0101”。  

FPGA

图 8 发送数据“01”和“a5”   如图 9 所示的是发送停止状态信号和数据“5a”时的图形,在图上表示为 SCL 处于高时SDA 的一个上升沿,然后是数据“0101,1010”。  

FPGA

图 9 发送停止状态信号和数据“5a”   仿真程序说明 I²C 程序符合 I²C 协议的时序和数据格式,可以实现模拟 I²C 协议的任务。

五、总结

本篇首先说明了 I²C 协议相关的内容,介绍协议基本概念和数据传输各个命令的具体含义以及协议对时序的要求。接下来介绍模拟 I²C 协议程序的框架,详细讲解框架中各个模块的功能并介绍详细代码。最后通过一个完成的仿真程序完成对程序的测试。I²C 在应用中有着广泛的用途,本篇希望通过这个例子为各位大侠提供一个可行的解决方案。





审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分