zl程序教程

您现在的位置是:首页 >  系统

当前栏目

【SoC FPGA学习】十四、基于 Linux 标准文件 I/O 的设备读写

Linux设备学习 基于 读写 FPGA 十四 SoC
2023-09-11 14:20:36 时间

一、什么是文件 I/O

Linux 下的输入输出(I/O),设计成“一切皆文件”,把各种各样的输入输出(I/O)当成文件来操作,统一用文件 I/O 函数的形式,提供给应用程序调用。

Linux 下的文件概念不仅仅是我们日常所理解的文件例如 txt 文本、 sh 脚本, Linux 系统下一个目录,一个设备也会被当做文件。尤其是字符设备, 对一个设备的操作, 就像是操作实际的文件一样方便。对于文件,可以执行打开(open)、 读取(read)、 写入(write)、 关闭(close) 等操作,而对于一个设备,也可以使用相同的方式进行操作。基于文件 I/O 的方式,用户在编写应用程序时,无需考虑底层硬件的差异,只需要使用系统提供的标准文件 I/O 操作函数进行读写即可, 从而保证用户编写的应用程序能够在不做修改或做较少修改的情况下的适配到其他硬件平台上。 本节重点是讲解如何使用文件 I/O 的方式来使用 SoC FPGA 开发板上的外设, 不对文件 I/O 的具体底层实现做过多的介绍。

二、基于文件IO的一般操作方法

使用文件 I/O 操作一个设备文件, 基本的编程流程是打开、操作(读、写、ioctl)、 关闭的顺序。

2.1、文件描述符

文件描述符 fd( file descriptor)是进程中代表某个文件的整数, 有的文献资料中又称它为文件句柄( file handle)。

有效的文件描述符取值范围从 0 开始,直到系统定义的某个界限值。这些指定范围的整数,实际上是进程文件描述符表的索引。文件描述符表是进程用来保存它所打开的文件信息的、 由操作系统维护的一个登记表,用户程序不能直接访问该表。

通俗一点解释, 文件描述符的作用,类似在生活中排队取的号牌,业务员(进程)通过叫号(引用文件描述符)就能找到办事的人(打开的文件)。

2.2、打开设备(open)

在 Linux 中,使用 open 函数打开一个文件,打开成功会返回一个大于等于0 的数值,也就是文件描述符。 在打开文件过程中,系统会将该文件的信息生成一个描述符内容,并存放到描述符索引表中,而该文件描述符就是文件信息在描述符索引表中的编号。 例如, 使用 open 函数打开 FPGA 侧添加的 I2C 控制器代码如下所示:

