| 软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA 
 1 概述 在第十四章我们学习了I2C EEPROM的使用,在该章将会学习通过AXI-I2C来处理PL端eeprom的读写。         实验目的: 掌握PL端的设备树的编写。掌握vivado工程部分,axi-iic相关的总线搭建。了解AXI-I2C IP的概念。
2 系统框图 3 介绍 3.1 AXI-IIC IP介绍 AXI-IIC控制器的功能构架图如下: 3.2 特性 -符合行业标准I2C协议 -通过axis4 - lite接口访问 -支持I2C主或从模式 -支持多主机操作 -软件可选择ACK位 -仲裁丢失中断并自动从主模式切换从模式 -寻址地址识别中断并自动从主模式到从模式 -起始位和停止位的产生和侦测 -重复起始位的产生 -ACK的产生和侦测 -总线忙检测 -支持1MHZ 、400KHZ 、100KHZ 工作时钟 -7位或10位寻址 -寻址使能和禁止 - 16字节深度的FIFO -节流功能 -动态生成启动和停止 3.3 寄存器概述 3.4 硬件电路分析 本实验只测试EEPROM  4 工程搭建 该部分生成的文件已经分别保存在“BOOT”文件夹内,可以直接使用。如果想要自己生成,按照下面步骤。 4.1 vivado工程搭建 详细的vivado工程搭建参考第一章,本章的vivado工程并非默认的vivado工程,请选择在该章目录中的vivado工程。 打开“soc_prj”文件夹。 进入vivado工程打开IP,查看IP是否连接正确。 4.2 设备树文件 打开vitis,导入之前生成的xsa文件,生成设备树。详细过程参考第一章vivado工程搭建部分。 将pl.dsti部分的设备树文件拷贝进“附件”文件夹“soc_dts”中的dts文件中。或者可以直接使用“22.axi_iic”文件夹中有已经完成了的设备树文件。 4.3 设置内核配置 由于我们自己写的驱动程序会和已有驱动冲突,因此我们需要先将kernel内的驱动去掉。 首先,使用source mz7xcfg.sh添加环境变量,然后使用make_kernel_menuconfig.sh来设置内核。 如上图所示,依次找到Device Drivers - Misc devices - EEPROM support,取消如图所示的选项,保存并退出。运行save_kernel_defconfig.sh保存内核配置文件。 依次执行make_kernel.sh编译内核,create_image.sh打包uboot文件。 使用/uisrc-lab-xlnx/boards/mzux/ubuntu/images/boot下的三个文件,替换sd卡BOOT分区中的三个文件。 4.4 制作系统文件 详细的制作系统文件参考第一章环境搭建部分,将从vivado和vitis中生成的文件复制进开发环境,使用脚本生成系统文件。也可以直接使用本章示例文件夹“3.boot”中的文件,按照第一章的步骤复制进开发环境进行系统制作,这里只做简单的介绍。(“4.BOOT”文件夹中有已经生成好的系统文件镜像文件,可以直接拷贝至系统SD卡的BOOT文件夹内) 5 程序分析 程序分析部分,主要研究下设备树文件,驱动程序和应用程序与第十四章的程序并无不同,相关具体函数可以参考之前的解释。 5.1 设备树分析 复制代码/dts-v1/;
/include/ "zynq-7000.dtsi"
/ {
 model = "Zynq MZ7X Development Board";
 compatible = "xlnx,zynq-MZ7X", "xlnx,zynq-7000";
 aliases {
  ethernet0 = &gem0;
  serial0 = &uart1;
  spi0 = &qspi;
  i2c0 = &axi_iic_0;
  mmc0 = &sdhci0;
  mmc1 = &sdhci1;
 };
 memory@0 {
  device_type = "memory";
  reg = <0x0 0x40000000>;
 };
 chosen {
  bootargs = ""; 
  stdout-path = "serial0:115200n8";
 };
 usb_phy0: phy0@e0002000 {
  compatible = "ulpi-phy";
  #phy-cells = <0>;
  reg = <0xe0002000 0x1000>;
  view-port = <0x0170>;
  drv-vbus;
 };
};
/ {
 amba_pl: amba_pl {
  #address-cells = <1>;
  #size-cells = <1>;
  compatible = "simple-bus";
  ranges ;
  axi_iic_0: i2c@41600000 {
   #address-cells = <1>;
   #size-cells = <0>;
   clock-names = "s_axi_aclk";
   clocks = <&clkc 15>;
   compatible = "xlnx,axi-iic-2.1", "xlnx,xps-iic-2.00.a";
   interrupt-names = "iic2intc_irpt";
   interrupt-parent = <&intc>;
   interrupts = <0 29 4>;
   reg = <0x41600000 0x10000>;
  };
 };
};
&axi_iic_0 {
 status = "okay";
 clock-frequency = <400000>;
 // scl-gpios = <&gpio 38 GPIO_ACTIVE_HIGH>;
 // sda-gpios = <&gpio 39 GPIO_ACTIVE_HIGH>;
 // rtc@68 {
 //  compatible = "dallas,ds1337";
 //  reg = <0x68>;
 // };
 eeprom@50 {
  compatible = "atmel,24c02";
  /* compatible = "at24,24c02"; */
  reg = <0x50>;
 };
};
&gem0 {
 status = "okay";
 phy-mode = "rgmii-id";
 xlnx,ptp-enet-clock = <0x69f6bcb>;
 phy-handle = <ðernet_phy>;
 ethernet_phy: ethernet-phy@0 {
  reg = <0>;
  device_type = "ethernet-phy";
 };
};
&gpio0 {
 emio-gpio-width = <64>;
 gpio-mask-high = <0x0>;
 gpio-mask-low = <0x5600>;
};
&intc {
 num_cpus = <2>;
 num_interrupts = <96>;
};
&qspi {
 u-boot,dm-pre-reloc;
 status = "okay";
 is-dual = <0>;
 num-cs = <1>;
 flash@0 {
  compatible = "n25q128a11";
  reg = <0x0>;
  spi-tx-bus-width = <4>;
  spi-rx-bus-width = <4>;
  spi-max-frequency = <125000000>;
 };
};
&sdhci0 {
 u-boot,dm-pre-reloc;
 status = "okay";
 xlnx,has-cd = <0x1>;
 xlnx,has-power = <0x0>;
 xlnx,has-wp = <0x0>;
};
&sdhci1 {
 status = "okay";
 xlnx,has-cd = <0x1>;
 xlnx,has-power = <0x0>;
 xlnx,has-wp = <0x1>;
};
&uart1 {
 u-boot,dm-pre-reloc;
 status = "okay";
};
&usb0 {
 status = "okay";
 dr_mode = "host";
 usb-phy = <&usb_phy0>;
 usb-reset = <&gpio0 46 0>;
};
&clkc {
 fclk-enable = <0x7>;
 ps-clk-frequency = <33333333>;
};
 具体的设备树如何查看,请参考第六章基于设备树的平台驱动。 第22行,将“axi_iic_0”设置一个别名“i2c1”。 第48行,vitis生成的关于PL端子卡的相关i2c配置。 和68行,为“axi_iic_0”添加“eeprom”设备,方便自己编写的驱动获取相关设置。 第174行,将“i2c0”中的“eeprom”设备注释掉,避免与子卡的eeprom冲突。 5.2 驱动程序分析 复制代码//添加头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <linux/i2c.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#define EEPROM_MAJOR 400
