【SoC FPGA学习】七、基于虚拟地址映射的 I2C 编程应用

在前面两节的内容中, 通过虚拟地址映射的形式,已经完成了基于虚拟地址映射的 PIO、 UART IP 核的使用。这些 IP 核对于使用过基于 NIOS II 的SOPC 技术开发的读者,应该来说已经是十分的熟悉了。而这个 oc_i2c 核,则是一个第三方开源的 IP 核, 不仅提供了完整的 IP 手册, 而且 Linux 系统中也有对该 IP 核的驱动支持。使用起来非常的方便。 而 Intel 在 Platform Designer 中提供的 Avalon I2C (Master) IP 核,由于 Linux 系统源码中没有针对该 IP 核的驱动源码, 网络上也暂无相关资料,因此本书选择使用 oc_i2c 核来作为系统的I2C 控制器

I2C 协议在基于 FPGA 的系统中应用非常广泛, 从简单的 EEPROM 存储器, 到各种视频图像收发器和传感器,以及电容触摸屏等, 都以 I2C 协议作为基本的控制接口的通信协议。为了方便读者在自己的系统中应用该 IP 核。 本节将针对该 IP 核详细介绍其功能和寄存器映射,并给出基于虚拟地址映射的驱动设计实例。

一、OpenCores I2C IP 简介

I2C 是一种两线制双向通行的串行总线, 支持三种通行速率,分别为标准速度(100Kbps)、 快速(400Kbps)、 高速(3.5Mbps), 高速模式需要 IO 口能够支持相应的通信速率。

OpenCores I2C IP 核实现了 I2C 协议主机的功能, 该 IP 核能够支持400Kbit/s 的快速通信模式和 100Kbit/s 的标准通信模式。其特性如下所示:

  • 兼容飞利浦公司的 I2C 协议标准
  • 支持多主机操作控制
  • 支持软件可编程的时钟频率
  • 支持时钟拉伸和等待状态的生成
  • 软件可编程的应答位
  • 中断或查询方式的字节数据传输
  • 仲裁丢失中断并自动取消传输
  • 支持产生起始位、停止位、重复起始位和应答位
  • 支持检测起始位、停止位、重复起始位
  • 总线忙状态检测
  • 支持 7 位和 10 位地址模式
  • 支持较宽范围是输入时钟频率
  • 支持 3.5Mps 高速模式、 400Kbit/s 的快速通信模式和 100Kbit/s的标准通信模式

OpenCores 网站上下载到的原版 IP 核是使用的 Wishbone 总线, 没有默认对 Avalon MM 总线提供支持。但是实际上 Wishbone 总线的 Slave 接口可以直接与 Avalon MM Slave 接口兼容,因此只需在 IP 封装时进行简单的映射即可实现。本节不对如何进行 Wishbone 总线到 Avalon MM 总线的映射做讲解,仅提供一个映射好的 oc_i2c IP 核。使用时,只需将其整个文件夹拷贝到用户当前工程目录下, 就可以在打开 Platform Designer 的时候自动识别到该 IP。 接下来用户就可以直接像添加 uart 或 pio 核一样添加该 IP 了

二、OpenCores I2C IP 寄存器映射

OpenCores I2C IP 内部提供 7 个 8 位的寄存器,但是实际占用的地址空间仅为 5 个, 其中发送寄存器和接收寄存器使用同一个地址,状态寄存器和命令寄存器使用同一个地址,如下表所示:

名称地址偏移读写属性寄存器描述
PRERlo0x0可读可写时钟频率预分频寄存器低字节
PRERhi0x1可读可写时钟频率预分频寄存器高字节
CTR0x2可读可写时钟频率预分频寄存器高字节
TXR0x3仅写发送寄存器
RXR0x3仅读接收寄存器
CR0x4仅写命令寄存器
SR0x4仅读状态寄存器
  • PRER: 时钟频率预分频寄存器

该寄存器用来设置 SCL 信号的频率, 该 I2C 控制器内部是使用的 5 倍的SCL 时钟频率作为基本时钟的,因此,预分频寄存器需要配置 5 倍的预期 SCL信号的频率。当控制寄存器中的 EN 位为 0 时,可以通过软件编程修改该寄存器的值。

