Menu Close

SOC系列课程 添加PS I2C功能

前面我们基于Zynq实现了最基本的串口打印hello world功能,这一篇文章我们会基于hello world的基础上实现PS端I2C的功能。

完成功能: Zynq单板配置PS端I2C接口并且进行I2C功能验证。

实验平台:智芯融的BM7030开发板,其他开发板操作类似。

核心芯片:Xilinx Xc7z030-2ffg676I

实验步骤:Zynq配置PS的I2C 芯片,并进行写入读取。

I2C协议概述

I2C总线是PHLIPS公司在20世纪80年代推出的一种串行总线,实际上是目前全球使用最广泛地通信协议之一。

如果您正在使用HDMI显示器,各式传感器,通信模块,AD/DA,自动控制等等项目,您可能会发现正在使用I2C总线。

I2C即是一种总线,也是一种通讯协议。在嵌入式开发中,通讯协议可分为两层:物理层和协议层。物理层是数据在物理媒介传输的保障;协议层主要是规定通讯逻辑,同一收发双方的数据打包、解包标准。物理层相当于现实中的公路,而协议层则是交通规则,汽车可以在路上行驶,但是需要交通规则对行驶规则进行约束,不然将出现危险,也就是数据传输紊乱、丢包。

I2C可以支持多个设备,总线包含一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过该地址来访问不同设备。主机通过SDA线发送设备地址(SLAVE_ADDRESS)查找从机,SLAVE_ADDRESS可以是7位或10位,紧跟着SLAVE_ADDRESS的一个数据位用来表示数据传输方向,即第8位或11位。为0时表示写数据,为1时表示读数据。

数据有效性

SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。

换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。如图13-1为数据有效性的时序图。

https://pic1.zhimg.com/80/v2-2ca40d7d5aef1dfe773f0ffb62eccbd4_720w.png

(图片来自I2C总线协议文档)

起始条件S和停止条件P

起始条件S:当SCL高电平时,SDA由高电平向低电平转换;

停止条件P:当SCL高电平时,SDA由低电平向高电平转换。

起始和停止条件一般由主机产生。总线在起始条件后处于busy的状态,在停止条件的某段时间后,总线才再次处于空闲状态。如图13-2为起始和停止条件的信号产生时序图。

https://pic1.zhimg.com/80/v2-9ca988e77d6a42a227c7cd19fbef590c_720w.png

(图片来自I2C总线协议文档)

传输数据格式:传输的每个字节必须为8位,而总字节数不受限制。每个字节后必须跟一个响应位。首先开始传输的是数据最高位,即MSB位。如果此时从机正忙于其他功能,如正在中断服务程序,则需要使SCL线保持低电平迫使主机进入等待状态,直到从机准备完成。

响应ACK

https://pic1.zhimg.com/80/v2-1b94eb9c96d3a4bfa02ae36b29c537f4_720w.png

(图片来自I2C总线协议文档)

数据接收方收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权,将SDA电平拉高,由接收方控制。若希望继续,则给出“应答(ACK)”信号,即SDA为低电平;反之给出“非应答(NACK)”信号,即SDA为高电平。

通讯过程

通讯过程中会不断地往状态寄存器I2C_SRx写入参数,我们可以通过读取相应位的值来了解通讯状态。

下图为主发送器的通讯过程:

https://pic1.zhimg.com/80/v2-480c166721f555e232fd10e392a9a318_720w.png

这里简单说明下主发送器向外发送数据的过程。

主机产生起始信号S,产生“EV5”事件,对I2C_SR1寄存器的SB位置1,表示起始信号已发送;

接下来发送设备地址,并等待从机应答(A),从机应答后产生“EV6”和“EV8”事件,此时I2C_SR1寄存器的ADDR位和TxE位被置1。ADDR置1表示地址已发送,TxE置1表示数据寄存器为空;

往数据寄存器I2C_DR写入要发送的数据,此时TxE被置0,数据寄存器非空,通过SDA信号线逐位发送数据后,又会产生“EV8”事件,TxE位又被置1,并且从机给出应答。那么,重复这个过程,就可以连续发送多个字节的数据了;

数据发送完成后,产生停止信号P,产生“EV8_2”事件,此时I2C_SR1的TxE位和BTF位被置1,BTF位被置1表示字节发送结束。那么至此,通讯结束。

以上的过程会在I2C读写数据编程中作为参考。

另外,如果设置控制寄存器I2C_CR2相应位的中断使能,则以上事件产生时,都会进入I2C中断,进入中断程序后再通过检查寄存器的相应位来判断具体为哪个事件。

而接收数据的过程,也就是读数据的过程,有两次起始信号。第一次起始信号后,发送从设备地址找到从设备后,向从设备内部写入需要读取的数据地址;在第二次起始信号后才开始读取数据。

I2C传输的优缺点