#define EEPROM_MINOR 0
static struct class *EEPROM_cls;
//设计一个全局数据对象
struct eeprom_24c02r_device
{
 struct cdev cdev;
 struct i2c_client *client; //记录匹配成功后的i2c client
};
struct eeprom_24c02r_device *eeprom_dev;
int eeprom_i2c_send(struct i2c_client *client, char *buf, int count)
{
 int ret = 0;
 struct i2c_adapter *adapter = client->adapter;
 struct i2c_msg msg;
 msg.addr = client->addr; //消息包是发送给哪个从设备
 msg.buf = buf;    //传送的数据
 msg.flags = 0;    //是发送数据还是接收数据
 msg.len = count;   //数据的个数
 //参数1---i2c控制器对象---来自于client
 //参数2---统一的数据包
 //参数3---数据包的个数
 //返回值已经正确传输数据的个数
 ret = i2c_transfer(adapter, &msg, 1);
 return 0;
}
int eeprom_i2c_recv(struct i2c_client *client, char *buf, int count)
{
 int ret = 0;
 struct i2c_adapter *adapter = client->adapter;
 struct i2c_msg msg;
 msg.addr = client->addr; //消息包是发送给哪个从设备
 msg.buf = buf;    //传送的数据
 msg.flags = 1;    //是发送数据还是接收数据
 msg.len = count;   //数据的个数
 //参数1---i2c控制器对象---来自于client
 //参数2---统一的数据包
 //参数3---数据包的个数
 //返回值已经正确传输数据的个数
 ret = i2c_transfer(adapter, &msg, 1);
 return 0;
}
ssize_t eeprom_24c02r_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
 int ret = 0;
 char *temp_buf = kzalloc(count, GFP_KERNEL);
 //先从从设备读取数据
 ret = eeprom_i2c_recv(eeprom_dev->client, temp_buf, count);
 printk("Successful packet acquisition ret = %d\n", ret);
 if (ret < 0)
 {
  printk("eeprom i2c recv error!\n");
  return -1;
 }
 //将数据传输到应用层
 ret = copy_to_user(buf, temp_buf, count);
 if (ret > 0)
 {
  printk("copy to user error!\n");
  return -EFAULT;
 }
 kfree(temp_buf);
 return count;
}
ssize_t eeprom_24c02r_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
 int ret = 0;
 char *temp_buf = kzalloc(count, GFP_KERNEL);
 //先从应用层获取数据
 ret = copy_from_user(temp_buf, buf, count);
 if (ret > 0)
 {
  printk("copy from user error!\n");
  return -EFAULT;
 }
 //将数据发送到底层
 ret = eeprom_i2c_send(eeprom_dev->client, temp_buf, count);
 printk("Successful packet acquisition ret = %d\n", ret);
 if (ret < 0)
 {
  printk("eeprom i2c send error!\n");
  return -1;
 }
 kfree(temp_buf);
 return count;
}
const struct file_operations eeprom_fops = {
 .owner = THIS_MODULE,
 .read = eeprom_24c02r_read,
 .write = eeprom_24c02r_write,
};
//驱动匹配到硬件后自动进入probe函数
int eeprom_24c02r_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
 int ret = 0;
 //通过手动分配,组合主次设备号
 dev_t devno = MKDEV(EEPROM_MAJOR, EEPROM_MINOR);
 //给全局数据分配空间
 eeprom_dev = kzalloc(sizeof(*eeprom_dev), GFP_KERNEL);
 if (eeprom_dev == NULL)
 {
  printk("error,Failed to allocate eeprom_dev space!\n");
  return -1;
 }
 //记录i2c匹配成功的client
 eeprom_dev->client = client;
 //注册字符设备
 //参数一:注册的主设备
 //参数二:注册的设备共有多少次设备号
 //参数三:设备名字,在/proc/devices显示
 ret = register_chrdev_region(devno, 1, "eeprom_24c02r");
 if (ret < 0)
 {
  printk("register chrdev region error!\n");
  return 0;
 }
 //初始化字符设备
 cdev_init(&eeprom_dev->cdev, &eeprom_fops);
 eeprom_dev->cdev.owner = THIS_MODULE;
 //创建类
 EEPROM_cls = class_create(THIS_MODULE, "EEPROM_class");
 //创建设备
 device_create(EEPROM_cls, NULL, devno, NULL, "EEPROM_device%d", 0);
 //向系统注册一个设备
 cdev_add(&eeprom_dev->cdev, devno, 1);
 if (ret < 0)
 {
  printk("cdev add faile!\n");
  return -1;
 }
 printk("Test Success!\n");
 return 0;
}
int eeprom_24c02r_remove(struct i2c_client *clinet)
{
 dev_t devno = MKDEV(EEPROM_MAJOR, EEPROM_MINOR);
 cdev_del(&eeprom_dev->cdev);
 //删除设备
 device_destroy(EEPROM_cls, devno);
 //删除类
 class_destroy(EEPROM_cls);
 unregister_chrdev_region(devno, 1);
 kfree(eeprom_dev);
 return 0;
}
//用于驱动从设备树中匹配相应的硬件
const struct of_device_id of_eeprom_24c02r[] = {
 {.compatible = "atmel,24c02"},
};
//构建i2c driver
struct i2c_driver eeprom_24c02r_drv = {
 .driver = {
  .owner = THIS_MODULE,
  .name = "eeprom_24c02r",       //体现在/sys/bus/i2c/driver/eeprom_24c02r_drv
  .of_match_table = of_match_ptr(of_eeprom_24c02r) //从设备树中根据compatible获取相应的硬件信息
 },
 .probe = eeprom_24c02r_probe,
 .remove = eeprom_24c02r_remove,
};
//实现装载入口函数
static __init int eeprom_24c02r_drv_init(void)
{
 //构建i2c设备,并注册到i2c总线上
 return i2c_add_driver(&eeprom_24c02r_drv);
}
//实现卸载入口函数
static __exit void eeprom_24c02r_drv_exit(void)
{
 i2c_del_driver(&eeprom_24c02r_drv);
}
//声明装载入口函数和卸载入口函数
module_init(eeprom_24c02r_drv_init);
module_exit(eeprom_24c02r_drv_exit);
//添加GPL协议
MODULE_LICENSE("GPL");
MODULE_AUTHOR("msxbo");
 5.3 应用程序分析 复制代码#include <linux/i2c-dev.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