例如,如果 IP 核的输入时钟频率为 50MHz, 期望的 SCL 频率为100KHz, 那么

prescale = 50 𝑀𝐻𝑧5100 𝐾𝐻ℎ𝑧− 1 = 99(𝑑𝑒𝑐) = 63(ℎ𝑒𝑥)

因此需向 PRERlo 寄存器中写入 0x63, 向 PRERhi 寄存器中写入 0x00

  • CTRL:控制寄存器

该寄存器中的各个位设定了 I2C IP 核工作时的各种属性,如 FIFO 门限值、总线速率、 使能开关等。

数据位位意义位功能描述
bit7ENIP核工作使能位 1:使能 I2C IP核工作 0:禁止 I2C IP核工作
bit6IENIP核终端使能位 1:使能 I2C IP 核产生中断 0:禁止I2C IP核产生中断
bit[5:0]保留保留位,未使用

该 IP 核仅在控制寄存器中的 EN 位为 1 时才响应新的命令,只有当 IP 核没有进行数据传输时才能清零该位。 如果在一个传输过程中清零该位, IP 核会将 I2C 总线挂起

  • TXR: 发送数据寄存器

该寄存器中的有效数据位分成三个部分,用以产生起始位和结束位的 STA(bit9)、 STO(bit8) 位

数据位位意义位功能描述
bit[7:1]AD下一个需要通过 I2C 传输的字节数据
bit0RW_D当该次传输处于 I2C 协议中的地址相时,该位数据位指定了 I2C 传输的方向,为 0 表示 I2C 写从机传输,为 1 表示 I2C 读传输。 当处于 I2C 协议中的数据相时, 该位数据代表了需要发送的数据的bit[0]
  • RXR: 接收数据寄存器

该寄存器存储了 I2C 控制器最新一次接收到的数据。

  • CR: 命令寄存器

该寄存器中的各个位设置了 I2C IP 在进行传输时的各项功能, 例如是否产生起始位、应答位、结束位等。

数据位位意义位功能描述
bit7STA起始位,当该位为 1 时,在本次的字符数据数据传输前会先产生一个起始位。
bit6STO停止位,当该位为 1 时,在本次的字符数据数据传输结束后会产生一个停止位
bit6RD从从机读数据
bit7WR向从机写数据
bit3ACK应答位,当作为接收方时,发送 ACK(ACK=0) 或 NACK(ACK= 1)
bit[2:1]保留位保留位
bit0IACK中断响应,当向该位写 1,清除中断。

注意,该寄存器为只写型寄存器, 只能向其中写入数据, 读取该寄存器读到的永远是 0。

  • SR:状态寄存器
数据位位意义位功能描述
bit7RxACK从机应答位状态,该位指示了是否从从机接收到了应答位。1: 接收到了 NACK2: 接收到了有效的 ACK
bit6BUSY总线忙状态标志,当该位为 1 是,表明 I2C 控制器正忙。 检测到起始位,该位变为 1, 检测到结束位,该位变为 0。
bit5AL失去总线仲裁,当检测到结束位,但是并没有读写请求, 或者I2C 控制器设置 SDA 信号为高,但是 SDA 信号却是低电平。 此时 AL 位会置 1, 指示 I2C 控制器获取仲裁失败。
bit[4:2]保留位保留位
bit1TIP当前正在传输数据,当该位为 1 时表明 I2C 控制器正在传输(接收或发送) 数据, 该位为 0 表明传输完成
bit0IF中断标志, 当有中断条件发生时,该位变为 1, 在 IEN 位为 1 的情况下, 会产生中断信号。 以下两种情况会使得中断标志位置1:一个字节的传输结束仲裁丢失

三、I2C IP 核应用实例

在了解了 I2C IP 核的寄存器映射之后,就可以根据寄存器功能编写简单的 I2C 控制器的驱动程序了。

3.1、在 DS-5 中建立 I2C 应用工程

要设计 I2C IP 应用工程,第一步是在 DS-5 软件中创建工程。在 DS-5 软件中选中上一节创建的 fpga_uart 工程, 复制并粘贴为新的工程,命名为fpga_i2c, 以建立好基本的工程。 在复制完成之后, 依旧先将新工程下的 Debug目录删除, 这样就完成了 i2c 应用工程的创建。