优点:通信节点极少,硬件实现简单,可扩展性强的优点。I2C总线的另一优点是支持多主控,总线上任何能够进行发送/接收数据的设备都可以占领总线。当然,任意时间点上只能存在一个主控。

缺点:传输速率一般,只能支持半双工

硬件环境搭建

在BM7030中,I2C的PS端的硬件原理图设计如下: %title插图%num

上图中,我们可以看到PS的MIO14,15分配到I2C的SCL,SDA接口。

%title插图%num

BM7030的I2C的主要器件是PCA9548,这是个I2C的多工复用芯片,在这个项目中我们只和PCA9548进行通信,首先需要查找芯片I2C的地址。

%title插图%num

上图为硬件相关的I2C多工芯片,分为8路,分别接到EEPROM,始终器件,SFP光口,音频和HDMI接口。

%title插图%num

PCA的I2C的地址由A0,A1和A2的电平来决定,I2C地址位01110(A2)(A1)(A0)来确定,硬件设计中A2为高电平,A1和A0为低电平,最终I2C的地址位01110100,对应0x70。

Zynq的PS硬件配置

Vivado中配置Zynq IP核,PS端自带I2C协议控制器可以路由到MIO或EMIO。在硬件上可以自由配置:

在Vivado的Block Diagram中点击Zynq IP核,点击Peripheral I/o Pins,在此我们使用I2C的MIO口去配置I2C,查看BM7030硬件原理图中PS端MIO口的配置如下为MIO14和MIO15,可以核对和原理图的IO管脚是否一致。

C:\Users\HW-PC\OneDrive\1_SOC教学\I2c\1.jpg

在Block Design空白处点击右键,点击Validate Design,点击确定

右键点击ARM_SOCCreate HDL Wrapper形成新的arm_soc_wrapper文件,wrapper文件就是将Processing_System7_0封装为一个Verilog接口的文件

%title插图%num

选择Let Vivado manage Wrapper and auto-update,生成新的arm_soc_wrapper.v 文件

%title插图%num

生成新的arm_soc_wrapper.v文件后,因为FPGA外部接口未改变,arm_soc_top.v最后形成的顶层文件不用更改;点击保存,模块会自动分层,source文件层结构如下图:

%title插图%num

点击左侧Generate Bitstream,会生成bit文件

当Bitstream 生成后操作如下:FileExportHardwareExport Hardware选择Include bitstream OK

FileLaunch SDK 会生成SDK界面,从Vivado到SDK界面,是硬件环境到软件环境的转换

%title插图%num %title插图%num

SDK start界面如下图所示:

其中硬件描述文件system.hdf 列出了每个PS7-cortexta9_[0-1]中每个模块的地址映射,如ps7_uart_0的起始地址位为0xe0000000,最高地址位为0xe0000fff. 新增加的PS7_I2C _0的起始地址位0xe0004000,最高地址位为0xe0004fff。

%title插图%num

建立应用程序项目:FilenewApplication Project,命名为I2C_test,

%title插图%num

选择空白模板empty Application:

%title插图%num

点击src点击右键,分别增加hellowworld项目中的初始化文件,platform.c,platform_config.h和platform.h这三个文件,新增加以及主函数文件main.c文件。

%title插图%num

I2C的main函数比较简单,注释如下。代码参考了CSDN的

https://blog.csdn.net/qq_41332806/article/details/106583937网站:

主要进行硬件地址的初始化,I2C的数据配置,具体可以参考代码如下:

#include <stdio.h>

#include “xil_printf.h”

#include “xparameters.h”

#include “xiicps.h”

// IIC device ID

#define IIC_DEV_ID XPAR_PS7_I2C_0_DEVICE_ID

#define IIC_RATE 200000

//7位设备地址

#define SLAVE_ADDR 0x70

static XIicPs IicPs;

static XIicPs_Config * IicPs_Cfg;

//测试配置列表

u8 configList[12][2]={

{0x00,0x00},

{0x01,0x01},

{0x02,0x02},

{0x03,0x03},

{0x04,0x04},

{0x05,0x05},

{0x06,0x06},

{0x07,0x07},

{0x08,0x08},

{0x09,0x09},

{0x0A,0x0A},

{0x0B,0x0B}

};

// 初始化IIC,并设置IIC速率

int initIic()

{

int status;

// 1.查找IIC设备

IicPs_Cfg = XIicPs_LookupConfig(IIC_DEV_ID);

// 2.初始化

status = XIicPs_CfgInitialize(&IicPs, IicPs_Cfg, IicPs_Cfg->BaseAddress);

if(status != XST_SUCCESS)

{

print(“initial IIC failed \n”);

return XST_FAILURE;

}

//设置IIC速率

status = XIicPs_SetSClk(&IicPs, IIC_RATE);

if(status != XST_SUCCESS)

{

print(“set IIC clock rate failed \n”);

return XST_FAILURE;

}

return XST_SUCCESS;

}

