本帖最后由 LINUX课程 于 2024-9-11 11:06 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 本章将会讲述关于TTC定时器的相关内容,学习linux内核提供的定时器api函数,通过这些api函数可以完成很多要求的定时应用。 实验目的: - 掌握linux定时器的驱动的编写。
- 了解MPSOC的定时器功能特性。
- 了解TTC定时器的寄存器。
2 系统框图 在开始之前,我们先来看下PS端的系统定时器。PS端有很多不同类型的定时器和计数器。 Zynq的PS的CPU0和CPU1各包含1个私有定时器以及一个私有看门狗定时器并且共享一个全局定时器。另外Zynq的PS还有TTC0以及TTC1定制控制器。每个TTC定时器里面包含三个定时器/计数器。本文所讲解的是CPU0的私有定时器中断的使用。 本方案中只对CPU0的私有定时器进行说明。 Zynq的私有定时器和看门狗定时器具有以下共同的特性: 1-定时计数器位宽为32位,当达到0时产生中断 2-参考时钟为CPU时钟的1/2,比如CPU是666.6666M 那么定时器的参考时钟为333.3333M 3-有一个8bit预分频器,可以对参考时钟进一步分频 4-有一个可配置的预加载寄存器 5-可配置为单次模式或者自动重载模式 3 介绍 3.1 中断资源概述 PS的CPU0和CPU1各有1个私有定时器。上图中,红色部分为PS的私有中断资源,本文中demo就是以CPU0的定时器中断使用作为演示方案。CPU0的定时器中断经过中断控制派发器派发给CPU0。CPU0的私有定时器IRQ ID=29。 3.2 私有定时器的寄存器 在UG585中没有关于私有定时器的详细说明,我们通过SDK代码的理解补充了这部分说明。私有定时器的基地址为0xF8F00600。CPU的私有定时器一共有4个寄存器: XSCUTIMER_LOAD寄存器XSCUTIMER_LOAD_OFFSET (0x00U) Field Name | Bits | Type | Reset Value | Description | | | | | |
XSCUTIMER_COUNTER寄存器XSCUTIMER_COUNTER_OFFSET (0x04U) Field Name | Bits | Type | Reset Value | Description | | | | | |
XSCUTIMER_CONTROL寄存器XSCUTIMER_CONTROL_OFFSET (0x08U) Field Name | Bits | Type | Reset Value | Description | | | | | 私有定时器的控制寄存器 15~8bit:预分频设置 2bit:当为1中断使能 1bit:当为1自动重载使能 0bit:当为1表示定时器使能 |
XSCUTIMER_ISR寄存器XSCUTIMER_ISR_OFFSET (0x0CU) Field Name | Bits | Type | Reset Value | Description | | | | | 私有定时器的中断寄存器 0bit:当为1的时候表示发生中断 |
3.3 节拍与延时时间的互换 系统之所以能够记录时间,是因为每个系统都有一个节拍器。在已经部署好开发环境的虚拟机的uisrc-lab-xlnx目录中打开终端输入“source scripts/mzuxcfg.sh”之后,再输入“make_kernel_menuconfig.sh”。可以调整内核中的节拍频率。 在这里我们设置了250HZ的频率,也就是1秒250个节拍,节拍约高精度约高,但是同样过高的频率也会导致过高的CPU占用,因此除非是需要高精度,100HZ或250HZ已经足矣。 4 搭建工程 4.1 vivado工程的搭建 详细的vivado搭建参考“[米联客-XILINX-H3_CZ08_7100] LINUX驱动篇连载-01 Hello World”vivado和vitis工程搭建。如果使用的是附件中的工程生成的系统文件,或者使用的是附件中提供的相关的boot文件夹中的文件。那么,就无需改动vivado工程的设置。 4.2 上传文件至虚拟机 将文件“Timer”上传至虚拟机,可以使用U盘,scp,vm-tools等一系列方式实现。 至此准备工作,已经完成。 5 程序分析 5.1 驱动程序分析 - //添加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/ide.h>
- #include <linux/cdev.h>
- #include <linux/timer.h>
- //设置一个设备全局变量
- struct timer_device
- {
- dev_t devno;
- struct cdev cdev;
- struct class *class;
- struct device *device;
- unsigned long count;
- struct timer_list timer;
- };
- struct timer_device timer_dev;
- //timer回调函数,倒计时结束后会运行回调函数
- static void timer_callback(struct timer_list *timer)
- {
- printk(KERN_CRIT "timer_callback.\n");
- }
- //打开设备函数
- static int timer_open(struct inode *inode, struct file *filp)
- {
- printk("timer_open success.\n");
- return 0;
- }
- //接收用户空间传来的数据
- static ssize_t timer_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
- {
- int flag = 0, i = 0; //flag用来判断是否读取成功,i为一个计数器
- unsigned long temp; //用来临时存放计算值
- timer_dev.count = 0;
- flag = copy_from_user(&temp, buf, count); //使用copy_from_user读取用户态发送过来的数据
- if (flag != 0)
- {
- printk("Kernel receive data failed!\n");
- return 1;
- }
- //将char*转为unsigned long
- for (i = 0; i < count; i++)
- {
- timer_dev.count *= 10;
- timer_dev.count += temp % 256 - 48;
- temp = temp >> 8;
- }
- printk(KERN_CRIT "wait time = %ld\n", timer_dev.count);
- //设置定时器的延时
- mod_timer(&timer_dev.timer, jiffies + msecs_to_jiffies(timer_dev.count));
- return 0;
- }
- //关闭设备函数
- static int timer_release(struct inode *inode, struct file *filp)
- {
- printk("timer_release success.\n");
- return 0;
- }
- static struct file_operations timer_fops = {
- .owner = THIS_MODULE,
- .open = timer_open,
- .write = timer_write,
- .release = timer_release,
- };
- //实现装载入口函数
- static __init int timer_drv_init(void)
- {
- int ret = 0;
- printk("timer_drv_init start.\n");
- //动态申请设备号
- ret = alloc_chrdev_region(&timer_dev.devno, 0, 1, "timer_device");
- if (ret < 0)
- {
- printk("alloc_chrdev_region fail!\n");
- return 0;
- }
- //设备初始化
- cdev_init(&timer_dev.cdev, &timer_fops);
- timer_dev.cdev.owner = THIS_MODULE;
- //申请创建设备类
- timer_dev.class = class_create(THIS_MODULE, "timer_class");
- if (IS_ERR(timer_dev.class))
- {
- printk("class_create fail!\n");
- return 0;
- }
- //建立设备节点
- timer_dev.device = device_create(timer_dev.class, NULL, timer_dev.devno, NULL, "timer_device");
- if (IS_ERR(timer_dev.device))
- {
- printk("device_create fail!\n");
- return 0;
- }
- //向系统注册一个字符设备
- cdev_add(&timer_dev.cdev, timer_dev.devno, 1);
- //初始化定时器
- timer_setup(&timer_dev.timer, timer_callback, 0);
- printk("timer_drv_init success.\n");
- return 0;
- }
- //实现卸载入口函数
- static __exit void timer_drv_exit(void)
- {
- printk("timer_drv_exit start.\n");
- //删除定时器
- del_timer_sync(&timer_dev.timer);
- //注销字符设备
- cdev_del(&timer_dev.cdev);
- //删除设备节点
- device_destroy(timer_dev.class, timer_dev.devno);
- //删除设备类
- class_destroy(timer_dev.class);
- //注销设备号
- unregister_chrdev_region(timer_dev.devno, 1);
- printk("timer_drv_exit success.\n");
- }
- //声明装载入口函数和卸载入口函数
- module_init(timer_drv_init);
- module_exit(timer_drv_exit);
- //添加GPL协议
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码行8~18,设置了一个设备的全局变量,里面包含了设备的所有属性。 行20~24,计时器的回调函数,用来处理定时器结束时触发的。 行33~55,驱动程序读取操作,先拷贝用户空间数据,然后转换类型,最后通过mod_timer函数设置定时器。 行63~68,驱动程序接口。 行70~112,驱动入口函数,完成的任务有申请设备号,设备初始化,创建设备类,建立设备节点,注册字符设备,初始化定时器。 行114~129,驱动出口函数,与入口函数中的步骤相反,一步步注销申请的资源。 1:timer_list结构体 - struct timer_list {
- struct hlist_node entry;
- unsigned long expires;
- void (*function)(struct timer_list *);
- u32 flags;
- #ifdef CONFIG_LOCKDEP
- struct lockdep_map lockdep_map;
- #endif
- };
复制代码含义:该结构体规定了一个定时器所需要的各个参数。 2:timer_setup函数 - #define timer_setup(timer, callback, flags)
复制代码含义:定时器初始化函数,内容包括了初始化定时器、绑定回调函数。初始化另外还有init_timer()函数和add_timer()函数,这一对函数组合适用和本函数的效果是一样的,所以我们使用timer_setup函数来代替繁琐的设置工作。 具体分析: - timer:9.3.1中的timer_list结构体
- callback:回调函数,在定时器倒计时结束时会自动调用
- flags:TIMER_*标志
- 返回值:无
3:del_timer函数 - int del_timer(struct timer_list * timer)
复制代码含义:该函数是与初始化对应的注销函数,这个函数会删除一个定时器,但是定时器可能运行在其他处理器上,所以在调用这个函数的时候需要确定这个定时器已经退出。 具体分析: - timer:初始化完成的timer_list结构体
- 返回值:0,定时器还没被激活。1,定时器已经激活。
4:del_timer_sync函数 - int del_timer(struct timer_list * timer)
复制代码含义:这是del_timer 函数的sync版本,区别就是在注销时,不需要手动去确认定时器是否已经退出,它会自动等待定时器退出后注销定时器,一般我们采用这个函数来编写我们的驱动。 具体分析: - timer:初始化完成的timer_list结构体
- 返回值:0,定时器还没被激活。1,定时器已经激活。
5:mod_timer函数 - int del_timer_sync(struct timer_list *timer)
复制代码含义:用来修改定时器的值,设置的同时还会启动定时器。 具体分析: - timer:设定好的timer_list结构体
- expires:超时时间
- 返回值:0,调用 mod_timer 函数前定时器未被激活。1,调用 mod_timer 函数前定时器已被激活。
6:节拍与时间互换的函数 - /*毫秒互换*/
- int jiffies_to_msecs(const unsigned long j)
- long msecs_to_jiffies(const unsigned int m)
- /*微秒互换*/
- int jiffies_to_usecs(const unsigned long j)
- long usecs_to_jiffies(const unsigned int u)
- /*纳秒互换*/
- u64 jiffies_to_nsecs(const unsigned long j)
- unsigned long nsecs_to_jiffies(u64 n)
复制代码含义:将节拍与毫秒,微秒,纳秒互换的相关函数。 5.2 应用程序分析 - #include <stdio.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdlib.h>
- #include <string.h>
- int main(int argc, char *argv[])
- {
- int fd, ret = 0;
- char *filename;
- char writebuf[10] = {0};
- filename = argv[1];
- fd = open(filename, O_RDWR); //打开设备
- if (fd < 0)
- {
- printf("Can't open file %s\n", filename);
- return -1;
- }
- memcpy(writebuf, argv[2], strlen(argv[2])); //将内容拷贝到缓冲区
- ret = write(fd, writebuf, strlen(argv[2])); //写数据
- if (ret < 0)
- {
- printf("Write file %s failed!\n", filename);
- }
- else
- {
- printf("Write file success!\n");
- }
- ret = close(fd); //关闭设备
- if (ret < 0)
- {
- printf("Can't close file %s\n", filename);
- return -1;
- }
- return 0;
- }
复制代码第24行将从终端输入的第二个参数复制进writebuff中。 第25行把writebuff中的内容写入到文件描述符fd中。 写入成功后关闭设备。 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 = ttc_drv
- APP = ttc_app
- all :
- #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
- rm -rf *.mod.c *.mod.o *.symvers *.order *.o
- 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 串口,否则会登录失败。 7.2 程序准备 将驱动与程序上传至开发板,查看文件是否传输成功。 在终端输入“ls”命令。确认当前所需文件,已经被上传到当前的文件夹内了。 然后在root 状态下 insmod 安装驱动。“lsmod”查看当前安装的驱动。 在管理员权限下,终端输入“chmod 777 ttc_app”,改变应用程序的权限。 通过以上操作可以确定驱动已经安装,并且应用程序可以运行。 7.3 实验结果 在终端输入“./ttc_app /dev/timer_device 3000”,三秒后定时器触发。 测试成功。 |