3.2、虚拟地址映射

第二步还是完成虚拟地址映射。映射方式和 led_pio、 uart_0 外设一致, 可以直接在 fpga_uart 实验中以有代码的基础上添加 i2c_0 的部分即可,代码如下所示:

static volatile unsigned char *i2c_0_virtual_base = NULL;	//i2c_0虚拟地址

int fpga_init(long int *virtual_base) {
	int fd;
	void *periph_virtual_base;	//外设空间虚拟地址

	//打开MPU
	if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
		printf("ERROR: could not open \"/dev/mem\"...\n");
		return (1);
	}

	//将外设地址段映射到用户空间
	periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
	MAP_SHARED, fd, HW_REGS_BASE);
	if (periph_virtual_base == MAP_FAILED) {
		printf("ERROR: mmap() failed...\n");
		close(fd);
		return (1);
	}
	//映射得到i2c_0外设虚拟地址
	i2c_0_virtual_base = (unsigned char *)(periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + I2C_0_BASE)
					& (unsigned long) ( HW_REGS_MASK)));
	*virtual_base = periph_virtual_base;	//将外设虚拟地址保存,用以释放时候使用
	return fd;
}

虚拟地址映射的格式,相信读者经过前面两节实验的学习,已经非常的熟悉了,本节就不再赘述。 只不过,与之前的 uart 和 pio 核不同的是,本 I2C IP核,是一个总线位宽为 8 位的 IP 核, 寄存器是按照字节为单位进行编址的,因此,在定义该 IP 的虚拟地址指针时,需要定义为 8 位的 char 型, 而不是之前的 PIO 和 UART 核的 long 型。 同时,在最后得到 i2c_0_virtual_base 的值时,也使用了类型转换将 long 型的虚拟地址转换为了 char 型

经过虚拟地址映射, 在程序中得到了一个名为 i2c_0_virtual_base 的地址,使用该地址就能够读写 I2C IP 核的各个寄存器了。

对于该 I2C 控制器, 在下载的源码中提供了一个寄存器描述文件, 名为“oc_i2c_master.h”, 在该文件中,详细描述了每个寄存器的偏移地址, 以及各个功能位的掩码。 该文件内容如下所示:

/*
/
                                                             
  Include file for OpenCores I2C Master core                 
                                                             
  File    : oc_i2c_master.h                                  
  Function: c-include file                                   
                                                             
  Authors: Richard Herveille (richard@asics.ws)              
           Filip Miletic                                     
                                                             
           www.opencores.org                                 
                                                             
/
                                                             
 Copyright (C) 2001 Richard Herveille                        
                    Filip Miletic                            
                                                             
 This source file may be used and distributed without        
 restriction provided that this copyright statement is not   
 removed from the file and that any derivative work contains 
 the original copyright notice and the associated disclaimer.
                                                             
     THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY     
 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED   
 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS   
 FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL THE AUTHOR      
 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,         
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES    
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE   
 GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR        
 BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF  
 LIABILITY, WHETHER IN  CONTRACT, STRICT LIABILITY, OR TORT  
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT  
 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE         
 POSSIBILITY OF SUCH DAMAGE.                                 
                                                             
/
*/

/*
 * Definitions for the Opencores i2c master core
 */

/* --- i2c master's 寄存器偏移地址 --- */
	
/* ----- 可读可写型寄存器                                            */

#define OC_I2C_PRER_LO 0x00     /* 时钟频率预分频寄存器低字节偏移地址  */
#define OC_I2C_PRER_HI 0x01     /* 时钟频率预分频寄存器高字节偏移地址 */
#define OC_I2C_CTR     0x02     /* 控制寄存器偏移地址                  */
										
/* ----- 只写型寄存器                                         */
										
#define OC_I2C_TXR     0x03     /* 发送寄存器偏移地址            */
#define OC_I2C_CR      0x04     /* 命令寄存器偏移地址            */
	
/* ----- 只读性寄存器     */
										