int fd; //定义文件描述符
fd = open("/dev/i2c-2", O_RDWR); //打开 I2C-2 总线设备
if (fd < 0) { //如果描述符小于 0,则证明打开失败
	printf("open %s failed\n", "/dev/i2c-2");
	return -1;

注意,打开设备文件时候一定要检查函数的返回值,因为有效的设备描述符是从 0 开始的直到到系统定义的某个界限值, 因此如果 open 函数的返回值小于 0, 则表明设备文件打开失败。

2.3、向设备写入数据(write)

把数据写入文件,可调用 write()函数实现, write()的函数原型在<unistd.h中定义:

ssize_t write(int fd, const void *buf, size_t count);

操作成功,返回实际写入的字节数,出错则返回-1, 同时设置全局变量 errno 报告具体错误的原因,比如 errno=ENOSPC 表示磁盘满了。

参数 fd 是打开文件的描述符, buf 是数据缓冲区, 存放着准备写入文件的数据, count 是请求写入的字节数。实际写入的字节数可以小于请求写的字节数。 例如, 使用 write 函数向已经打开的 FPGA 侧添加的 I2C 控制器中写入 8个字节数据的代码如下所示:

for (i = 0; i < 8; i++) //初始化要写入的数据: 0、 1„„7
	tx_buf[i] = i;
	len = write(fd, tx_buf, 8); //向 I2C 设备写入 8 个字节数据
	if (len < 0) {
		printf("write data faile \n");
		return -1;
}

写完之后,需要检查返回值是否小于 0, 如果小于 0 则表明写入失败。

2.4、读取设备数据(read)

从打开的文件读取数据,可调用 read()函数实现。 read()函数原型在<unistd.h>中定义:

ssize_t read(int fd, void *buf, size_t count);

操作成功,返回实际读取的字节数,如果已到达文件结尾,返回 0,否则返回-1 表示出错,同时设置全局变量 errno 报告具体错误的原因。实际读取的字节数,可以小于请求的字节数 count,比如下面两种情况:文件长度小于请求的长度,即还没达到请求的字节数时,就已到达文件结尾。如果文件是 50 字节长,而 read 请求读 100 字节( count=100),则首次调用 read 时,它返回 50,紧接着的下次调用,它返回 0,表示已到达文件结尾;

读设备文件时,有些设备每次返回的数据长度小于请求的字节数,如终端设备一般按行返回,即每读到一行数据就返回。

参数 fd 是调用 open()时返回的文件描述符, buf 是用来接收所读数据的缓冲区, count 是请求读取的字节数。

例如, 使用 read 函数从已经打开 FPGA 侧添加的 I2C 控制器中读取 8 个字节数据的代码如下所示:

char rx_buf[8]; /* 用于储存接收数据 */
len = read(fd, rx_buf, 8); /* 在设置的数据地址连续读入数据 */
if (len < 0) {
	printf("read data faile \n");
	return -1;

注意, 读取数据完成后,需要检查实际读取到的数据大小,即 read 函数的返回值, 如果小于 0 则表明读取失败。

2.5、杂项操作(ioctl)

文件 I/O 操作还有很多不好归到 read()/write()的,只好放到这个函数中。尤其是设备文件, 比如修改设备寄存器的值等, read()/write()函数会比较的麻烦,因此通过 ioctl()函数提供设备特有的操作。 ioctl()是文件 I/O 的杂项函数,其函数原型在<sys/ioctl.h>中定义:

int ioctl(int fd, int cmd, …);

一般情况下, 操作成功返回 0,失败返回-1, 由 errno 报告具体错误原因。但有的设备文件可能会返回一个正值表示输出参数,其含义取决于具体的设备文件。

参数 fd 是打开文件的描述符,参数 cmd 是文件的操作命令,这个参数的取值还决定后面的参数含义,“„”表示从参数是可选的、类型不确定的。ioctl()的 cmd 操作命令是文件专有的,不同的文件, cmd 往往是不同的,没有共用性,比如嵌入式系统中的设备文件, SPI 和 I2C 所支持的 ioctl()操作命令就不同。对于 SPI 设备,支持使用 ioctl()来设置 SPI 的工作模式。而I2C 则不支持该命令。 同时, I2C 设备支持通过 ioctl()指定 I2C 总线上的从设备地址,而 SPI 则没有此命令。 以下为使用 ioctl()函数指定 SPI 工作模式和使用ioctl()函数设定 I2C 总线上从设备地址的代码:

//设置 SPI 工作在模式 0
ret = ioctl(fd_spi, SPI_IOC_WR_MODE, 0);

//设置 I2C 从设备地址为 0x78
ret = ioctl(fd_i2c, I2C_SLAVE, 0x78 >> 1);

ioctl()函数在设备文件中应用非常的广泛,很多情况下,使用 ioctl()函数来操作硬件设备, 会比直接使用 write()或 read()函数拥有更高的效率。

2.6、关闭设备

文件 I/O 操作完成后,应该调用 close()关闭打开的文件,释放打开文件时所占用的系统资源。 close()函数原型在<unistd.h>文件中定义:

int close(int fd);

如果文件顺利关闭,返回 0,否则返回-1, 同时设置全局变量 errno 报告具体错误的原因。参数 fd 是打开文件时调用 open()或 creat()函数返回的文件描述符。

2.7、其他操作

关于文件 I/0,还有其他的一些常用函数,例如 fsync()fseek()。 在设备文件中,由于这两个函数不如前面讲的 5 个用的普遍,因此这里不做介绍。但仍然需要说明的是,这两个函数虽然针对设备文件使用的不是很多,但是在普通文件的应用中,却有重要的功能价值。

三、使用文件 IO 实现 I2C 编程

使用文件 I/O 来编程使用 I2C 控制器, 就比使用基于虚拟地址映射的方式要简单方便许多了。不仅如此,由于基于文件 I/O 的操作使用的是 Linux 内核驱动的方式来获取 I2C 控制器中的寄存器数据的,而在 Linux 内核中, 可以很方便的注册中断,以使用中断的方式来完成数据的收发,因此能够提高程序的运行效率

在基于虚拟地址映射的 Linux 硬件编程中, 我们介绍了如何自己编写驱动程序,使用 I2C 控制器映射到 linux 用户空间的虚拟地址来完成数据的传输。 本节实验将使用文件 I/O 的方式,实现通过 I2C 控制器完成 EEPROM 存储器读写的功能。

对于 I2C 总线上的每一个设备,都有一个器件地址,当访问该设备时必须先指定其器件地址, I2C 设备驱动提供了一个 ioctl 命令用来指定需要操作的设备地址, 该命令的 16 进制值为 0x0703,使用时,仅需在 ioctl 函数中传入该命令即可设置从设备地址, 代码如下所示:

#define I2C_SLAVE 0x0703
#define I2C_ADDR 0xA2
ret = ioctl(fd, I2C_SLAVE, I2C_ADDR >> 1); /* 设置从机地址 */

学习代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>

#define I2C_SLAVE 0x0703
#define I2C_TENBIT 0x0704
#define I2C_ADDR 0xA0
#define DATA_LEN 8
#define I2C_DEV_NAME "/dev/i2c-2"

int main(int arg, char*args[]) {

	int ret, len;
	int i, flag = 0;
	int fd;
	char tx_buf[DATA_LEN + 1]; /* 用于储存数据地址和发送数据 */
	char rx_buf[DATA_LEN]; /* 用于储存接收数据 */
	char addr[1]; /* 用于储存读/写的数据地址 */
	
	addr[0] = 0; /* 数据地址设置为 0 */
	fd = open(I2C_DEV_NAME, O_RDWR); /* 打开 I2C 总线设备 */
	if (fd < 0) {
		printf("open %s failed\n", I2C_DEV_NAME);
		return -1;
	}
	ret = ioctl(fd, I2C_SLAVE, I2C_ADDR >> 1); /* 设置从机地址 */
	if (ret < 0) {
		printf("setenv address faile ret: %x \n", ret);
		return -1;
	}
	/* 由于没有设置从机地址长度,所以使用默认的地址长度为 8 */
	tx_buf[0] = addr[0]; /* 发数据时,第一个发送是数据地址 */
	for (i = 1; i < DATA_LEN; i++) /* 初始化要写入的数据: 0、 1„„7 */
		tx_buf[i] = i;
	len = write(fd, tx_buf, DATA_LEN + 1); /* 把数据写入到 AT24C02, */
	if (len < 0) {
		printf("write data faile \n");
		return -1;
	}
	usleep(1000 * 100); /* 需要延迟一段时间才能完成写入 EEPROM */
	len = write(fd, addr, 1); /* 设置数据地址 */
	if (len < 0) {
		printf("write data addr faile \n");
		return -1;
	}
	len = read(fd, rx_buf, DATA_LEN); /* 在设置的数据地址连续读入数据 */
	if (len < 0) {
		printf("read data faile \n");
		return -1;
	}
	printf("read from eeprom:");
	for (i = 0; i < DATA_LEN - 1; i++) { /* 对比写入数据和读取的数据 */
		printf(" %x", rx_buf[i]);
		if (rx_buf[i] != tx_buf[i + 1])
			flag = 1;
	}
	printf("\n");
	if (!flag) { /* 如果写入/读取数据一致,打印测试成功 */
		printf("eeprom write and read test sussecced!\r\n");
	} else { /* 如果写入/读取数据不一致,打印测试失败 */
		printf("eeprom write and read test failed!\r\n");
	}
	return 0;
}

四、小节

本章通过简单的例子,介绍了使用文件 I/O 的方式操作 FPGA 侧添加的见外设控制器的方法。 相较于使用虚拟地址映射的方式实现 FPGA 侧设备驱动,使用 Linux 系统支持的驱动程序来完成这些外设的控制不仅简化了程序写过程, 提高了程序可移植性,而且内核驱动方式实现的设备驱动,能够方便的实现中断的功能,从而提高控制器的运行效率。