赞
踩
虽然上期说了接下来要更新SPI,但是由于我的板子没有SPI外设,只仿真也太不像话了,所以还是先写I2C了,等买个小SPI模块再更新SPI。
I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。
对于硬件设计人员来说,只需要2个管脚,极少的连接线和面积,就可以实现芯片间的通讯,对于软件开发者来说,可以使用同一个I2C驱动库,来实现实现不同器件的驱动,大大减少了软件的开发时间。极低的工作电流,降低了系统的功耗,完善的应答机制大大增强通讯的可靠性。
I2C也是一种低速接口,常见工作速率主要有以下五种:
1.标准模式(Standard):不超过100Kbps
2.快速模式(Fast):不超过400Kbps
3.快速模式+(Fast-Plus):不超过1Mbps
4.高速模式(High-Speed):不超过3.4Mbps
5.超快模式(Ultra-Fast):不超过5Mbps(单向传输)
这里的速度单位bps是比特每秒,指数据传输的速率,但是由于I2C总线每个时钟周期只传输一比特数据,所以这个速率也等于I2C总线时钟的频率,即Hz。
由于I2C总线的时序比起串口来说较为复杂,本人对于I2C的时序有一些个人的理解,本期就粗略的介绍一下I2C的时序,在讲解时序之前先明确一些定义:
MASTER:主机,读写操作的发起方。
SLAVE:从机,读写操作的接收方。
SCL:I2C总线的时钟信号,由主机产生,接上拉电阻,空闲为高电平。
SDA:I2C总线的数据信号,接上拉电阻,空闲为高电平。
START:SCL为高时SDA的下降沿。
STOP:SCL为高时SDA的上升沿。
ACK:SDA低电平。
NO_ACK:SDA高电平。
I2C总线一次写入的时序如下:
START为起始信号,SDA下降沿以后,在SCL的低电平发送数据,图中第一个字节叫做CONTROL BYTE,是由七位从机器件ID和一位读写控制位构成的,至于图中为什么是1010xxB,主要是一般从机地址都是1010xxx;最后一位0是读写控制位,0代表写,1代表读。最后有一位响应位(ACK),代表从机接收到主机发送的内容并示意主机继续。
下一个字节开始就是数据操作了,图中把这个字节叫做字地址,但是地址归根结底也是一个八位数据,发送完一个字节的数据又是一个响应位,从机成功响应后接着发下一个数据。只要从机一直响应,主机可以一直发下去,直到从机响应失败,或者主机不想继续发送了,主动产生一个停止位。
I2C总线读时序:
读时序和写时序类似,也是先发器件ID和读写标志,然后进行数据操作,但是请注意,可以看到第一个起始信号后的读写信号是0,代表本次操作是写操作,然后下一个START之后的读写控制位才是1,代表本次操作才是读操作,因为第一次操作要先发送读取的地址,第二次操作再从刚刚发送的地址处读出数据。
很多文章将这一整个时序视为I2C的一次读操作,第二次的START叫做RESTART即重启信号,但是这其实就是一次写操作和一次读操作,事实证明,写完地址过一段时间再进行读操作,也能正确的读出该地址的数据,所以也没有什么RESTART,就是短时间内的第二次START,但是为了操作方便,还是按照这种方式来编写代码。
所以I2C的时序其实很简单,启动后第一个字节发送器件ID和读写控制位,等待响应,响应后根据读写控制位来进入写操作或读操作,每操作一个字节就等待响应,响应成功后进行下一个字节的操作,直到响应失败或主机停止。
上期说过verilog的编写思路主要就是计数器和状态机,根据上一部分的讲解,可以看到I2C的时序还是稍微复杂一点,只用计数器实现还是很不方便的,于是这次我将用状态机架构来实现I2C的主机和从机。
根据第一部分的时序总结,可以将一次读写操作分为以下几个状态:
平时为空闲状态(IDLE),开始信号(START/RESTART),器件ID和读写标志(DEVICE_ID),响应(ACK),写数据(WRITE),读数据(READ),结束信号(DONE)。
根据这个状态机做出波形图如下:
其中还有一个比特计数器,来计数当前操作的比特。
这个状态机已经涵盖了I2C所有可能的状态,但是响应(ACK)是双向的,当主机写的时候,是从机响应,当主机读的时候,是主机响应,所以还要将响应(ACK)分为主机响应(M_ACK)和从机响应(S_ACK),总共就是九种状态。
编写状态机的代码,从绘制状态转移图开始:
然后根据上图编写状态机即可。
编写状态机时一般都采用三段式状态机,具体什么是三段式状态机本文就不多说了,状态机编码除非状态多的离谱不然一般都采用独热码。
SDA的时序基本已解决,接下来看SCL的时序,I2C协议规定在SCL的低电平更新数据,在SCL的高电平采样数据,而且一般都是在低电平的最中心更新数据,所以很多教程都是用计数器生成SCL的过程中在中心位置产生脉冲来作为更新数据和采样数据的信号,但是实际器件很多时候并不需要那么准确的在中心位置更新和采样,比如我读写过的EEPROM都是直接取SCL下降沿作为ACK的开始输出低电平,下一个下降沿再释放总线,所以最好让数据更新点可调,可以满足各种情况,复用性高。
对此我们可以直接在SCL的变化沿进行数据操作,然后将输出的SCL进行一定周期的延时,这样我们可以自由的控制输出SCL的相位,还能简化代码编写的过程。
如上图,可以看出来只要在SCL每个周期的上升沿更新数据,下降沿采样数据即可,而起始位和结束位同样在下降沿。
上述内容主要是编写I2C主机的思路,为了将协议完整掌握,我们还要编写一个从机,从机一般都是用来和寄存器空间对接的,所以用户接口可以留ram接口,方便操作内存,时序方面类比主机,只不过要更精简一点,因为大部分时间都是主机在操作总线,所以像START和DONE这种状态从机就不需要了。
附上状态转移图:
其中中间第三行的状态是WAIT,没注意被遮住了,我的我的。
旁边是整理思路的时候写的,有些是错的,建议别看,以上就是编写思路,接下来介绍根据以上思路编写的代码。
我编写接口的理念是结构简单,复用性高,对于I2C主机,首先想到需要兼容的就是总线速度,所以要将I2C总线的速率参数化,除了速度,I2C还有个7位地址和10位地址的区别,不过这个问题不是问题,不管是7位地址还是10位地址,I2C总线的操作时序都是没有变化的,只是前两个字节都需要发送器件ID,这种事交给发送控制端去考虑就好,接口只需要负责兼容。
除此以外还有前文提到的SCL延时功能,也可以通过参数配置。
端口部分留下和控制器交互的操作启动输入,操作结束输出,操作长度,I2C必须的读写标志,器件ID,还有和fifo交互的数据接口即可。
综上所述,I2C主机的端口如下:
- module i2c_master #(
- parameter SYS_CLK = 50_000_000, // 输入时钟周期,单位为 Hz
- parameter IIC_FREQ = 100_000, // 总线速度,单位为 Hz
- parameter SCL_DELAY_OW = 0, // 时钟线延延时重载使能
- parameter SCL_DELAY_USR = 0 // 时钟线用户输入延时,单位为 周期(输入时钟)
- )(
- input clk, // 输入时钟
- input rst_n, // 同步复位
-
- input op_start, // 启动信号
- output op_done, // 结束信号
- input [3:0] wr_len, // 写操作长度
- input [3:0] rd_len, // 读操作长度
- input [1:0] rw_flag, // 读写标志 1 : read, 0 : write.
- input [6:0] device_id, // 器件ID
- output dreq, // 数据请求
- input [7:0] din, // 数据输入
- output dvld, // 数据有效
- output [7:0] dout, // 数据输出
-
- output wire scl_out, // IIC时钟输出
- output wire scl_ctrl, // IIC时钟线控制
- input wire sda_in, // IIC数据输入
- output wire sda_out, // IIC数据输出
- output wire sda_ctrl // IIC数据线控制
- );
可以看到端口声明中I2C的接口并不是scl和sda,而是分成了输入输出和控制,这是因为I2C总线是多主多从的协议,总线在不用的时候就要释放掉以便其他器件操作总线,所以scl和sda都是以inout的形式接入FPGA的,对于inout port,规范的操作是在top module把它分成in,out和tri三个信号接入其他模块,具体操作的代码是:
- assign scl = scl_ctrl ? scl_out : 1'bz;
- assign sda = sda_ctrl ? sda_out : 1'bz;
- assign scl_in = scl;
- assign sda_in = sda;
然后是变量声明和组合逻辑,代码如下:
- // -------------------- Declaration --------------------
-
- // SCL相对于输入时钟的计数
- localparam SCL_CYCLE = SYS_CLK / IIC_FREQ;
- // 实际SCL延迟,当重载参数为1时选用用户定义的延迟,否则使用默认的1/4计数延迟
- localparam SCL_DELAY = SCL_DELAY_OW ? SCL_DELAY_USR : SCL_CYCLE/4 - 1;
-
- // 状态机编码
- localparam IDLE = 8'b0000_0000;
- localparam START = 8'b0000_0001;
- localparam RE_ST = 8'b0000_0010;
- localparam DEVID = 8'b0000_0100;
- localparam WRITE = 8'b0000_1000;
- localparam READ = 8'b0001_0000;
- localparam M_ACK = 8'b0010_0000;
- localparam S_ACK = 8'b0100_0000;
- localparam DONE = 8'b1000_0000;
-
- // 寄存器声明
- reg start_ff1;
- reg start_ff2;
- reg start_reg;
- reg read_flag;
- reg op_type;
-
- reg iic_clk;
- reg [9:0] clk_cnt;
-
- reg [3:0] bit_cnt;
- reg [3:0] wr_byte_cnt;
- reg [3:0] rd_byte_cnt;
- reg iic_busy;
- reg [7:0] iic_dout;
- reg [7:0] iic_din;
-
- reg [7:0] cur_state;
- reg [7:0] nxt_state;
- reg [127:0] state_ascii;
-
- reg slave_ack;
- reg delay_array [0:SCL_DELAY-1];
-
- // 线网声明
- wire scl = delay_array[SCL_DELAY-1];
- wire update_edge = clk_cnt == SCL_CYCLE/2 - 1;
- wire latch_edge = clk_cnt == SCL_CYCLE - 1;
- wire one_byte = bit_cnt == 7;
-
- // 输出信号连接
- assign scl_out = scl;
- assign scl_ctrl = iic_busy;
- assign sda_out = iic_dout[7];
- assign sda_ctrl = iic_busy & (cur_state != S_ACK) & (cur_state != READ);
- assign dout = iic_din;
- assign dreq = cur_state == WRITE & one_byte & update_edge;
- assign dvld = cur_state == READ & one_byte & latch_edge;
- assign op_done = ((wr_byte_cnt == wr_len & op_type == 0)|(rd_byte_cnt == rd_len & op_type == 1)) & iic_busy;
时序逻辑代码如下:
- // -------------------- I2C start --------------------
-
- // 接收开始信号并寄存
- always @(negedge clk) begin
- if(!rst_n) begin
- start_ff1 <= 0;
- start_ff2 <= 0;
- end else begin
- start_ff1 <= op_start;
- start_ff2 <= start_ff1;
- end
- end
-
- always @(negedge clk) begin
- if(!rst_n)
- start_reg <= 1'b0;
- else if(start_ff1 & ~start_ff2)
- start_reg <= 1'b1;
- else if(cur_state == START)
- start_reg <= 1'b0;
- end
-
- always @(negedge clk) begin
- if(!rst_n)
- read_flag <= 1'b0;
- else if(cur_state == RE_ST)
- read_flag <= 1'b0;
- else if(cur_state == START)
- read_flag <= rw_flag[1];
- end
-
- // -------------------- I2C busy status --------------------
-
- // 根据状态产生总线忙碌信号
- always @(negedge clk) begin
- if(!rst_n)
- iic_busy <= 0;
- else if((cur_state == START | cur_state == RE_ST) & latch_edge)
- iic_busy <= 1;
- else if(cur_state == DONE & latch_edge)
- iic_busy <= 0;
- end
-
- // -------------------- SCL generator --------------------
-
- // 产生SCL
- always @(posedge clk) begin
- if (!rst_n)
- iic_clk <= 1'b0;
- else if (update_edge)
- iic_clk <= 1'b1;
- else if (latch_edge)
- iic_clk <= 1'b0;
- end
-
- // SCL计数器
- always @(posedge clk) begin
- if (!rst_n) begin
- clk_cnt <= 10'd0;
- end
- else begin
- if (latch_edge)
- clk_cnt <= 10'd0;
- else
- clk_cnt <= clk_cnt + 1'd1;
- end
- end
-
- // SCL输出延时
- always @(posedge clk)
- delay_array[0] <= iic_clk;
-
- genvar i;
- generate
- for (i = 0;i < SCL_DELAY-1;i = i + 1) begin
- always @(posedge clk)
- delay_array[i+1] <= delay_array[i];
- end
- endgenerate
-
-
- // -------------------- FSM --------------------
-
- // 状态机第一段,时序逻辑切换状态
- always @(posedge clk) begin
- if(!rst_n)
- cur_state <= IDLE;
- else if(update_edge)
- cur_state <= nxt_state;
- end
-
- // 状态机第二段,组合逻辑产生次态
- always @(*) begin
- case (cur_state)
- IDLE:begin
- if(start_reg) // 开始信号为高进入开始状态
- nxt_state <= START;
- else if(read_flag)
- nxt_state <= RE_ST;
- else
- nxt_state <= IDLE;
- end
-
- START:begin
- if(iic_busy) // 总线忙碌进入发送ID状态
- nxt_state <= DEVID;
- else
- nxt_state <= START;
- end
-
- RE_ST:begin
- if(iic_busy) // 总线忙碌进入发送ID状态
- nxt_state <= DEVID;
- else
- nxt_state <= RE_ST;
- end
-
- DEVID:begin
- if(one_byte) // 发送一个字节进入等待从机响应状态
- nxt_state <= S_ACK;
- else
- nxt_state <= DEVID;
- end
-
- WRITE:begin
- if(one_byte) // 写一个字节进入等待从机响应状态
- nxt_state <= S_ACK;
- else
- nxt_state <= WRITE;
- end
-
- READ:begin
- if(one_byte) // 读一个字节进入主机响应状态
- nxt_state <= M_ACK;
- else
- nxt_state <= READ;
- end
-
- M_ACK:begin
- if(op_done) // 主机不响应进入结束状态
- nxt_state <= DONE;
- else // 主机响应进入读状态
- nxt_state <= READ;
- end
-
- S_ACK:begin
- if(!slave_ack) // 从机不响应进入结束状态
- nxt_state <= DONE;
- else if(op_done) // 主机操作完成进入结束状态
- nxt_state <= DONE;
- else if(op_type) // 从机响应且为读操作进入读状态
- nxt_state <= READ;
- else // 从机响应且为写操作进入写状态
- nxt_state <= WRITE;
- end
-
- DONE:begin
- if(!iic_busy) // 总线空闲则进入空闲状态
- nxt_state <= IDLE;
- else
- nxt_state <= DONE;
- end
-
- default: nxt_state <= IDLE;
- endcase
- end
-
- // 状态机第三段,各个状态输出信号
- // SCL上升沿更新数据
- always @(posedge clk) begin
- if(!rst_n)
- bit_cnt <= 0;
- else if(update_edge) begin
- if(cur_state == DEVID | cur_state == WRITE | cur_state == READ)
- bit_cnt <= bit_cnt + 1;
- else
- bit_cnt <= 0;
- end
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- iic_dout <= 0;
- else if(update_edge) begin
- if(cur_state == START)
- iic_dout <= {device_id,rw_flag[0]};
- else if(cur_state == RE_ST)
- iic_dout <= {device_id,rw_flag[1]};
- else if(cur_state == WRITE | cur_state == DEVID)
- iic_dout <= {iic_dout[6:0],1'b0};
- else if(cur_state == READ)
- iic_dout <= (rd_byte_cnt == rd_len - 1) ? 8'h80 : 8'h00;
- else if(cur_state == S_ACK)
- iic_dout <= slave_ack ? din : 8'h00;
- else if(cur_state == M_ACK)
- iic_dout <= 8'h00;
- else if(cur_state == IDLE)
- iic_dout <= 8'h00;
- end
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- wr_byte_cnt <= 0;
- else if(cur_state == WRITE & one_byte & update_edge)
- wr_byte_cnt <= wr_byte_cnt + 1;
- else if(cur_state == DONE)
- wr_byte_cnt <= 0;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- rd_byte_cnt <= 0;
- else if(cur_state == READ & one_byte & update_edge)
- rd_byte_cnt <= rd_byte_cnt + 1;
- else if(cur_state == DONE)
- rd_byte_cnt <= 0;
- end
-
- // SCL下降沿采样数据
- always @(posedge clk) begin
- if(!rst_n)
- iic_din <= 0;
- else if(latch_edge) begin
- if(cur_state == READ)
- iic_din[7 - bit_cnt] <= sda_in;
- else
- iic_din <= 0;
- end
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- op_type <= 0;
- else if(cur_state == DEVID & one_byte & latch_edge)
- op_type <= sda_in;
- else if(cur_state == IDLE)
- op_type <= 0;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- slave_ack <= 0;
- else if(latch_edge) begin
- if(cur_state == S_ACK)
- slave_ack <= ~sda_in;
- else
- slave_ack <= 0;
- end
- end
-
- // simulation only
- always @(*) begin
- case(cur_state)
- IDLE:state_ascii <= "IDLE";
- START:state_ascii <= "START";
- RE_ST:state_ascii <= "RESTART";
- DEVID:state_ascii <= "DEVID";
- WRITE:state_ascii <= "WRITE";
- READ:state_ascii <= "READ";
- M_ACK:state_ascii <= "M_ACK";
- S_ACK:state_ascii <= "S_ACK";
- DONE:state_ascii <= "DONE";
- default:state_ascii <= "UNKNOWN";
- endcase
- end
-
- endmodule
本次编写的I2C模块和串口模块的发送方式不同,选用了移位输出。
此次I2C的代码中没有sda和scl的同步链模块,这是因为sda和scl都是inout类型,由于inout是在顶层操作的,所以把同步工作也留给顶层去处理,而且I2C读写过程中由于总线不停的被占用和释放,经常产生毛刺,还需要对SDA做一个简单的滤波工作再接入I2C模块。
虽然代码看上去还是有点多,但是这已经是我能想到的兼顾复用性和稳定性的最简单的I2C主机了,代码大部分都在编写状态机,组合逻辑的部分也很简单,都是基于状态机和SCL变化沿的输出,很容易理解。
读写标志信号rw_flag有三种情况,为0时,写操作,写wr_len个字节的数据后停止;为1时是读操作,直接开始读rd_len个字节的数据后停止;为2时是先写后读的操作,也就是大部分器件的读指定寄存器的操作,先写wr_len个字节的数据后停止,再重新开始然后读rd_len个字节的数据后停止。
还有最后的代码,备注了simulation only的那部分,是一个仿真小技巧,因为vivado在仿真过程中,状态机的值没有办法显示为定义的状态名,仿真的时候就会很不方便,状态机全是0001,0010这种,很难分的清这个值是哪个状态,因此我们声明一个变量,每当状态切换时,同步把变量名作为字符串赋给这个变量,然后在仿真界面拖入这个变量,选择Radix -> ASCII,这样就能在仿真过程中显示状态名啦,而且由于这个变量完全没有使用,所以在综合的过程中就会优化掉,没有任何影响。
最后,附上顶层sda滤波模块的代码:
滤波模块:
- module filter(
- input wire clk,
- input wire sin,
- output wire sout
- );
-
- (* ASYNC_REG = "true" *)reg in_ff1;
- (* ASYNC_REG = "true" *)reg in_ff2;
- (* ASYNC_REG = "true" *)reg in_ff3;
- (* ASYNC_REG = "true" *)reg out_ff;
-
- always @(posedge clk) begin
- in_ff1 <= ~sin;
- in_ff2 <= in_ff1;
- in_ff3 <= in_ff2;
- end
-
- always @(posedge clk) begin
- if(in_ff2 == in_ff1)
- out_ff <= in_ff2;
- end
-
- assign sout = ~out_ff;
-
- endmodule
由于I2C没有从机无法仿真,没有响应直接就停止操作了,所以本文的仿真环节留到从机编写完成之后再进行。
I2C从机比起主机要简单很多,只用给出响应和返回数据即可,数据接收和串口类似,不过I2C是MSB在前LSB在后。从机可以直接在SCL的变化沿进行数据操作,所以不需要延时。
I2C从机不需要什么参数,只需要设定从机ID即可,和其他模块的接口就按照上文所说的,用ram接口,ram接口一般包含en,we,addr,din,dout。en使能时读取addr处的数据到dout,we和en同时使能时将din写入addr处,为了减少接口数量,就不要en信号了,让en总是处于使能状态即可,综上所述,I2C从机模块的代码如下:
- `timescale 1ns / 1ps
-
- module i2c_slave #(
- parameter DEVICE_ID = 7'b1010010 // 7'h52 从机ID
- )(
- input clk, // 输入时钟
- input rst_n, // 同步复位
-
- input SCL_in, // I2C时钟输入
- input SDA_in, // I2C数据输入
- output SDA_out, // I2C数据输出
- output sda_ctrl, // I2C数据控制
-
- input [7:0] rd_data, // 读ram数据
- output [7:0] wr_data, // 写ram数据
- output [7:0] op_addr, // 操作ram地址
- output reg wr_en // 写ram使能
- );
-
- //变量声明
-
- localparam IDLE = 6'b000001;
- localparam DEVID = 6'b000010;
- localparam WAIT = 6'b000100;
- localparam WBACK = 6'b001000;
- localparam M_ACK = 6'b010000;
- localparam S_ACK = 6'b100000;
-
- reg scl_ff1;
- reg scl_ff2;
- reg scl_ff3;
- reg scl_sync;
- reg sda_ff1;
- reg sda_ff2;
- reg sda_ff3;
- reg sda_sync;
-
- reg [5:0] cur_state;
- reg [5:0] nxt_state;
- reg [63:0] state_ascii;
-
- reg rw_flag;
- reg ad_flag;
- reg start_flag;
- reg done_flag;
- reg slave_ack;
- reg master_ack;
- reg [6:0] deviceid;
- reg [7:0] iic_dout;
- reg [7:0] iic_din;
- reg [3:0] bit_cnt;
- reg [7:0] r_addr;
- reg [7:0] r_data;
-
- wire scl_rise = scl_ff3 & ~scl_sync;
- wire scl_fall = ~scl_ff3 & scl_sync;
- wire sda_rise = sda_ff3 & ~sda_sync;
- wire sda_fall = ~sda_ff3 & sda_sync;
- wire one_byte = bit_cnt == 7;
-
- assign SDA_out = iic_dout[7];
- assign sda_ctrl = cur_state == WBACK | cur_state == S_ACK;
- assign op_addr = r_addr;
- assign wr_data = r_data;
-
- // 输入信号同步链滤波
-
- always @(posedge clk) begin
- scl_ff1 <= SCL_in;
- scl_ff2 <= scl_ff1;
- scl_ff3 <= (scl_ff2 == scl_ff1) ? scl_ff2 : scl_ff3;
- scl_sync <= scl_ff2;
- end
-
- always @(posedge clk) begin
- sda_ff1 <= SDA_in;
- sda_ff2 <= sda_ff1;
- sda_ff3 <= (sda_ff2 == sda_ff1) ? sda_ff2 : sda_ff3;
- sda_sync <= sda_ff2;
- end
-
- // 状态机
-
- always @(posedge clk) begin
- if(!rst_n)
- cur_state <= IDLE;
- else if(scl_fall)
- cur_state <= nxt_state;
- end
-
- always @(*) begin
- case (cur_state)
- IDLE:begin
- if(start_flag)
- nxt_state <= DEVID;
- else
- nxt_state <= IDLE;
- end
-
- DEVID:begin
- if(one_byte)
- nxt_state <= S_ACK;
- else
- nxt_state <= DEVID;
- end
-
- WAIT:begin
- if(done_flag)
- nxt_state <= IDLE;
- else if(one_byte)
- nxt_state <= S_ACK;
- else
- nxt_state <= WAIT;
- end
-
- WBACK:begin
- if(one_byte)
- nxt_state <= M_ACK;
- else
- nxt_state <= WBACK;
- end
-
- M_ACK:begin
- if(master_ack)
- nxt_state <= WBACK;
- if(!master_ack)
- nxt_state <= IDLE;
- else
- nxt_state <= M_ACK;
- end
-
- S_ACK:begin
- if(rw_flag & slave_ack)
- nxt_state <= WBACK;
- else
- nxt_state <= WAIT;
- end
-
- default:nxt_state <= IDLE;
- endcase
- end
-
- always @(posedge clk) begin
- if(!rst_n) begin
- iic_dout <= 0;
- bit_cnt <= 0;
- end else if(scl_fall) begin
- case(cur_state)
- DEVID:begin
- bit_cnt <= bit_cnt + 1;
- iic_dout <= (deviceid == DEVICE_ID) ? 8'h00 : 8'hff;
- end
-
- WAIT:begin
- bit_cnt <= bit_cnt + 1;
- iic_dout <= (deviceid == DEVICE_ID) ? 8'h00 : 8'hff;
- end
-
- WBACK:begin
- bit_cnt <= bit_cnt + 1;
- iic_dout <= {iic_dout[6:0],1'b0};
- end
-
- M_ACK:begin
- bit_cnt <= 0;
- end
-
- S_ACK:begin
- bit_cnt <= 0;
- iic_dout <= rd_data;
- end
-
- default:begin
- iic_dout <= 0;
- bit_cnt <= 0;
- end
- endcase
- end
- end
-
- always @(posedge clk) begin
- if(!rst_n) begin
- r_addr <= 0;
- r_data <= 0;
- end else if(cur_state == WAIT & scl_fall & one_byte) begin
- if(ad_flag) begin
- r_addr <= iic_din;
- end else begin
- r_data <= iic_din;
- end
- end else if(wr_en)
- r_addr <= r_addr + 1;
- end
-
- always @(posedge clk) begin
- if(!rst_n) begin
- deviceid <= 0;
- iic_din <= 0;
- master_ack <= 0;
- slave_ack <= 0;
- end else if(scl_rise) begin
- case(cur_state)
- DEVID:deviceid[6 - bit_cnt] <= sda_sync;
- WAIT:iic_din[7 - bit_cnt] <= sda_sync;
- M_ACK:master_ack <= ~sda_sync;
- S_ACK:slave_ack <= (deviceid == DEVICE_ID);
- IDLE:deviceid <= 0;
- default:begin
- master_ack <= 0;
- slave_ack <= 0;
- end
- endcase
- end
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- wr_en <= 0;
- else if(cur_state == WAIT & scl_fall & one_byte & !ad_flag)
- wr_en <= 1;
- else
- wr_en <= 0;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- ad_flag <= 1;
- else if(cur_state == WAIT & scl_fall & one_byte & ad_flag)
- ad_flag <= 0;
- else if(cur_state == IDLE)
- ad_flag <= 1;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- rw_flag <= 0;
- else if(cur_state == DEVID & scl_rise & one_byte)
- rw_flag <= sda_sync;
- else if(cur_state == IDLE)
- rw_flag <= 0;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- start_flag <= 0;
- else if(scl_sync & sda_fall)
- start_flag <= 1;
- else if(cur_state == DEVID)
- start_flag <= 0;
- end
-
- always @(posedge clk) begin
- if(!rst_n)
- done_flag <= 0;
- else if(scl_sync & sda_rise)
- done_flag <= 1;
- else if(cur_state == IDLE)
- done_flag <= 0;
- end
-
- // simulation only
- always @(*) begin
- case (cur_state)
- IDLE:state_ascii <= "IDLE";
- DEVID:state_ascii <= "DEVID";
- WAIT:state_ascii <= "WAIT";
- WBACK:state_ascii <= "WBACK";
- M_ACK:state_ascii <= "M_ACK";
- S_ACK:state_ascii <= "S_ACK";
- default:state_ascii <= "IDLE";
- endcase
- end
-
- endmodule
一般来说,Verilog代码中应该尽量减少信号的耦合,不同信号应该尽量分开单独编写一个always块,我这里就是懒了,感觉不冲突的信号都写到一起去了,这样并不好,除非是高度关联的信号,不然一般都分开写比较好。
现在I2C的主机从机都已经编写完成,可以对其进行仿真了。
I2C的仿真需要一点平时不怎么用的小技巧,众所周知,I2C协议规定时钟线和数据线需要上拉,在空闲时都为高电平,而在仿真里,一条wire不被占用的时候,就成了高阻态z,这样就没法进行仿真了。所以我们需要用pullup语句模拟上拉电阻,这样这条wire在空闲的时候值就是1了。
- wire scl;
- wire sda;
- pullup(sda);
- assign sda =
- sda_ctrl ? sda_out :
- sda_slv ? SDA_out : 1'bz;
- assign sda_in = sda;
- assign SDA_in = sda;
例化主机和从机,主机使用标准模式速度,使用默认延时,设定从机地址,主机操作地址设定为相同地址,开始仿真:
- i2c_master #(
- .SYS_CLK (50_000_000), // unit "hz"
- .IIC_FREQ (100_000), // unit "hz"
- .SCL_DELAY_OW (0), // delay overwrite enable
- .SCL_DELAY_USR (20) // delay input by user
- ) i2c_master_inst(
- .clk (clk),
- .rst_n (rst_n),
-
- .op_start (op_start),
- .op_done (op_done),
- .wr_len (wr_len),
- .rd_len (rd_len),
- .rw_flag (rw_flag), // 1 : read, 0 : write.
- .device_id (device_id),
- .dreq (txq),
- .din (txd),
- .dvld (rxv),
- .dout (rxd),
-
- .scl_out (scl),
- .sda_in (sda_in),
- .sda_out (sda_out),
- .sda_ctrl (sda_ctrl));
-
- i2c_slave #(
- .DEVICE_ID (7'b1010010) // 'h52
- ) i2c_slave_inst(
- .clk(clk),
- .rst_n(rst_n),
- .SCL_in(scl),
- .SDA_in(SDA_in),
- .SDA_out(SDA_out),
- .sda_ctrl(sda_slv),
- .rd_data(8'h9a),
- .wr_data(),
- .op_addr(),
- .wr_en());
先看主机时序,START状态SDA产生下降沿,进入写器件ID状态,写完等待从机响应,从机响应后根据读写标志进行读写操作,移位计数器循环计数移位输出。
可以看到在响应位后会有毛刺,这是因为主机是延时了四分之一周期的,而从机是直接在SCL变化沿操作的,所以在下降沿后从机已经完成响应操作并释放了总线,但是主机还没有进行下一步操作,所以总线空闲,呈现高电平,这种情况在实际应用中也不会影响读写结果的,只要在SCL高电平期间,数据保持稳定即可。
读写完成后在一个SCL的高电平器件拉高SDA作为一次操作的结束信号。
然后是从机时序,接收到主机发出的开始信号后,直接进入接收ID的状态,接收到的ID与自身ID相同时做出响应,否则不响应。
因为在响应之后,从机不知道主机是要继续写还是要结束操作,所以称该状态为等待状态,假如主机继续发送数据,从机就继续接收缓存并响应,假如在等待状态下收到主机发出的结束信号,就结束接收进入空闲状态。
终于来到了上板的环节,无论仿真的时序有多完美,没有上板就都是虚的。因为I2C是主从结构,没法环回验证,只能主机从机分开验证,先通过读写板上的EEPROM来验证主机的正确性,再通过主机读写从机来验证从机的正确性。
首先遇到的第一个问题就是如何与I2C主机交互,看代码可以知道我们I2C主机的用户接口比较多,没有匹配的通用接口,即使可以通过VIO控制所有的用户接口,但是这样的模块终究是复用性很差的,那么为了提高模块的复用性,对于这种用户接口很多的模块,可以用寄存器去控制,把所有用户接口和寄存器连接,然后用配置接口去控制寄存器,这样就可以通过配置接口去控制该模块了,常用的配置接口有native ram,axi lite还有Xilinx自己的drp等,为了简便这里就用native ram接口去编写一个寄存器控制模块。
- `timescale 1ns / 1ps
-
- module i2c_ctrl(
- input clk,
- input en,
- input we,
- input [7:0] din,
- output reg [7:0] dout,
- input [7:0] addr,
- output op_start,
- input op_done,
- output [3:0] wr_len,
- output [3:0] rd_len,
- output [1:0] rw_flag,
- output [6:0] device_id,
- input txq,
- output [7:0] txd,
- input rxv,
- input [7:0] rxd
- );
-
- wire [3:0] tx_ptr = reg_tx_buffer_ctrl[3:0];
- wire [3:0] rx_ptr = reg_rx_buffer_ctrl[3:0];
-
- // write only
- reg [7:0] reg_tx_buffer_0 = 0; // 0x00
- reg [7:0] reg_tx_buffer_1 = 0; // 0x01
- reg [7:0] reg_tx_buffer_2 = 0; // 0x02
- reg [7:0] reg_tx_buffer_3 = 0; // 0x03
- reg [7:0] reg_tx_buffer_4 = 0; // 0x04
- reg [7:0] reg_tx_buffer_5 = 0; // 0x05
- reg [7:0] reg_tx_buffer_6 = 0; // 0x06
- reg [7:0] reg_tx_buffer_7 = 0; // 0x07
-
- // read only
- reg [7:0] reg_rx_buffer_0 = 0; // 0x08
- reg [7:0] reg_rx_buffer_1 = 0; // 0x09
- reg [7:0] reg_rx_buffer_2 = 0; // 0x0a
- reg [7:0] reg_rx_buffer_3 = 0; // 0x0b
- reg [7:0] reg_rx_buffer_4 = 0; // 0x0c
- reg [7:0] reg_rx_buffer_5 = 0; // 0x0d
- reg [7:0] reg_rx_buffer_6 = 0; // 0x0e
- reg [7:0] reg_rx_buffer_7 = 0; // 0x0f
-
- // read - write
- reg [7:0] reg_tx_buffer_ctrl = 0; // 0x10
- // bit 7: clear buffer, self reset
- // bit 3-0: tx_ptr
- reg [7:0] reg_rx_buffer_ctrl = 0; // 0x11
- // bit 7: clear buffer, self reset
- // bit 3-0: rx_ptr
-
- reg [7:0] reg_op_status = 0; // 0x12
- // bit 0: op start, self reset
- // bit 7: op done
- reg [7:0] reg_op_deviceid = 0; // 0x13 8'b01010000 = 8'h50
- // bit 6-0: device id
- reg [7:0] reg_op_length = 0; // 0x14
- // bit 7-0: operate length
- reg [7:0] reg_op_rwctrl = 0; // 0x15
- // bit 0: read-write flag
-
- reg [7:0] reg_reserve_0 = 0; // 0x16
- reg [7:0] reg_reserve_1 = 0; // 0x17
- reg [7:0] reg_reserve_2 = 0; // 0x18
- reg [7:0] reg_reserve_3 = 0; // 0x19
- reg [7:0] reg_reserve_4 = 0; // 0x1a
- reg [7:0] reg_reserve_5 = 0; // 0x1b
- reg [7:0] reg_reserve_6 = 0; // 0x1c
- reg [7:0] reg_reserve_7 = 0; // 0x1d
- reg [7:0] reg_reserve_8 = 0; // 0x1e
- reg [7:0] reg_reserve_9 = 0; // 0x1f
-
- assign op_start = reg_op_status[0];
- assign wr_len = reg_op_length[3:0];
- assign rd_len = reg_op_length[7:4];
- assign rw_flag = reg_op_rwctrl[1:0];
- assign device_id = reg_op_deviceid[6:0];
- assign txd =
- tx_ptr == 0 ? reg_tx_buffer_0 :
- tx_ptr == 1 ? reg_tx_buffer_1 :
- tx_ptr == 2 ? reg_tx_buffer_2 :
- tx_ptr == 3 ? reg_tx_buffer_3 :
- tx_ptr == 4 ? reg_tx_buffer_4 :
- tx_ptr == 5 ? reg_tx_buffer_5 :
- tx_ptr == 6 ? reg_tx_buffer_6 :
- tx_ptr == 7 ? reg_tx_buffer_7 : 8'h00;
-
- always @(posedge clk) begin
- if(en) begin
- case (addr)
- 8'h08:dout <= reg_rx_buffer_0;
- 8'h09:dout <= reg_rx_buffer_1;
- 8'h0a:dout <= reg_rx_buffer_2;
- 8'h0b:dout <= reg_rx_buffer_3;
- 8'h0c:dout <= reg_rx_buffer_4;
- 8'h0d:dout <= reg_rx_buffer_5;
- 8'h0e:dout <= reg_rx_buffer_6;
- 8'h0f:dout <= reg_rx_buffer_7;
- 8'h10:dout <= reg_tx_buffer_ctrl;
- 8'h11:dout <= reg_rx_buffer_ctrl;
- 8'h12:dout <= reg_op_status;
- 8'h13:dout <= reg_op_deviceid;
- 8'h14:dout <= reg_op_length;
- 8'h15:dout <= reg_op_rwctrl;
- 8'h16:dout <= reg_reserve_0;
- 8'h17:dout <= reg_reserve_1;
- 8'h18:dout <= reg_reserve_2;
- 8'h19:dout <= reg_reserve_3;
- 8'h1a:dout <= reg_reserve_4;
- 8'h1b:dout <= reg_reserve_5;
- 8'h1c:dout <= reg_reserve_6;
- 8'h1d:dout <= reg_reserve_7;
- 8'h1e:dout <= reg_reserve_8;
- 8'h1f:dout <= reg_reserve_9;
- default:dout <= dout;
- endcase
- if(we) begin
- case (addr)
- 8'h13:reg_op_deviceid <= din;
- 8'h14:reg_op_length <= din;
- 8'h15:reg_op_rwctrl <= din;
- 8'h16:reg_reserve_0 <= din;
- 8'h17:reg_reserve_1 <= din;
- 8'h18:reg_reserve_2 <= din;
- 8'h19:reg_reserve_3 <= din;
- 8'h1a:reg_reserve_4 <= din;
- 8'h1b:reg_reserve_5 <= din;
- 8'h1c:reg_reserve_6 <= din;
- default:;
- endcase
- end
- end
- end
-
- // -------------------- Tx buffer ctrl --------------------
-
- always @(posedge clk) begin
- if(reg_tx_buffer_ctrl[7]) begin
- reg_tx_buffer_0 <= 0;
- reg_tx_buffer_1 <= 0;
- reg_tx_buffer_2 <= 0;
- reg_tx_buffer_3 <= 0;
- reg_tx_buffer_4 <= 0;
- reg_tx_buffer_5 <= 0;
- reg_tx_buffer_6 <= 0;
- reg_tx_buffer_7 <= 0;
- end else if(we) begin
- case (addr)
- 8'h00:reg_tx_buffer_0 <= din;
- 8'h01:reg_tx_buffer_1 <= din;
- 8'h02:reg_tx_buffer_2 <= din;
- 8'h03:reg_tx_buffer_3 <= din;
- 8'h04:reg_tx_buffer_4 <= din;
- 8'h05:reg_tx_buffer_5 <= din;
- 8'h06:reg_tx_buffer_6 <= din;
- 8'h07:reg_tx_buffer_7 <= din;
- default:;
- endcase
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h10)
- reg_tx_buffer_ctrl <= din;
- end else if(®_reserve_7) begin
- reg_tx_buffer_ctrl <= reg_tx_buffer_ctrl & 8'b0111_1111;
- end else if(txq) begin
- reg_tx_buffer_ctrl[3:0] <= reg_tx_buffer_ctrl[3:0] + 1;
- end
- end
-
- // -------------------- Tx buffer ctrl --------------------
-
- always @(posedge clk) begin
- if(reg_tx_buffer_ctrl[7]) begin
- reg_rx_buffer_0 <= 0;
- reg_rx_buffer_1 <= 0;
- reg_rx_buffer_2 <= 0;
- reg_rx_buffer_3 <= 0;
- reg_rx_buffer_4 <= 0;
- reg_rx_buffer_5 <= 0;
- reg_rx_buffer_6 <= 0;
- reg_rx_buffer_7 <= 0;
- end else if(rxv) begin
- case (rx_ptr)
- 8'h00:reg_rx_buffer_0 <= rxd;
- 8'h01:reg_rx_buffer_1 <= rxd;
- 8'h02:reg_rx_buffer_2 <= rxd;
- 8'h03:reg_rx_buffer_3 <= rxd;
- 8'h04:reg_rx_buffer_4 <= rxd;
- 8'h05:reg_rx_buffer_5 <= rxd;
- 8'h06:reg_rx_buffer_6 <= rxd;
- 8'h07:reg_rx_buffer_7 <= rxd;
- default:;
- endcase
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h11)
- reg_rx_buffer_ctrl <= din;
- end else if(®_reserve_8) begin
- reg_rx_buffer_ctrl <= reg_rx_buffer_ctrl & 8'b0111_1111;
- end else if(rxv) begin
- reg_rx_buffer_ctrl[3:0] <= reg_rx_buffer_ctrl[3:0] + 1;
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h12)
- reg_op_status <= din;
- end else if(®_reserve_9) begin
- reg_op_status <= reg_op_status & 8'b1111_1110;
- end else begin
- reg_op_status <= reg_op_status | {op_done,7'b0000_000};
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h1d)
- reg_reserve_7 <= din;
- end else if(!reg_tx_buffer_ctrl[7]) begin
- reg_reserve_7 <= 0;
- end else begin
- reg_reserve_7 <= {reg_reserve_7[6:0],1'b1};
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h1e)
- reg_reserve_8 <= din;
- end else if(!reg_rx_buffer_ctrl[7]) begin
- reg_reserve_8 <= 0;
- end else begin
- reg_reserve_8 <= {reg_reserve_8[6:0],1'b1};
- end
- end
-
- always @(posedge clk) begin
- if(we) begin
- if(addr == 8'h1e)
- reg_reserve_9 <= din;
- end else if(!reg_op_status[0]) begin
- reg_reserve_9 <= 0;
- end else begin
- reg_reserve_9 <= reg_reserve_9 + 1;
- end
- end
-
- endmodule
最后用VIO去控制这个ram接口,对寄存器进行读写,就可以轻松控制I2C主机了。再用ILA抓取I2C模块内部的SCL和SDA,方便观察总线波形。编写好的顶层模块如下:
- `timescale 1ns / 1ps
-
- module top(
- input wire sys_clk,
- input wire sys_rst_n,
- inout wire scl,
- inout wire sda
- );
-
- wire clk_50M;
- wire sync_rst_n;
-
- wire write;
- wire read;
- wire [7:0] wr_data;
- wire [7:0] rd_data;
- wire [7:0] rw_addr;
-
- wire en;
- wire we;
- wire [7:0] din;
- wire [7:0] dout;
- wire [7:0] addr;
-
- wire scl_in;
- wire scl_out;
- wire scl_ctrl;
- wire sda_in;
- wire sda_out;
- wire sda_ctrl;
-
- assign scl = scl_ctrl ? scl_out : 1'bz;
- assign sda = sda_ctrl ? sda_out : 1'bz;
- assign scl_in = scl;
- assign sda_in = sda;
-
- sys_pll pll_inst(
- .clk_in (sys_clk),
- .clk_out (clk_50M)
- );
-
- filter filter_rst(
- .clk (clk_50M),
- .sin (sys_rst_n),
- .sout (sync_rst_n));
-
- ram_ctrl ram_ctrl_inst(
- .clk (clk_50M),
- .write (write),
- .read (read),
- .wr_data (wr_data),
- .rd_data (rd_data),
- .rw_addr (rw_addr),
- .en (en),
- .we (we),
- .din (din),
- .dout (dout),
- .addr (addr));
-
- i2c_master_top i2c_master_top_inst(
- .clk (clk_50M),
- .rst_n (sync_rst_n),
- .scl_out (scl_out),
- .scl_ctrl (scl_ctrl),
- .sda_in (sda_in),
- .sda_out (sda_out),
- .sda_ctrl (sda_ctrl),
- .en (en),
- .we (we),
- .din (din),
- .dout (dout),
- .addr (addr));
-
- vio_0 vio_inst(
- .clk (clk_50M),
- .probe_out0 (write),
- .probe_out1 (read),
- .probe_out2 (wr_data),
- .probe_out3 (rw_addr),
- .probe_in0 (rd_data),
- .probe_in1 (rw_flag),
- .probe_in2 (device_id),
- .probe_in3 ({wr_len,rd_len})
- );
-
- ila_i2c ila_inst(
- .clk (clk_50M),
- .probe0 (scl_in),
- .probe1 (sda_in)
- );
-
- endmodule
程序上板后,先对寄存器进行配置,向对应地址写入值:
读写标志设置为0,写操作,写操作长度3,写入三字节数据,器件ID设为EEPROM的ID,在TX buffer的前两字节里写入了00和10,第三个字节处写入8D,即往0x0010地址处写入0x8D。往启动寄存器中写入01开始发送,然后抓取总线的波形:
可以看到和预期的波形一致,EEPROM也正确地产生了响应位。
再次对寄存器进行配置:
读写标志设置为10,先写后读操作,写操作长度为2,读操作长度为1,写两字节数据,读一字节数据,器件ID不变。对于我写的寄存器模块,还需要清除发送buffer的指针。因为要读同一地址,发送buffer中的内容也不用变,即读出EEPROM在0x0010处的数据。往启动寄存器中写入01开始发送:
成功地读出了0x8D!证明该主机模块是可以使用的。
有了验证通过的主机,就可以使用主机来验证从机了,本次验证在两块FPGA开发板上进行,在另一块板上例化从机后,把I2C接口约束在任意两个引出的IO,修改此开发板上主机的引脚约束,也约束在两个引出的IO上,这样通过杜邦线连接就可以实现I2C通信了。
- module top(
- input sys_clk,
- input sys_rst_n,
- input scl,
- inout sda,
- output [3:0] led_out
- );
-
- wire clk_100M;
- wire pll_locked;
-
- wire [7:0] wr_data;
- wire [7:0] rd_data;
- wire [7:0] op_addr;
- wire wr_en;
-
- wire scl_in;
- wire sda_in;
- wire sda_out;
- wire sda_ctrl;
-
- wire [3:0] flow;
-
- assign led_out = flow;
- assign scl_in = scl;
- assign sda_in = sda;
- assign sda = sda_ctrl ? sda_out : 1'bz;
-
- sys_pll pll_inst(
- .CLK (sys_clk),
- .CLKOP (clk_100M),
- .LOCK (pll_locked));
-
- timer #(
- .SYS_CLK (100_000_000)
- ) timer_inst(
- .clk(clk_100M),
- .rst_n(sys_rst_n),
- .intr_a(intr_a),
- .intr_b(intr_b),
- .flow(flow));
-
- spram #(
- .DATA_WIDTH(8),
- .RAM_DEPTH(256)
- ) spram(
- .clk(clk_100M),
- .en(1'b1),
- .we(wr_en),
- .addr(op_addr),
- .din(wr_data),
- .dout(rd_data));
-
- i2c_slave #(
- .DEVICE_ID (7'b1010010) // 7'h52 从机ID
- ) i2c_slave_inst(
- .clk(clk_100M), // 输入时钟
- .rst_n(sys_rst_n), // 同步复位
-
- .SCL_in(scl_in), // I2C时钟输入
- .SDA_in(sda_in), // I2C数据输入
- .SDA_out(sda_out), // I2C数据输出
- .sda_ctrl(sda_ctrl), // I2C数据控制
-
- .rd_data(rd_data), // 读ram数据
- .wr_data(wr_data), // 写ram数据
- .op_addr(op_addr), // 操作ram地址
- .wr_en(wr_en) // 写ram使能
- );
-
- endmodule
用杜邦线连接的I2C需要注意一个问题,I2C协议中SCL和SDA都是上拉的,一般开发板上的电路都是在外部做了上拉的,所以不需要进行额外的处理,但是杜邦线肯定是没有上拉的,所以需要在引脚配置界面将SCL和SDA设置为上拉模式,主机和从机的四个引脚都需要上拉。
因为从机是一字节地址,所以写长度设为2,写入一字节地址一字节数据,寄存器配置为在0x00处写入0x53。
可以看出从机是正常响应的,接下来再读0x00处的数据,读出0x53,从机验证完成。
这一篇内容拖了时间太长,中间还经历了换不同板子,换不同厂家的芯片,刚开始写的一些内容到后来已经不一样了,毕竟用公司的板子,还是限制比较多,有些接口还得自己买外界模块才能做,SPI flash模块已经到了,下一篇内容就是SPI接口了,欢迎持续关注!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。