#define OC_I2C_RXR     0x03     /* 接收寄存器偏移地址             */
#define OC_I2C_SR      0x04     /* 状态寄存器偏移地址             */
	
/* ----- 位定义                                             */
	
/* ----- 控制寄存器                                     */
#define OC_I2C_EN (1<<7)        /* IP核使能位,为1使能,为0禁止*/
#define OC_I2C_IEN (1<<6)       /* 中断使能位,为1使能中断,为0禁止中断              */

/* ----- 命令寄存器                                        */
 
#define OC_I2C_STA (1<<7)       /* 产生起始位*/
#define OC_I2C_STO (1<<6)       /* 产生结束位 */
#define OC_I2C_RD  (1<<5)       /* 从从机读*/
#define OC_I2C_WR  (1<<4)       /* 向从机写 */
#define OC_I2C_ACK (1<<3)       /* 作为接收方,给从机产生应答,  0:ACK;1:NACK*/
#define OC_I2C_IACK (1<<0)      /* 中断响应位*/

/* ----- 状态寄存器                                      */

#define OC_I2C_RXACK (1<<7)     /* 来自从机的应答  ,1:NACK;  0:ACK*/
#define OC_I2C_BUSY  (1<<6)     /* I2C传输忙标志位                           */
#define OC_I2C_TIP   (1<<1)     /* I2C传输过程标志位 */
#define OC_I2C_IF    (1<<0)     /* 中断标志位 */

/* 位置位和清除宏函数定义                                     */

#define OC_ISSET(reg,bitmask)       ((reg)&(bitmask))
#define OC_ISCLEAR(reg,bitmask)     (!(OC_ISSET(reg,bitmask)))
#define OC_BITSET(reg,bitmask)      ((reg)|(bitmask))
#define OC_BITCLEAR(reg,bitmask)    ((reg)|(~(bitmask)))
#define OC_BITTOGGLE(reg,bitmask)   ((reg)^(bitmask))
#define OC_REGMOVE(reg,value)       ((reg)=(value))

使用这些定义,就可以在程序中使用定义来代表具体的数值了,方便程序和阅读的维护。例如要想向该 I2C IP 核的预分频寄存器的高低字节中分别写入0x00,0x63, 就可以使用下面的代码:

*(i2c_0_virtual_base + OC_I2C_PRER_HI) = 0x00; //写预分频寄存器高字节
*(i2c_0_virtual_base + OC_I2C_PRER_LO) = 0x63; //写预分频寄存器高字节

而无需再使用下面的直接写偏移地址值的方式。

*(i2c_0_virtual_base + 1) = 0x00; //写预分频寄存器高字节
*(i2c_0_virtual_base + 0) = 0x63; //写预分频寄存器高字节

3.3、I2C IP 核基本寄存器配置

在使用 I2C IP 核进行基本的数据传输之前,先要对其中的一些寄存器进行合理配置。 例如分频寄存器、 控制寄存器。

另外由于本例是基于虚拟地址操作的,在用户空间无法使用中断,因此开启中断没有什么意义,所以 CTR 寄存器的值全写 0。编程时,使用查询的方式来获知当前的各种状态。

另外需要设置 I2C 控制器的预分频寄存器,例如设置 I2C 总线通信速率为100Kbps,则