//当执行app文件时没有带参数产生提示
void print_usage(char *str)
{
 printf("%s r     : read at24c02 addresss 0x100\n", str);
 printf("%s w val : write at24c02 addresss 0x100\n", str);
}
int main(int argc, char *argv[])
{
 int i;
 int fd;
 unsigned char val; //字节
 unsigned int register_addr = 0x100; //片内地址
 //存储读写数据的数组
 char wbuf[9] = {0};
 char rbuf[8] = {0};
 //判读执行文件时是否带有一个以上的参数,没有则提示
 if (argc < 2)
 {
  print_usage(argv[0]);
  exit(1);
 }
 /*打开设备文件*/
 fd = open("/dev/EEPROM_device0", O_RDWR);
 if (fd < 0)
 {
  perror("open failed");
  exit(1);
 }
 if (strcmp(argv[1], "r") == 0)
 {
  //确定读哪个位置,读操作时先写片内地址
  if (write(fd, ®ister_addr, 1) < 1)
  {
   perror("write failed");
   exit(1);
  }
  //将EEPROM中的数据读到rbuf中
  if (read(fd, rbuf, 8) < 0)
  {
   perror("read failed");
   exit(1);
  }
  else
  {
   for (i = 0; i < 8; i++)
   {
    printf("rbuf[%d] = 0x%c ", i, rbuf[i]);
    if (i == 3)
     printf("\n");
   }
   printf("\n");
  }
 }
 else if ((strcmp(argv[1], "w") == 0) && (argc == 3))
 {
  char num[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
  int x = strtoul(argv[2], NULL, 0);
  if (x < 0 || x > 15)
  {
   perror("write overflow");
   close(fd);
   exit(1);
  }
  else
   val = num[x];
  //先写片内地址
  wbuf[0] = register_addr; // 片内地址0x08
  wbuf[1] = val;
  printf("val=%d\n", wbuf[1]);
  for (i = 2; i < 9; i++)
   wbuf[i] = '9';
  //连续向fd文件中写入8个8字符
  if (write(fd, wbuf, 9) < 0)
  {
   perror("write failed");
   close(fd);
   exit(1);
  }
  printf("write data ok!\n");
 }
 close(fd);
 return 0;
}
 6 程序编译 详细的程序编译部分可以参考第一章程序编译部分。将编译好的文件上传至开发板。 6.1 Makefile文件分析 复制代码#已经编译过的内核源码路径
KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
#当前路径
CURRENT_DIR = $(shell pwd)
MODULE =  eeprom_24c02r_drv
APP = eeprom_24c02r_app
all : 
#进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
 make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
 rm -rf *.mod.c *.mod.o *.o *.symvers *.order 
ifneq ($(APP), )
 $(CROSS_COMPILE)gcc $(APP).c -o $(APP)
endif
clean : 
 make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
 rm -rf $(APP) *.tmp
#指定编译哪个文件
obj-m += $(MODULE).o
 第10行,和第11行需要根据自己编写的驱动文件名进行修改,其他的部分与之前的一致。 7 演示 7.1 硬件准备 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,“[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-04 从vitis移植Ubuntu实现二次开发”) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 接入12V直流电源开机。 7.2 程序准备 将驱动与程序上传至开发板,查看文件是否传输成功。 在终端输入“ls”命令。确认当前所需文件,已经被上传到当前的文件夹内了。 在管理员权限下,终端输入“chmod +x eeprom_24c02r”,改变应用程序的权限。 然后在root 状态下 insmod 安装驱动。“lsmod”查看当前安装的驱动。 通过以上操作可以确定应用程序,与驱动均已准备完成。 7.3 实验结果 在终端输入“./eeprom_24c02r_app r”,输出如下。可以看到,该应用程序读取了eeprom中前8个字节的内容。 再在终端输入“./eeprom_24c02r_app w 0xA”,输出如下。可以看到,读取的第一个字节的位置的0x1,已经被写入的修改了,变为了0xA。 测试成功。 |