超级版主
- 积分
- 4666
- 金钱
- 4666
- 注册时间
- 2019-5-8
- 在线时间
- 1224 小时
|
1)实验平台:正点原子STM32MP157开发板
2) 章节摘自【正点原子】《STM32MP157嵌入式Linux驱动开发指南》
3)购买链接:https://item.taobao.com/item.htm?&id=629270721801
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyzmp157.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32MP157技术交流群:691905614
四十二章 Linux RTC驱动实验
RTC也就是实时时钟,用于记录当前系统时间,对于Linux系统而言时间是非常重要的,就和我们使用Windows电脑或手机查看时间一样,我们在使用Linux设备的时候也需要查看时间。本章我们就来学习一下如何编写Linux下的RTC驱动程序。
42.1 Linux内核RTC驱动简介
RTC设备驱动是一个标准的字符设备驱动,应用程序通过open、release、read、write和ioctl等函数完成对RTC设备的操作,本章我们主要学习如何使用STM32MP1内部自带的RTC外设。
Linux内核将RTC设备抽象为rtc_device结构体,因此RTC设备驱动就是申请并初始化rtc_device,最后将rtc_device注册到Linux内核里面,这样Linux内核就有一个RTC设备的。至于RTC设备的操作肯定是用一个操作集合(结构体)来表示的,我们先来看一下rtc_device构体,此结构体定义在include/linux/rtc.h文件中,结构体内容如下(删除条件编译):
示例代码42.1.1 rtc_device结构体
- 100 struct rtc_device {
- 101 struct device dev; /* 设备 */
- 102 struct module *owner;
- 103
- 104 int id; /* ID */
- 105
- 106 const struct rtc_class_ops *ops; /* RTC设备底层操作函数 */
- 107 struct mutex ops_lock;
- 108
- 109 struct cdev char_dev; /* 字符设备 */
- 110 unsigned long flags;
- 111
- 112 unsigned long irq_data;
- 113 spinlock_t irq_lock;
- 114 wait_queue_head_t irq_queue;
- 115 struct fasync_struct *async_queue;
- 116
- 117 int irq_freq;
- 118 int max_user_freq;
- 119
- 120 struct timerqueue_head timerqueue;
- 121 struct rtc_timer aie_timer;
- 122 struct rtc_timer uie_rtctimer;
- 123 struct hrtimer pie_timer; /* sub second exp, so needs hrtimer */
- 124 int pie_enabled;
- 125 struct work_struct irqwork;
- 126 /* Some hardware can't support UIE mode */
- 127 int uie_unsupported;
- ......
- 159 };
复制代码
我们需要重点关注的是ops成员变量,这是一个rtc_class_ops类型的指针变量,rtc_class_ops为RTC设备的最底层操作函数集合,包括从RTC设备中读取时间、向RTC设备写入新的时间值等。因此,rtc_class_ops是需要用户根据所使用的RTC设备编写的,此结构体定义在include/linux/rtc.h文件中,内容如下:
示例代码42.1.2 rtc_class_ops 结构体
- 75 struct rtc_class_ops {
- 76 int (*ioctl)(struct device *, unsigned int, unsigned long);
- 77 int (*read_time)(struct device *, struct rtc_time *);
- 78 int (*set_time)(struct device *, struct rtc_time *);
- 79 int (*read_alarm)(struct device *, struct rtc_wkalrm *);
- 80 int (*set_alarm)(struct device *, struct rtc_wkalrm *);
- 81 int (*proc)(struct device *, struct seq_file *);
- 82 int (*alarm_irq_enable)(struct device *, unsigned int enabled);
- 83 int (*read_offset)(struct device *, long *offset);
- 84 int (*set_offset)(struct device *, long offset);
- 85 };
复制代码
看名字就知道rtc_class_ops操作集合中的这些函数是做什么的了,但是我们要注意,rtc_class_ops中的这些函数只是最底层的RTC设备操作函数,并不是提供给应用层的file_operations函数操作集。RTC是个字符设备,那么肯定有字符设备的file_operations函数操作集,Linux内核提供了一个RTC通用字符设备驱动文件,文件名为drivers/rtc/dev.c,dev.c文件提供了所有RTC设备共用的file_operations函数操作集,如下所示:
示例代码42.1.3 RTC通用 file_operations操作集
- 431 static const struct file_operations rtc_dev_fops = {
- 432 .owner = THIS_MODULE,
- 433 .llseek = no_llseek,
- 434 .read = rtc_dev_read,
- 435 .poll = rtc_dev_poll,
- 436 .unlocked_ioctl = rtc_dev_ioctl,
- 437 .open = rtc_dev_open,
- 438 .release = rtc_dev_release,
- 439 .fasync = rtc_dev_fasync,
- 440 };
复制代码
看到示例代码42.1.3是不是很熟悉了,标准的字符设备操作集。应用程序可以通过ioctl函数来设置/读取时间、设置/读取闹钟的操作,对应的rtc_dev_ioctl函数就会执行。 rtc_dev_ioctl最终会通过操作rtc_class_ops中的read_time、set_time等函数来对具体RTC设备的读写操作。我们简单来看一下rtc_dev_ioctl函数,函数内容如下(有省略):
示例代码42.1.4 rtc_dev_ioctl函数代码段
- 202 static long rtc_dev_ioctl(struct file *file,
- 203 unsigned int cmd, unsigned long arg)
- 204 {
- 205 int err = 0;
- 206 struct rtc_device *rtc = file->private_data;
- 207 const struct rtc_class_ops *ops = rtc->ops;
- 208 struct rtc_time tm;
- 209 struct rtc_wkalrm alarm;
- 210 void __user *uarg = (void __user *)arg;
- 211
- 212 err = mutex_lock_interruptible(&rtc->ops_lock);
- 213 if (err)
- 214 return err;
- ......
- 253 switch (cmd) {
- ......
- 317 case RTC_RD_TIME: /* 读取时间 */
- 318 mutex_unlock(&rtc->ops_lock);
- 319
- 320 err = rtc_read_time(rtc, &tm);
- 321 if (err < 0)
- 322 return err;
- 323
- 324 if (copy_to_user(uarg, &tm, sizeof(tm)))
- 325 err = -EFAULT;
- 326 return err;
- 327
- 328 case RTC_SET_TIME: /* 设置时间 */
- 329 mutex_unlock(&rtc->ops_lock);
- 330
- 331 if (copy_from_user(&tm, uarg, sizeof(tm)))
- 332 return -EFAULT;
- 333
- 334 return rtc_set_time(rtc, &tm);
- .....
- 385 default:
- 386 /* Finally try the driver's ioctl interface */
- 387 if (ops->ioctl) {
- 388 err = ops->ioctl(rtc->dev.parent, cmd, arg);
- 389 if (err == -ENOIOCTLCMD)
- 390 err = -ENOTTY;
- 391 } else {
- 392 err = -ENOTTY;
- 393 }
- 394 break;
- 395 }
- 396
- 397 done:
- 398 mutex_unlock(&rtc->ops_lock);
- 399 return err;
- 400 }
复制代码
第317行,RTC_RD_TIME为时间读取命令。
第320行,如果是读取时间命令的话就调用rtc_read_time函数获取当前RTC时钟, rtc_read_time会调用__rtc_read_time函数,__rtc_read_time函数内容如下:
示例代码42.1.5 __rtc_read_time函数代码段
- 84 static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
- 85 {
- 86 int err;
- 87
- 88 if (!rtc->ops) {
- 89 err = -ENODEV;
- 90 } else if (!rtc->ops->read_time) {
- 91 err = -EINVAL;
- 92 } else {
- 93 memset(tm, 0, sizeof(struct rtc_time));
- 94 err = rtc->ops->read_time(rtc->dev.parent, tm);
- 95 if (err < 0) {
- 96 dev_dbg(&rtc->dev, "read_time: fail to read: %d\n",
- 97 err);
- 98 return err;
- 99 }
- 100
- 101 rtc_add_offset(rtc, tm);
- 102
- 103 err = rtc_valid_tm(tm);
- 104 if (err < 0)
- 105 dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n");
- 106 }
- 107 return err;
- 108 }
复制代码
从第94行可以看出,__rtc_read_time函数会通过调用rtc_class_ops中的read_time成员变量来从RTC设备中获取当前时间。rtc_dev_ioctl函数对其他的命令处理都是类似的,比如RTC_ALM_READ命令会通过rtc_read_alarm函数获取到闹钟值,而rtc_read_alarm函数经过层层调用,最终会调用rtc_class_ops中的read_alarm函数来获取闹钟值。
至此,Linux内核中RTC驱动调用流程就很清晰了,如图42.1.1所示:
图42.1.1 Linux RTC驱动调用流程
当rtc_class_ops准备好以后需要将其注册到Linux内核中,这里我们可以使用rtc_device_register函数完成注册工作。此函数会申请一个rtc_device并且初始化这个rtc_device,最后向调用者返回这个rtc_device,此函数原型如下:
- struct rtc_device *rtc_device_register(const char *name,
- struct device *dev,
- const struct rtc_class_ops *ops,
- struct module *owner)
复制代码
函数参数和返回值含义如下:
name:设备名字。
dev:设备。
ops:RTC底层驱动函数集。
owner:驱动模块拥有者。
返回值:注册成功的话就返回rtc_device,错误的话会返回一个负值。
当卸载RTC驱动的时候需要调用rtc_device_unregister函数来注销注册的rtc_device,函数原型如下:
- void rtc_device_unregister(struct rtc_device *rtc)
复制代码
函数参数和返回值含义如下:
rtc:要删除的rtc_device。
返回值:无。
还有另外一对rtc_device注册函数devm_rtc_device_register和devm_rtc_device_unregister,分别为注册和注销rtc_device。
42.2 STM32MP1内部RTC驱动分析
先直接告诉大家,STM32MP1的RTC驱动我们不用自己编写,因为ST已经写好了。其实对于大多数的SOC来讲,内部RTC驱动都不需要我们去编写,半导体厂商会编写好。但是这不代表我们就偷懒了,虽然不用编写RTC驱动,但是我们得看一下这些原厂是怎么编写RTC驱动的。
分析驱动,先从设备树入手,打开stm32mp151.dtsi,在里面找到如下rtc设备节点,节点内容如下所示:
示例代码42.2.1 stm32mp151.dtsi文件rtc节点
- 1746 rtc: rtc@5c004000 {
- 1747 compatible = "st,stm32mp1-rtc";
- 1748 reg = <0x5c004000 0x400>;
- 1749 clocks = <&scmi0_clk CK_SCMI0_RTCAPB>,
- 1750 <&scmi0_clk CK_SCMI0_RTC>;
- 1751 clock-names = "pclk", "rtc_ck";
- 1752 interrupts-extended = <&exti 19 IRQ_TYPE_LEVEL_HIGH>;
- 1753 status = "disabled";
- 1754 };
复制代码
第1747行设置兼容属性compatible的值为“st,stm32mp1-rtc”,因此在Linux内核源码中搜索此字符串即可找到对应的驱动文件,此文件为drivers/rtc/rtc-stm32.c,在rtc-stm32.c文件中找到如下所示内容:
示例代码42.2.2 设备 platform驱动框架
- 719 static const struct of_device_id stm32_rtc_of_match[] = {
- 720 { .compatible = "st,stm32-rtc", .data = &stm32_rtc_data },
- 721 { .compatible = "st,stm32h7-rtc", .data = &stm32h7_rtc_data },
- 722 { .compatible = "st,stm32mp1-rtc", .data = &stm32mp1_data },
- 723 {}
- 724 };
- 725 MODULE_DEVICE_TABLE(of, stm32_rtc_of_match);
- 726
- ......
- 1020 static struct platform_driver stm32_rtc_driver = {
- 1021 .probe = stm32_rtc_probe,
- 1022 .remove = stm32_rtc_remove,
- 1023 .driver = {
- 1024 .name = DRIVER_NAME,
- 1025 .pm = &stm32_rtc_pm_ops,
- 1026 .of_match_table = stm32_rtc_of_match,
- 1027 },
- 1028 };
- 1029
- 1030 module_platform_driver(stm32_rtc_driver);
复制代码
第719~723行,设备树ID表。第722行,刚好有一个compatible属性和设备树的rtc的compatible属性值一样,所以rtc设备节点会和此驱动匹配。
第1020~1028行,标准的platform驱动框架,当设备和驱动匹配成功以后stm32_rtc_probe函数就会执行,我们来看一下stm32_rtc_probe函数,函数内容如下(有省略):
示例代码42.2.3 stm32_rtc_probe函数代码段
- 789 static int stm32_rtc_probe(struct platform_device *pdev)
- 790 {
- 791 struct stm32_rtc *rtc;
- 792 const struct stm32_rtc_registers *regs;
- 793 struct resource *res;
- 794 int ret;
- 795
- 796 rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL);
- 797 if (!rtc)
- 798 return -ENOMEM;
- 799
- 800 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
- 801 rtc->base = devm_ioremap_resource(&pdev->dev, res);
- ......
- 856 ret = clk_prepare_enable(rtc->rtc_ck);
- 857 if (ret)
- 858 goto err;
- ......
- 872 ret = stm32_rtc_init(pdev, rtc);
- 873 if (ret)
- 874 goto err;
- 875
- 876 rtc->irq_alarm = platform_get_irq(pdev, 0);
- 877 if (rtc->irq_alarm <= 0) {
- 878 ret = rtc->irq_alarm;
- 879 goto err;
- 880 }
- ......
- 892 rtc->rtc_dev = devm_rtc_device_register(&pdev->dev, pdev->name,
- 893 &stm32_rtc_ops, THIS_MODULE);
- 894 if (IS_ERR(rtc->rtc_dev)) {
- 895 ret = PTR_ERR(rtc->rtc_dev);
- 896 dev_err(&pdev->dev, "rtc device registration failed, err=%d\n",
- 897 ret);
- 898 goto err;
- 899 }
- 900
- 901 /* Handle RTC alarm interrupts */
- 902 ret = devm_request_threaded_irq(&pdev->dev, rtc->irq_alarm, NULL,
- 903 stm32_rtc_alarm_irq, IRQF_ONESHOT,
- 904 pdev->name, rtc);
- 905 if (ret) {
- 906 dev_err(&pdev->dev, "IRQ%d (alarm interrupt) already claimed\n",
- 907 rtc->irq_alarm);
- 908 goto err;
- 909 }
- ......
- 940
- 941 return 0;
- ......
- 954 }
复制代码
第796行,调用devm_kzalloc申请rtc大小的空间,返回申请空间的首地址。
第800行,调用 platform_get_resource 函数从设备树中获取到 RTC 外设寄存器基地址。
第801行,调用函数 devm_ioremap_resource 完成内存映射,得到 RTC 外设寄存器物理基地址对应的虚拟地址。
第856行,调用clk_prepare_enable函数使能时钟。
第872行,初始化STM32MP1 rtc的寄存器。
第876行,获取设备树的中断号。
第892行,调用 devm_rtc_device_register 函数向系统注册 rtc_devcie,RTC 底层驱动集为stm32_rtc_ops。stm32_rtc_ops操作集包含了读取/设置RTC时间,读取/设置闹钟等函数。
第902行,调用devm_request_threaded_irq函数请求RTC中断,中断服务函数为stm32_rtc_alarm_irq,用于RTC闹钟中断。
stm32_rtc_ops内容如下所示:
示例代码42.2.4 rtc_class_ops操作集
- 623 static const struct rtc_class_ops stm32_rtc_ops = {
- 624 .read_time = stm32_rtc_read_time,
- 625 .set_time = stm32_rtc_set_time,
- 626 .read_alarm = stm32_rtc_read_alarm,
- 627 .set_alarm = stm32_rtc_set_alarm,
- 628 .alarm_irq_enable = stm32_rtc_alarm_irq_enable,
- 629 };
复制代码
我们就以第624行的stm32_rtc_read_time函数为例讲解一下rtc_class_ops的各个RTC底层操作函数该如何去编写。stm32_rtc_read_time函数用于读取RTC时间值,此函数内容如下所示:
示例代码42.2.5 stm32_rtc_read_time代码段
- 364 static int stm32_rtc_read_time(struct device *dev,
- struct rtc_time *tm)
- 365 {
- 366 struct stm32_rtc *rtc = dev_get_drvdata(dev);
- 367 const struct stm32_rtc_registers *regs = &rtc->data->regs;
- 368 unsigned int tr, dr;
- 369
- 370 /* Time and Date in BCD format */
- 371 tr = readl_relaxed(rtc->base + regs->tr);
- 372 dr = readl_relaxed(rtc->base + regs->dr);
- 373
- 374 tm->tm_sec = (tr & STM32_RTC_TR_SEC) >> STM32_RTC_TR_SEC_SHIFT;
- 375 tm->tm_min = (tr & STM32_RTC_TR_MIN) >> STM32_RTC_TR_MIN_SHIFT;
- 376 tm->tm_hour = (tr & STM32_RTC_TR_HOUR) >> STM32_RTC_TR_HOUR_SHIFT;
- 377
- 378 tm->tm_mday = (dr & STM32_RTC_DR_DATE) >> STM32_RTC_DR_DATE_SHIFT;
- 379 tm->tm_mon = (dr & STM32_RTC_DR_MONTH) >> STM32_RTC_DR_MONTH_SHIFT;
- 380 tm->tm_year = (dr & STM32_RTC_DR_YEAR) >> STM32_RTC_DR_YEAR_SHIFT;
- 381 tm->tm_wday = (dr & STM32_RTC_DR_WDAY) >> STM32_RTC_DR_WDAY_SHIFT;
- 382
- 383 /* We don't report tm_yday and tm_isdst */
- 384
- 385 bcd2tm(tm);
- 386
- 387 return 0;
- 388 }
复制代码
第371~372行,调用readl_relaxed读取STM32MP1的RTC_TR和RTC_DR这两个寄存器的值,其中TR寄存器为RTC时间寄存器,保存着时、分、秒信息;DR为RTC的日期寄存器,保存着年、月、日信息。通过这两个寄存器我们就可以得到RTC时间
第374~381行,前两行获取到了TR和DR这两个寄存器的值,这里需要从这两个寄存器值中提取出具体的年、月、日和时、分、秒信息。
第385行,上面得到的时间信息为BCD格式的,这里通过bcd2tm函数将BCD格式转换为rtc_time格式, rtc_time结构体定义如下:
示例代码42.2.6 rtc_time结构体类型
- 20 struct rtc_time {
- 21 int tm_sec;
- 22 int tm_min;
- 23 int tm_hour;
- 24 int tm_mday;
- 25 int tm_mon;
- 26 int tm_year;
- 27 int tm_wday;
- 28 int tm_yday;
- 29 int tm_isdst;
- 30 };
复制代码
42.3 RTC时间查看与设置
42.3.1 使能内部RTC
在Linux内核移植的时候,设备树是经过精简的,就没有启动RTC功能。打开stm32mp157d-atk.dts文件,添加如下代码所示:
示例代码42.3.1.1 rtc节点信息
- 1 &rtc {
- 2 status = "okay";
- 3 };
复制代码
追加的RTC节点内容很简单,就是把status属性改为“okay”。接着我们重新编译设备树,然后使用新编译的stm32mp157d-atk.dtb文件启动开发板。
42.3.2 查看时间
RTC是用来记时的,因此最基本的就是查看时间,Linux内核启动的时候可以看到系统时钟设置信息,如图42.3.2.1所示:
图42.3.2.1 Linux启动log信息
从图42.3.2.1中可以看出,Linux内核在启动的时候将rtc设置为rtc0,大家的启动信息可能会和图42.3.2.1中不同,但是基本上都是一样的。
如果要查看时间的话输入“date”命令即可,结果如图42.3.2.2所示:
图42.3.2.2 当前时间值
从图42.3.2.2可以看出,当前时间为2000年1月1日 03:30:29,很明显时间不对,我们需要重新设置RTC时间。
RTC时间设置也是使用的date命令,输入“date --help”命令即可查看date命令如何设置系统时间,结果如图42.3.2.3所示:
图42.3.2.3 date命令帮助信息
比如现在设置当前时间为2021年5月2日 18:53:00,因此输入如下命令:
- date -s "2021-05-02 18:53:00"
复制代码
设置完成以后再次使用date命令查看一下当前时间就会发现时间改过来了,如图42.3.2.4所示:
图42.3.2.4 当前时间
大家注意我们使用“date -s”命令仅仅是修改了当前时间,此时间还没有写入到STM32MP1内部RTC里面或其他的RTC芯片里面,因此系统重启以后时间又会丢失。我们需要将当前的时间写入到RTC里面,这里要用到hwclock命令,输入如下命令将系统时间写入到RTC里面:
- hwclock -w //将当前系统时间写入到RTC里面
复制代码
时间写入到RTC里面以后就不怕系统重启以后时间丢失了,如果STM32MP1开发板底板接了纽扣电池,那么开发板即使断电了时间也不会丢失。大家可以尝试一下不断电重启和断电重启这两种情况下开发板时间会不会丢失。 |
|