prescale = ( (50𝑀𝐻𝑧 / (5100 𝐾𝐻ℎ𝑧) )1 = 99(𝑑𝑒𝑐) = 63(ℎ𝑒𝑥)

因此使用下面的代码来设置分频寄存器

uint32_t prescale; //预分频值
prescale = 50000000/(speed * 5) - 1; //计算得到预分频值, speed 为期望速率
*(i2c_0_virtual_base + OC_I2C_PRER_HI) = prescale >> 8; //写预分频寄存器高字节
*(i2c_0_virtual_base + OC_I2C_PRER_LO) = prescale & 0xff;//写预分频寄存器低字节

完整的 I2C 控制器初始化代码如下所示:

int i2c_init(int speed)
{
	uint32_t prescale;	//预分频值
	prescale = 50000000/(speed * 5) - 1;	//计算得到预分频值,speed为期望速率
	*(i2c_0_virtual_base + OC_I2C_PRER_HI) = prescale >> 8;	//写预分频寄存器高字节
	*(i2c_0_virtual_base + OC_I2C_PRER_LO) = prescale & 0xff;//写预分频寄存器低字节
	*(i2c_0_virtual_base + OC_I2C_CTR) = OC_I2C_EN;	//控制寄存器
	return 0;
}

其中,函数的传入参数 speed 是 I2C 总线速率。

完成设置后,就可以通过读状态寄存器来判断当前的工作状态, 写发送数据寄存器来指定下一个要传输的字节内容,写命令寄存器来指定下一次传输的属性。

3.4、使用 I2C IP 读写图像传感器寄存器

前文提到, I2C 总线多用于各种视频图像收发器和传感器的控制接口,包括常见的图像传感器如 OV5640、 OV7670, HDMI 收发器 ADV7513、 PAL 视频解码器 ADV7180 等。 对于图像传感器的控制接口,图像传感器厂家一般将其称之为 SCCB 接口,不过这是一种兼容 I2C 接口的协议, 可以直接使用 I2C控制器来进行通信。 接下来本节将针对 OV5640 CMOS 摄像头的 I2C 接口协议,编写该 I2C 控制器的应用代码。

使用 SCCB 接口与传感器通信,包含了两种情况,分别为 1 字节存储器地址和 2 字节存储器地址。

例如,如下图为 OV7670(1 字节存储器地址)写一个字节数据到指定存储单元的传输时序图:

在这里插入图片描述

如下图为 OV5640(2 字节存储器地址) 写一个字节数据到指定存储单元的传输时序图:

在这里插入图片描述
可以看到, 对于 OV7670, I2C 主机写一个字节的数据需要在一次传输中发送三个字节,三个字节的意义分别为器件地址(ID Address, OV7670 的器件地址为 0x42)、 寄存器地址(Sub Address)、 写入数据(Write Data)。 而对于OV5640, I2C 主机写一个字节的数据需要在一次传输中发送四个字节, 四个字节的意义分别为器件地址(ID Address, OV5640 的器件地址为 0x78)、 寄存器地址高字节(Sub Address High Byte)、 寄存器地址低字节(Sub Address LowByte)、 写入数据(Write Data)。

每次传输时,第一个字节之前都会附加上起始位,最后一个字节结尾时都会加上停止位。 据此, 就可以编写基本的 I2C 主机向图像传感器写入一个字节数据的函数了,代码清单如下所示:

//使用I2C控制器写传输一个字节数据
void i2c_wr_1byte(unsigned char data, unsigned char cmd)
{
	int I2C_SR;	//状态变量

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	*(i2c_0_virtual_base + OC_I2C_TXR) = data;//将需要发送的内容写入发送寄存器
	*(i2c_0_virtual_base + OC_I2C_CR) = cmd; //写命令寄存器
}

//使用I2C控制器写一个值到寄存器中
void i2c_wr_reg(unsigned char dev_id, unsigned short sub_addr, bool mode, unsigned char data)
{
	i2c_wr_1byte(dev_id, OC_I2C_STA | OC_I2C_WR);	//产生起始地址并发送器件地址
	if(mode)	//模式为1 ,则为2字节寄存器地址模式
		i2c_wr_1byte(sub_addr >> 8, OC_I2C_WR);	//发送寄存器地址高字节

	i2c_wr_1byte(sub_addr & 0xff, OC_I2C_WR);	//发送寄存器地址低字节
	i2c_wr_1byte(data, OC_I2C_STO | OC_I2C_WR);//发送需要写的数据,并同时产生结束位
}

发送一个字节数据的基本流程为:

  • 1、读取状态寄存器的 bit1 位以检测 I2C 控制器是否处于传输过程中,如果处于传输过程中,则需等待传输完成。
  • 2、向发送寄存器中写入一个字节的待发送的数据。
  • 3、写命令寄存器,根据是否需要产生起始位、应答位或结束位设置对应位的值,并置 WR 位为 1 来使能本次写操作。

i2c_wr_1byte(unsigned char data, unsigned char cmd)该函数为传输单个字节的函数, 其包含了两个参数。

  • 1、data 为此次需要传输的字节内容
  • 2、cmd 为配合此次传输所需的命令寄存器的值,例如需要同时产生起始位并写数据,则可以使得 cmd= OC_I2C_STA | OC_I2C_WR, 如果需要同时产生结束位并写数据, 则可以使得 cmd= OC_I2C_STO |OC_I2C_WR。

i2c_wr_reg(unsigned char dev_id, unsigned short sub_addr, bool mode, unsigned char data)函数为写一个字节数据到指定设备的指定寄存器的函数

  • 1、dev_id 参数为设备的 ID, 例如对于 OV7670,该设备地址值为0x42,而对于 OV5640,该设备地址值为 0x78。
  • 2、sub_addr 参数为存储器地址, 这是一个 16 位的变量, 因此可以兼容单字节和双字节寄存器地址的设备,
  • 3、mode 参数用以指定存储器地址模式为 1 则表示 2 字节寄存器地址模式,为 0 则表示字节地址模式。
  • 4、data 为要写入到寄存器中的数据。

当需要对 OV5640 摄像头的某个寄存器写入确定的数据时,仅需调用该函数并传入指定的参数即可,例如,向 OV5640 的 0x3622 寄存器写入数据 1 的代码为:

i2c_wr_reg(0x78, 0x3622, 1, 1); //写 OV5640 的 0x3622 寄存器的值为1

使用 SCCB 接口与传感器通信读取一个存储器中的数据,与读取传统的EEPROM 存储器存在一些差别。读取同样也包含了两种情况,分别为 1 字节存储器地址和 2 字节存储器地址。例如,下图为 OV7670(1 字节存储器地址)的从一个寄存器中读取一个数据的传输时序图:

在这里插入图片描述

可以看到, 对于 OV7670, I2C 主机读一个字节的数据需要一次写和一次读操作组合完成。在第一次的传输中发送两个字节, 分别为器件地址写传输和 sub-address, 第二次传输发送一个字节,读取一个字节,发送的内容为器件地址读传输和读取到的字符数据。其中与典型的 EEPROM 存储器传输不同的地方在于,在第一次传输的第二个字节发送完成并得到 ACK 信号之后, 对于EEPROM,无需产生停止位,只需重新产生起始位并传输器件地址,即可读取到数据,而对于 OV7670, 在第一次传输的第二个字节发送完成并得到 ACK信号 之后, 必须先发送停止位,然后在发送新的起始位和器件地址,才能读到新的数据

对于 OV5640, 与 OV7670 的读操作本质上相同,区别仅在于 subaddress 是一个字节还是两个字节,这里不再赘述。据此就可以编写基本的 I2C主机从图像传感器读取一个字节数据的函数了。 代码清单如下所示:

//使用I2C控制器读一个字节数据
unsigned char i2c_rd_1byte(unsigned char cmd)
{
	int I2C_SR;	//状态变量

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	*(i2c_0_virtual_base + OC_I2C_CR) = cmd; //写命令寄存器

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	//从RX寄存器读取一个字节的数据并返回
	return *(i2c_0_virtual_base + OC_I2C_RXR);
}

//使用I2C控制器写一个值到寄存器中
unsigned char i2c_rd_reg(unsigned char dev_id, unsigned short sub_addr, bool mode)
{
	i2c_wr_1byte(dev_id, OC_I2C_STA | OC_I2C_WR);	//产生起始地址并发送器件地址
	if(mode)	//模式为1 ,则为2字节寄存器地址模式
		i2c_wr_1byte(sub_addr >> 8, OC_I2C_WR);	//发送寄存器地址高字节

	//发送寄存器地址低字节并产生结束位
	i2c_wr_1byte(sub_addr & 0xff, OC_I2C_STO | OC_I2C_WR);

	//产生起始地址并发送器件地址,读操作
	i2c_wr_1byte(dev_id | 0x1, OC_I2C_STA | OC_I2C_WR);

	//返回读取到的一个字节数据
	return i2c_rd_1byte(OC_I2C_STO | OC_I2C_RD | OC_I2C_ACK);
}

当需要从 OV5640 摄像头的某个寄存器读取一个数据时,仅需调用该函数并传入指定的参数即可, 其返回值即为读取到的数据。 例如,从 OV5640 的0x3622 寄存器读取一个数据的代码为:

unsigned char reg; //定义数据临时变量
reg = i2c_rd_reg(0x78, 0x3622, 1);//读取 OV5640 的 0x3622 寄存器的值
printf("reg is %x\n",reg); //打印读取到的内容

3.5、I2C IP 读写 OV5640 摄像头板级调试

通过上述内容, 完成了基于虚拟地址映射的 OpenCore I2C IP 核的控制。这里设计一个简单的测试程序,程序会首先读取摄像头的 ID 并打印,正常情况下读取到的值应该为 0x5640, 然后向 0x3622 寄存器写入一个值“1”,再读回并打印。 以下为程序清单:

//gcc标准头文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <stdbool.h>

//hps 厂家提供的底层定义头文件
#define soc_cv_av	//定义使用soc_cv_av硬件平台

#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"

//与用户具体HPS应用系统相关的硬件描述头文件
#include "hps_0.h"

#include "oc_i2c_master.h"

#define HW_REGS_BASE (ALT_STM_OFST )	//HPS外设地址段基地址
#define HW_REGS_SPAN (0x04000000 )		//HPS外设地址段地址空间
#define HW_REGS_MASK (HW_REGS_SPAN - 1 )	//HPS外设地址段地址掩码

static volatile unsigned char *i2c_0_virtual_base = NULL;	//i2c_0虚拟地址

int fpga_init(long int *virtual_base) {
	int fd;
	void *periph_virtual_base;	//外设空间虚拟地址

	//打开MPU
	if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
		printf("ERROR: could not open \"/dev/mem\"...\n");
		return (1);
	}

	//将外设地址段映射到用户空间
	periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
	MAP_SHARED, fd, HW_REGS_BASE);
	if (periph_virtual_base == MAP_FAILED) {
		printf("ERROR: mmap() failed...\n");
		close(fd);
		return (1);
	}
	//映射得到i2c_0外设虚拟地址
	i2c_0_virtual_base = (unsigned char *)(periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + I2C_0_BASE)
					& (unsigned long) ( HW_REGS_MASK)));
	*virtual_base = periph_virtual_base;	//将外设虚拟地址保存,用以释放时候使用
	return fd;
}


