本节主要介绍常用的串行通信接口UART、SPI、IIC时序及其Verilog代码实现。
URAT
异步全双工串行通信协议
所谓异步:发送、接收方使用各自的时钟控制数据的收发
所谓串行:将数据每个bit逐个传输
串行通信的传输方向:
单工:数据只能沿着一个方向传输
半双工:数据可以沿着两个方向传输,但需要分别进行
全双工:数据可以同时进行双向传输
发送方:将并行数据转换为串行数据进行传输
接收方:将串行数据转换为并行数据进行传输
1.时序格式

- 数据格式:
- 起始位:标志一帧数据的开始
- 数据位:一帧数据中的有效数据,可以是5~8个bit
- 校验位:用于检测数据传输是否出错
- 奇校验
- 偶校验
- 无校验位
- 停止位:标志一帧数据的结束,可以是1(默认)、1.5、2个bit
- 串口通信速率:
- 波特率:每秒钟传输二进制数据的位数,单位bit/s(bps)
- 常用波特率:9600、19200、38400、57600、115200bit/s
2.设计思路
模式选择:
- 开始位:1bit
- 数据位:8bit
- 校验位:无
- 停止位:1bit
- 波特率:115200bit/s
收发设计分析:
- 由于使用的波特率为115200,那么意味着串口发送或者接收1bit数据的时间为1个波特,即$\frac1{115200}s$
- 使用FPGA系统时钟周期来计数,若其为50MHz(20ns),那么需要$(1/115200)/(20\times 10^{-9})\approx 434$个系统时钟周期对1bit数据计数。这样我们就需要一个至少为 9 位的波特率计数器来计数,实际使用设为 16 位是为了其他波特率的使用
- 还需要一个4bit的计数器对总共要发送的10bit数据计数
- 在系统时钟计数器计数到一半时去采集数据,这时候的数据采集是最稳定的
串口接收模块波形图:
串口发送模块波形图:
3.uart串口接收代码设计
- 结合上述时序图,还是很容易看懂下述代码的。主要是通过两个计数器计数,一个计数器是对每个1bit内部计数(baud_cnt),一个计数器是对10个bit计数(rx_cnt),此外,rx_flag这个标志此时处于接收过程中的标志位还是比较妙的。其次,通过对uart_rxd下降沿的判断(有做两级寄存减少亚稳态),得到一个start_en的开始接收数据标志
1 | module uart_rx( |
4.uart串口发送代码设计
- 结合上述时序图,还是很容易看懂下述代码的。发送部分是外部信号给一个发送使能标志
1 | module uart_tx( |
5.uart串口通信回环实验
上位机通过串口调试助手发送数据给启明星开发板, 启明星开发板 PL 端通过USB_UART 串口接收数据并将接收到的数据发送给上位机,完成串口数据环回
系统框图:
顶层模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49module uart_loopback(
input sys_clk , //外部50MHz时钟
input sys_rst_n, //系外部复位信号,低有效
//UART端口
input uart_rxd , //UART接收端口
output uart_txd //UART发送端口
);
//parameter define
parameter CLK_FREQ = 50000000; //定义系统时钟频率
parameter UART_BPS = 115200 ; //定义串口波特率
//wire define
wire uart_rx_done; //UART接收完成信号
wire [7:0] uart_rx_data; //UART接收数据
//*****************************************************
//** main code
//*****************************************************
//串口接收模块
uart_rx #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)
u_uart_rx(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.uart_rxd (uart_rxd ),
.uart_rx_done (uart_rx_done),
.uart_rx_data (uart_rx_data)
);
//串口发送模块
uart_tx #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)
u_uart_tx(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.uart_tx_en (uart_rx_done),
.uart_tx_data (uart_rx_data),
.uart_txd (uart_txd ),
.uart_tx_busy ( )
);
endmodule上板验证结果:
SPI
- 串行外围设备接口,是高速(相比于UART、IIC)、全双工、同步通信总线
- 应用:EEPROM、FLASH、ADC、DSP、数字信号解码器
- 优点:全双工、通讯简单,数据传输速率快,灵活的数据传输方式,不限于8位,可以是任意大小的字
- 缺点:没有指定的流控制、无应答机制、数据可靠性上有一定的缺陷
1.时序格式
通讯模式:主从通讯模式,通讯双方有主从之分,根据从机设备的数量,SPI通讯设备之间的连接方式可分为一主一从和一主多从
四根数据线作用:
- SCK:时钟信号线,由主机产生,决定通信速率
- MOSI(Master Output Slave Input):主机发送数据 or 从机接收数据的线
- MISO(Master Input Slave Output):主机接收数据 or 从机发送数据的线
- CS:片选信号,低电平表示选中
工作模式:CPOL、CPHA决定有四种工作模式
- 时钟极性:
- CPOL=0:SPI总线空闲时SCK=0
- CPOL=1:SPI总线空闲时SCK=1
- 时钟相位:
- CPHA=0:SCK第一个跳变沿采样
- CPHA=1:SCK第二个跳变沿采样
- 模式0:CPOL=0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
- 模式1:CPOL=0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
- 模式2:CPOL=1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
- 模式3:CPOL=1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
- 时钟极性:
SPI基本的通讯过程,以模式0为例:
2.设计思路
- 通过对主机的系统时钟分频,得到SCK,并通过计数器判断SCK的上升沿和下降沿
- 设置一个发送计数器控制发送的bit位,同理,设置一个接收计数器控制接收的bit位
3.SPI driver设计代码
1 | //时间单位/精度 |
仿真结果:
IIC
两线式串行总线+半双工同步通讯方式
用于主机和从机在数据量不大,传输距离比较短的场合
物理层的基本描述:
- 从机设备上的SDA都接到总线的SDA上
- 从机设备上的SCL都接到总线的SCL上
- 从机设备都有唯一的设备地址
- SCL和SDA都需要上拉电阻(3.3K~10K)
- 两条总线:
- SCL(Serial Clock Line)控制数据接收时序的时钟线
- SDA(Serial Data)传输数据的线
1.时序格式
空闲状态:SCL、SDA皆为高电平(上拉电阻拉高)
开始信号:对应SCL=1时,SDA从0变1
停止信号:对应SCL=1时,SDA由1变0
数据传输时:
- SCL=1时,SDA保持稳定
- SCL=0时,SDA可以变化
应答信号:发送完8个bit后,主机释放SDA以使从机发送是否应答信号
- 从机拉低SDA第9个bit,代表接收成功
- 从机拉高SDA第9个bit,代表接收失败
IIC器件地址:
每个 I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C 总线上,例如 E2PROM 器件,为了增加系统的 E2PROM 容量,可能需要多个 E2PROM。
器件可编程地址位的数量由它可使用的管脚决定,比如 E2PROM 器件一般会留下 3 个管脚用于可编程地址位。
但有些 I2C 器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟 PCF8563 的器件地址为固定的 7’h51)。
所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可
进行数据传输时:
- 主机首先向总线上发出开始信号,对应开始位 S(SDA从1到0拉低)
- 然后按照从高到低的位序发送器件地址,一般为 7bit
- 第 8bit 位为读写控制位 R/W,该位为 0 时表示主机对从机进行写操作,当该位为 1 时表示主机对从机进行读操作,然后接收从机响应。
对于 AT24C64 来说,其传输器件地址格式如下图所示 :
存储地址:
- 一般而言,每个兼容 I2C 协议的器件,内部总会有可供读写的寄存器或存储器
- 当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入或读出内容
- 该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定
写时序:(要注意的是, 所有 I2C 设备均支持单字节数据写入操作,但只有部分 I2C 设备支持页写操作,对于AT24C64 的页写,是不能发送超过一页的单元容量的数据的,而 AT24C64 的一页的单元容量为 32Byte,当写完一页的最后一个单元时,地址指针指向该页的开头,如果再写入数据,就会覆盖该页的起始数据 )
单次写:
连续写:
读时序:
当前地址读:
随机读:
- 随机地址读在发送完器件地址和字地址后,竟然又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为“ 0”,也就是写命令,第二次发送器件地址时后面的读写控制位为“1”,也就是读。为什么会有这样奇怪的操作呢?
- 这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次Dummy Write 也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置
- 等从机应答后,就可以以当前地址读的方式读数据了
- 随机地址读是没有发送数据的单次写操作和当前地址读操作的结合体。
连续读:当前地址读和随机读都是一次读取一个字节,连续读是将当前地址读或随机读的主机非应答改成应答,表示继续读取数据
- 当前地址读下的连续读
- 随机读下的连续读
IIC读写操作总结:
2.设计思路
很明显,读写操作均有先后顺序,故采用状态机实现。大致为:写物理地址->写字地址->写操作/读操作,若为读操作,则在读数据之前,还需写一次物理地址+1(改为读命令)
状态机的驱动时钟dri_clk必须是scl的4倍以上的频率时,才能正确产生I2C起始信号和停止信号,dri_clk可由FPGA系统时钟分频而来
3.IIC driver设计代码
- 结合上述状态机,纯看代码还是比较容易看懂,如果要我自己从0开始写,那也是个大工程,得先把计数器(cnt)控制的时序一点点画出来,再对着写
1 | module i2c_dri |
4.基于IIC协议的EEPROM读写测试
本节的实验任务是先向 E2PROM(AT24C64)的存储器地址 0 至 255 分别写入数据 0
255;写完之后再读取存储器地址 0255 中的数据,若读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁系统框图:
顶层模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88module top_e2prom(
input sys_clk , //系统时钟
input sys_rst_n , //系统复位
//eeprom interface
output iic_scl , //eeprom的时钟线scl
inout iic_sda , //eeprom的数据线sda
//user interface
output led //led显示eeprom读写测试结果
);
//parameter define
parameter SLAVE_ADDR = 7'b1010000 ; //器件地址(SLAVE_ADDR)
parameter BIT_CTRL = 1'b1 ; //字地址位控制参数(16b/8b)
parameter CLK_FREQ = 26'd50_000_000 ; //i2c_dri模块的驱动时钟频率(CLK_FREQ)
parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率
parameter L_TIME = 17'd125_000 ; //led闪烁时间参数
parameter MAX_BYTE = 16'd256 ; //读写测试的字节个数
//wire define
wire dri_clk ; //I2C操作时钟
wire i2c_exec ; //I2C触发控制
wire [15:0] i2c_addr ; //I2C操作地址
wire [ 7:0] i2c_data_w; //I2C写入的数据
wire i2c_done ; //I2C操作结束标志
wire i2c_ack ; //I2C应答标志 0:应答 1:未应答
wire i2c_rh_wl ; //I2C读写控制
wire [ 7:0] i2c_data_r; //I2C读出的数据
wire rw_done ; //E2PROM读写测试完成
wire rw_result ; //E2PROM读写测试结果 0:失败 1:成功
//*****************************************************
//** main code
//*****************************************************
//e2prom读写测试模块
e2prom_rw #(
.MAX_BYTE (MAX_BYTE ) //读写测试的字节个数
) u_e2prom_rw(
.clk (dri_clk ), //时钟信号
.rst_n (sys_rst_n ), //复位信号
//i2c interface
.i2c_exec (i2c_exec ), //I2C触发执行信号
.i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
.i2c_addr (i2c_addr ), //I2C器件内地址
.i2c_data_w (i2c_data_w), //I2C要写的数据
.i2c_data_r (i2c_data_r), //I2C读出的数据
.i2c_done (i2c_done ), //I2C一次操作完成
.i2c_ack (i2c_ack ), //I2C应答标志
//user interface
.rw_done (rw_done ), //E2PROM读写测试完成
.rw_result (rw_result ) //E2PROM读写测试结果 0:失败 1:成功
);
//i2c驱动模块
i2c_dri #(
.SLAVE_ADDR (SLAVE_ADDR), //EEPROM从机地址
.CLK_FREQ (CLK_FREQ ), //模块输入的时钟频率
.I2C_FREQ (I2C_FREQ ) //IIC_SCL的时钟频率
) u_i2c_dri(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
//i2c interface
.i2c_exec (i2c_exec ), //I2C触发执行信号
.bit_ctrl (BIT_CTRL ), //器件地址位控制(16b/8b)
.i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
.i2c_addr (i2c_addr ), //I2C器件内地址
.i2c_data_w (i2c_data_w), //I2C要写的数据
.i2c_data_r (i2c_data_r), //I2C读出的数据
.i2c_done (i2c_done ), //I2C一次操作完成
.i2c_ack (i2c_ack ), //I2C应答标志
.scl (iic_scl ), //I2C的SCL时钟信号
.sda (iic_sda ), //I2C的SDA信号
//user interface
.dri_clk (dri_clk ) //I2C操作时钟
);
//led指示模块
rw_result_led #(.L_TIME(L_TIME ) //控制led闪烁时间
) u_rw_result_led(
.clk (dri_clk ),
.rst_n (sys_rst_n ),
.rw_done (rw_done ),
.rw_result (rw_result ),
.led (led )
);
endmodule上板验证结果:
Reference
- 正点原子 启明星ZYNQ之FPGA开发指南V3.2 第二十七章 UART串口通信实验
- 新新新手Icer练习(八):uart的基本组成及其verilog实现_哔哩哔哩_bilibili
- FPGA实现SPI接口(1)–什么是SPI接口?_fpga spi-CSDN博客
- 新新新手Icer练习(九):SPI基本原理及其Verilog仿真实现_哔哩哔哩_bilibili
- 正点原子 启明星ZYNQ之FPGA开发指南V3.2 第三十二章 基于IIC协议的EEPROM读写测试
- 新新新手Icer练习(十):IIC基本原理 + Verilog仿真实现 + 上板信号采集 (EEPROM数据读写测试)_哔哩哔哩_bilibili