/******************************************************************

* function IIC完成单个寄存器的配置

*

* @parameter : XIicPs * iicPs =====> IIC设备结构体

* @parameter : u16 slaveAddr =====> IIC从机设备地址

* @parameter : u8 * Cfg_Ptr ====> 配置寄存器的指针

*

* @return s32 XST_SUCCESS or XST_FAILURE

******************************************************************/

s32 writeReg(XIicPs * iicPs, u16 slaveAddr, u8 * Cfg_Ptr)

{

s32 status ;

//IIC写入数据,从机地址,寄存器地址和写入的数据

status = XIicPs_MasterSendPolled(iicPs, Cfg_Ptr, 2, SLAVE_ADDR );

if (status != XST_SUCCESS)

{

printf(“configure register failed! \n”);

return XST_FAILURE;

}

//两次IIC写入之间保持一定间隔

usleep(8000);

return status;

}

/******************************************************************

* function IIC从寄存器中读出数据

*

* @parameter : XIicPs * iicPs =====> IIC设备结构体

* @parameter : u16 slaveAddr =====> IIC从机设备地址

* @parameter : u8 * Cfg_Ptr ====> 配置寄存器的指针

*

* @return u8 registerData =====> 从寄存器中读出的数据

******************************************************************/

u8 readReg(XIicPs * iicPs, u16 slaveAddr, u8 * regAddr)

{

s32 status ;

u8 registerData;

//发送设备地址,寄存器地址

status = XIicPs_MasterSendPolled(iicPs, regAddr, 1, SLAVE_ADDR );

if (status != XST_SUCCESS)

{

printf(“configure register failed! \n”);

return XST_FAILURE;

}

//从寄存器中读出数据

status = XIicPs_MasterRecvPolled(iicPs, &registerData, 1, SLAVE_ADDR);

if (status != XST_SUCCESS)

{

printf(“configure register failed! \n”);

return XST_FAILURE;

}

return registerData;

}

int main()

{

init_platform();

u8 dataBuf[31];

//初始化IIC

int status = initIic();

if(status != XST_SUCCESS)

{

printf(“initialize IIC failed \n”);

return XST_FAILURE;

}

//依次写入配置列表

for(int i=1; i < 12; i++ )

{

writeReg(&IicPs, SLAVE_ADDR, configList);

}

//将值从寄存器中读出

for(int i = 0; i < 12; i++)

{

dataBuf = readReg(&IicPs, SLAVE_ADDR, &configList[0]);

printf(“dataBuf[%d] = %x \n”,i , dataBuf);

}

print(“Hello World\n\r”);

cleanup_platform();

return 0;

}

该程序是I2C设备初始化的程序,使用writeReg()和readReg()函数对I2C的写入和读取,这两个函数会执行一次对I2C寄存器的读取和写入,子代码中包括Xilinx自带驱动程序进行XIicPs_MasterSendPolled()读取。自检成功返回XST_SUCCESS;读取或写入某个寄存器失败时返回XST_REGISTER_ERROR。

在BM7030主板上,ARM_UART接口和Zynq7030芯片之间通过USB转UART的芯片,实现USB到ARM之间的串口连接。将计算机的USB接口与BM7030 的ARM_UART(USB B type) 相连,使得计算机可以通过USB进行程序烧录和串口调试,如下图

%title插图%num

设置调试环境如下图:

在SDK界面中设置:

在hello world项目点击右键,选择Debug As Debug Configurations

进入下图,在system Debugger点击右键,选择New

%title插图%num

%title插图%num

设置如下图:

生成Debug选项“System Debugger using Debug_helloworld.elf on Local”

勾选Reset entire system和Program FGPA选项,这样每次点击Debug回重新烧录程序和重启SOC系统

点击Apply,点击Debug,进入Debug界面

%title插图%num

打开SDK自带计算机串口终端并单步运行程序:

如下图:选中SDK Terminal

点击绿色+,并选合适的UART端口,如COM8

注意:由于JTAG接口和UART在计算机device manager 下都是映射到UART端口,因此会有多个串口出现,用户需要自行确认(可以采用USB插拔的方法,确认正确的USB端口对应的UART接口)

设置完成后按F6或者点击Run Step over,进行单步调试,如果已经运行完成,可以按F11或者点击Run Debug重新载入Debug程序。(有时候需要重新launch SDK程序)

%title插图%num

调试结果如下图所示:在SDK Terminal 窗口中正确显示结果,

备注:需要按debug键(F11)进入debug模式,然后F6才生效,或者直接Run程序也可以。

如果I2C配置成功,串口则会打印如下信息:

%title插图%num

本文的项目主要是进行I2C的的硬件初始化,后续会有更深入的应用,比如会应用I2C对HDMI进行配置等等。

 

Posted in C语言, SoC

发表评论

相关链接