int i2c_init(int speed)
{
	uint32_t prescale;	//预分频值
	prescale = 50000000/(speed * 5) - 1;	//计算得到预分频值,speed为期望速率
	*(i2c_0_virtual_base + OC_I2C_PRER_HI) = prescale >> 8;	//写预分频寄存器高字节
	*(i2c_0_virtual_base + OC_I2C_PRER_LO) = prescale & 0xff;//写预分频寄存器高字节
	*(i2c_0_virtual_base + OC_I2C_CTR) = OC_I2C_EN;	//控制寄存器
	return 0;
}

//使用I2C控制器写传输一个字节数据
void i2c_wr_1byte(unsigned char data, unsigned char cmd)
{
	int I2C_SR;	//状态变量

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	*(i2c_0_virtual_base + OC_I2C_TXR) = data;//将需要发送的内容写入发送寄存器
	*(i2c_0_virtual_base + OC_I2C_CR) = cmd; //写命令寄存器
}

//使用I2C控制器写一个值到寄存器中
void i2c_wr_reg(unsigned char dev_id, unsigned short sub_addr, bool mode, unsigned char data)
{
	i2c_wr_1byte(dev_id, OC_I2C_STA | OC_I2C_WR);	//产生起始地址并发送器件地址
	if(mode)	//模式为1 ,则为2字节寄存器地址模式
		i2c_wr_1byte(sub_addr >> 8, OC_I2C_WR);	//发送寄存器地址高字节

	i2c_wr_1byte(sub_addr & 0xff, OC_I2C_WR);	//发送寄存器地址低字节
	i2c_wr_1byte(data, OC_I2C_STO | OC_I2C_WR);//发送需要写的数据,并同时产生结束位
}

//使用I2C控制器读一个字节数据
unsigned char i2c_rd_1byte(unsigned char cmd)
{
	int I2C_SR;	//状态变量

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	*(i2c_0_virtual_base + OC_I2C_CR) = cmd; //写命令寄存器

	do{
		I2C_SR = *(i2c_0_virtual_base + OC_I2C_SR);	//读取状态寄存器
	}while(I2C_SR & OC_I2C_TIP);	//判断TIP位是否为0

	//从RX寄存器读取一个字节的数据并返回
	return *(i2c_0_virtual_base + OC_I2C_RXR);
}

//使用I2C控制器写一个值到寄存器中
unsigned char i2c_rd_reg(unsigned char dev_id, unsigned short sub_addr, bool mode)
{
	i2c_wr_1byte(dev_id, OC_I2C_STA | OC_I2C_WR);	//产生起始地址并发送器件地址
	if(mode)	//模式为1 ,则为2字节寄存器地址模式
		i2c_wr_1byte(sub_addr >> 8, OC_I2C_WR);	//发送寄存器地址高字节

	//发送寄存器地址低字节并产生结束位
	i2c_wr_1byte(sub_addr & 0xff, OC_I2C_STO | OC_I2C_WR);

	//产生起始地址并发送器件地址,读操作
	i2c_wr_1byte(dev_id | 0x1, OC_I2C_STA | OC_I2C_WR);

	//返回读取到的一个字节数据
	return i2c_rd_1byte(OC_I2C_STO | OC_I2C_RD | OC_I2C_ACK);
}

int main(int argc, char ** argv) {

	int fd;
	int virtual_base = 0;	//虚拟基地址

	unsigned short  VER;	//摄像头VID
	unsigned char  PID;		//摄像头PID
	unsigned short CMOS_ID;//摄像头型号

	//完成fpga侧外设虚拟地址映射
	fd = fpga_init(&virtual_base);
	i2c_init(200000);

	VER = i2c_rd_reg(0x78, 0x300a, 1);//读取OV5640的0x300a寄存器的值
	PID = i2c_rd_reg(0x78, 0x300b, 1);//读取OV5640的0x300b寄存器的值
	CMOS_ID = (VER<<8)|PID;	//得到摄像头型号
	printf("CMOS MODE is %x\n",CMOS_ID);	//打印读取到的内容
	if(CMOS_ID == 0x5640)
	{
		i2c_wr_reg(0x78, 0x3622, 1, 1);	//写OV5640的0x3622寄存器的值为1
		unsigned char reg;	//定义数据临时变量
		reg = i2c_rd_reg(0x78, 0x3622, 1);//读取OV5640的0x3622寄存器的值
		printf("reg is %x\n",reg);	//打印读取到的内容
	}

	//程序退出前,取消虚拟地址映射
	if (munmap(virtual_base, HW_REGS_SPAN) != 0) {
		printf("ERROR: munmap() failed...\n");
		close(fd);
		return (1);
	}

	close(fd); //关闭MPU
	return 0;
}

在这里插入图片描述

可以看到,程序正确的读取到了摄像头的 ID,并从 0x3622 寄存器中读取到了写入的 1.因此基本的 CMOS 读写函数就编写完成了。

四、总结

本节针对开源的第三方 I2C IP 核进行了寄存器映射讲解,并编写了基于虚拟地址映射的读写函数,最后使用一个具体的 CMOS 图像传感器 OV5640 作为例子,进行了基本的板级验证,经过验证,证明该驱动确实能够正确的读写OV5640 摄像头的寄存器。

五、虚拟地址映射小结

本章节首先介绍了什么是 Linux 系统下的虚拟地址映射, 然后给出了 Linux应用程序中实现虚拟地址映射的基本方法。 以三个具体的 IP 实例为基础, 讲解了使用虚拟地址映射的方式进行读写的基本思路,最后都通过具体的应用实例验证了基于虚拟地址映射方式编写的驱动函数。 学习本章,不仅能够熟悉和掌握基于虚拟地址映射的硬件外设编程方式,还能熟悉各种外设 IP 寄存器结构和编程思路,希望读者能够基于这三个示例, 举一反三,掌握其他 IP 核的基于虚拟地址编程的方法。

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页