驱动移植

供应商无法提供相应的驱动程序,不过在 linux 最新的内核倒是有一份 pcf85363 的驱动,看代码并核对寄存器功能,是可以兼容 pcf85263 芯片。只是我们用的内核比较老 linux 4.9,rtc 子系统的接口有些变化,不能直接拿来用。根据 Linux 4.9 现有的驱动程序,修改了 pcf85363 驱动,可以正常的使用。

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/rtc/rtc-pcf85363.c?h=v6.0

驱动框架

RTC 设备驱动

RTC 子系统的设备驱动还是非常简单的,只要按要求实现五个接口就能使用。

struct rtc_class_ops {
......
int (*read_time)(struct device *dev, struct rtc_time *tm);
int (*set_time)(struct device *dev, struct rtc_time *tm);
int (*read_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
int (*set_alarm)(struct device *dev, struct rtc_wkalrm *alrm);
int (*alarm_irq_enable)(struct device *dev, unsigned int enabled);
......
};

通过以下接口即可注册到系统内:
devm_rtc_device_register(&client->dev, client->name, &rtc_ops, THIS_MODULE);

rtc_class_ops->read_time(struct device *dev, struct rtc_time *tm)
1. 应用层通过 ioctl(fd, RTC_RD_TIME, &rtc_tm) 读取时间的回调接口。
2. 驱动层需要读取芯片里面的时间日期寄存器组,转换为十进制更新到 rtc_tm。 rtc_class_ops->set_time(struct device *dev, struct rtc_time *tm)
1. 应用层通过 ioctl(fd, RTC_SET_TIME, &rtc_tm) 设置时间的回调接口。
2. 驱动层需要停止芯片计时,并将 tm 转换为 BCD 更新到时间日期寄存器组。 rtc_class_ops->read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
1. 应用层通过 ioctl(fd, RTC_ALM_READ, &rtc_tm) 读取闹钟的回调接口。
2. 驱动层需要读取芯片里面的 alarm 寄存器组,转换为十进制更新到 alrm->time。
3. 驱动层需要读取芯片里面的 alarm 控制寄存器,更新到 alrm->enabled。 rtc_class_ops->set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
1. 应用层通过 ioctl(fd, RTC_ALM_SET, &rtc_tm) 设置闹钟的回调接口。 rtc_class_ops->alarm_irq_enable(struct device *dev, unsigned int enabled)
1. 应用层通过 ioctl(fd, RTC_AIE_ON/RTC_AIE_OFF, 0) 命令开启关闭闹钟中断的回调接口。
2. 应用层通过 read(fd, &data, sizeof(unsigned long))/select() 等待被中断唤醒。
3. 驱动层需要通过 GPIO 注册中断,并在中断里读取中断标志位,通过标志位来确定调用如下接口,唤醒应用程序。
rtc_update_irq(pcf85x63->rtc, 1, RTC_IRQF | RTC_AF);

应用层访问设备驱动的流程:

APP ---> rtc/rtc-dev.c(/dev/rtcX) ---> rtc/interface.c ---> pcf85263.c
RTC 设备驱动验证

方法一:使用现成的命令 hwclock

root@localhost:~# hwclock -f /dev/rtc0 --show
2022-10-20 09:30:12.679335+0800

方法二:自己编写应用程序验证

参考:linux-4.9\tools\testing\selftests\timers\rtctest.c

RTC 调试过程

一、测量硬件电压和时钟晶体
  1. 根据芯片手册,测量供电是否正常。
  2. 测量晶振频率是否为 32768 Hz。
  3. 测量 I2C 总线的上拉是否正常。
二、测试芯片能不能正常工作

查看寄存器手册,只要启动 RTC 时钟,再读取秒数寄存器,有累加即可确认正常。

root@localhost:~# i2cset -f -y 1 0x51 0x2e 0x00
root@localhost:~# i2cget -f -y 1 0x51 0x01
0x15
root@localhost:~# i2cget -f -y 1 0x51 0x01
0x16
root@localhost:~# i2cget -f -y 1 0x51 0x01
0x17
三、驱动移植
  1. 如果供应商有现成的驱动程序,当然是最快的。
  2. 如果供应商没有,则看看最新的内核有没有。
  3. 如果都没有,就拿相似的驱动程序根据芯片手册编写。
四、时间同步
  1. 设置系统时间从RTC启动和恢复
  2. 通过NTP同步方式设置RTC时间
make ARCH=arm64 menuconfig
Device Drivers -->
[*] Real Time Clock -->
[*] Set system time from RTC on startup and resume
(rtc0) RTC used to set the system time
[*] Set the RTC time based on NTP synchronization
(rtc0) RTC used to synchronize NTP adjustment

驱动分析

一、初始化流程
  1. 从 DTS 获取配置的中断引脚,85263 通过引脚产生中断通知 SOC。
  2. 配置 0x2B 寄存器为 0x00, 清除所有的中断标志
  3. 配置 0x27 寄存器, 设置 INTA(7pin) 引脚作为中断输出引脚
  4. 申请中断并设置为低电平触发, 且在中断响应期间不重复触发(IRQF_ONESHOT)
  5. 按要求实现 rtc 的五个基本回调接口,并调用 devm_rtc_device_register() 注册到 RTC 子系统。
// 获取中断引脚
pcf85x63->irq_number = 0;
gpio_config.gpio = of_get_named_gpio_flags(np, "int_port", 0, (enum of_gpio_flags *)(&gpio_config));
if (gpio_is_valid(gpio_config.gpio)){
pcf85x63->irq_gpio = gpio_config.gpio;
pcf85x63->irq_number = gpio_to_irq(pcf85x63->irq_gpio);
} if(0 == pcf85x63->irq_number){
dev_err(&client->dev, "get int gpio failed....\n");
return -EINVAL;
} // 配置 0x2B 寄存器为 0x00, 清除所有的中断标志
regmap_write(pcf85x63->regmap, CTRL_FLAGS, 0);
// 配置 0x27 寄存器, 设置 INTA(7pin) 引脚作为中断输出引脚
regmap_update_bits(pcf85x63->regmap, CTRL_PIN_IO, PIN_IO_INTA_OUT, PIN_IO_INTAPM);
// 申请中断并设置为低电平触发(IRQF_TRIGGER_LOW), 且在中断响应期间不重复触发(IRQF_ONESHOT)
ret = devm_request_threaded_irq(&client->dev, pcf85x63->irq_number, NULL, pcf85x63_rtc_handle_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT, client->name, client);
if (ret) {
dev_warn(&client->dev, "unable to request irq, alarms disabled\n");
return -EINVAL;
} // 注册 RTC 设备
pcf85x63->client = client;
i2c_set_clientdata(client, pcf85x63);
pcf85x63->rtc = devm_rtc_device_register(&client->dev, client->name, &rtc_ops, THIS_MODULE);
if (IS_ERR(pcf85x63->rtc)){
dev_err(&client->dev, "register rtc device failed....\n");
return PTR_ERR(pcf85x63->rtc);
}
二、读取时间的实现
  1. 一次性读出时间日期寄存器组(00h ~ 07h)。
  2. 由于芯片寄存器是以 BCD 方式存储,所以需要转换为十进制,并复制到 tm。
static int pcf85x63_rtc_read_time(struct device *dev, struct rtc_time *tm)
{
struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev);
unsigned char buf[DT_YEARS + 1];
int ret, len = sizeof(buf); // 一次性读出时间日期寄存器组(00h ~ 07h)
if ((ret = regmap_bulk_read(pcf85x63->regmap, DT_100THS, buf, len))) {
dev_err(dev, "%s: error %d\n", __func__, ret);
return ret;
} // 通过 BCD 转换
tm->tm_year = bcd2bin(buf[DT_YEARS]);
tm->tm_year += 100; // adjust for 1900 base of rtc_time
tm->tm_wday = buf[DT_WEEKDAYS] & 7;
buf[DT_SECS] &= 0x7F;
tm->tm_sec = bcd2bin(buf[DT_SECS]);
buf[DT_MINUTES] &= 0x7F;
tm->tm_min = bcd2bin(buf[DT_MINUTES]);
tm->tm_hour = bcd2bin(buf[DT_HOURS]);
tm->tm_mday = bcd2bin(buf[DT_DAYS]);
tm->tm_mon = bcd2bin(buf[DT_MONTHS]) - 1;
return 0;
}
三、设置时间的实现
  1. 通过设置 0x2E 寄存器来切断外部时钟的分配器,实现停止计时。
  2. 通过设置 0x2F 寄存器重置预分频器。
  3. 将应用层传下的时间转换为 BCD 更新到时间日期寄存器组(00h ~ 07h)
  4. 配置 0x2E 寄存器为 0x00, 开始计时。

static int pcf85x63_rtc_set_time(struct device *dev, struct rtc_time *tm)
{
struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev);
unsigned char tmp[11] = {0};
unsigned char *buf = &tmp[2];
int ret; // 要设置时间之前需要做的事情
tmp[0] = STOP_EN_STOP; // 配置 0x2E 寄存器为 0x01, 切断时钟, 停止计数( RTC clock is stopped)
tmp[1] = RESET_CPR; // 配置 0x2F 寄存器为 0xA4,重置预分频器
if((ret = regmap_bulk_write(pcf85x63->regmap, CTRL_STOP_EN, tmp, 2)))
return ret; // 将时间转换为 BCD 更新到时间日期寄存器组(00h ~ 07h)
buf[DT_100THS] = 0;
buf[DT_SECS] = bin2bcd(tm->tm_sec);
buf[DT_MINUTES] = bin2bcd(tm->tm_min);
buf[DT_HOURS] = bin2bcd(tm->tm_hour);
buf[DT_DAYS] = bin2bcd(tm->tm_mday);
buf[DT_WEEKDAYS] = tm->tm_wday;
buf[DT_MONTHS] = bin2bcd(tm->tm_mon + 1);
buf[DT_YEARS] = bin2bcd(tm->tm_year % 100);
if(regmap_bulk_write(pcf85x63->regmap, DT_100THS, buf, sizeof(tmp) - 2))
return ret; // 配置 0x2E 寄存器为 0x00, 开始计时( RTC clock runs)
return regmap_write(pcf85x63->regmap, CTRL_STOP_EN, 0);
}
四、读取闹钟的回调
  1. 一次性读出闹钟寄存器组(08h ~ 0Ch)。
  2. 将寄存器的 BCD 数据转换为十进制复制到 alrm->time。
  3. 读取 0x29 闹钟寄存器, 查询闹钟中断是否被使能。
  4. 如果闹钟中断被使能则通过更新 alrm->enabled 成员让应用层知道。
static int pcf85x63_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev);
unsigned char buf[DT_MONTH_ALM1 - DT_SECOND_ALM1 + 1];
unsigned int val;
int ret; // 一次性读出闹钟寄存器组(08h ~ 0Ch)
if ((ret = regmap_bulk_read(pcf85x63->regmap, DT_SECOND_ALM1, buf, sizeof(buf))))
return ret; alrm->time.tm_sec = bcd2bin(buf[0]);
alrm->time.tm_min = bcd2bin(buf[1]);
alrm->time.tm_hour = bcd2bin(buf[2]);
alrm->time.tm_mday = bcd2bin(buf[3]);
alrm->time.tm_mon = bcd2bin(buf[4]) - 1; // 读取 0x29 闹钟寄存器, 查询闹钟中断是否被使能
if ((ret = regmap_read(pcf85x63->regmap, CTRL_INTA_EN, &val)))
return ret; // 如果闹钟中断被使能则通过更新 alrm->enabled 成员让应用层知道
alrm->enabled = !!(val & INT_A1IE);
return 0;
}
五、设置闹钟的回调
  1. 关闭闹钟中断,避免设置过程中触发中断。
  2. 将应用层传下来的时间转化为 BCD,并更新到闹钟寄存器组(08h ~ 0Ch)。
  3. 根据应用层传下来的 alrm->enabled 决定是否再次打开闹钟中断。
static int pcf85x63_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm)
{
struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev);
unsigned char buf[DT_MONTH_ALM1 - DT_SECOND_ALM1 + 1];
int ret; // 转换为 BCD
buf[0] = bin2bcd(alrm->time.tm_sec);
buf[1] = bin2bcd(alrm->time.tm_min);
buf[2] = bin2bcd(alrm->time.tm_hour);
buf[3] = bin2bcd(alrm->time.tm_mday);
buf[4] = bin2bcd(alrm->time.tm_mon + 1); // 在设置时间之前先把中断关闭, 避免误触发中断
if ((ret = _pcf85x63_rtc_alarm_irq_enable(pcf85x63, 0)))
return ret; // 将时更新到闹钟寄存器组(08h ~ 0Ch)
if ((ret = regmap_bulk_write(pcf85x63->regmap, DT_SECOND_ALM1, buf, sizeof(buf))))
return ret; // 根据应用层的设置, 决定是否启用闹钟中断
return _pcf85x63_rtc_alarm_irq_enable(pcf85x63, alrm->enabled);
}
六、启用关闭闹钟中断
  1. 配置 0x10 寄存器, 启用/关闭 时、分、秒、日、月、的闹钟功能
  2. 配置 0x29 闹钟寄存器, 启用/关闭 闹钟中断, 上述月、日、时、分、秒有闹钟事件会触发中断
  3. 清除闹钟中断标志
static int _pcf85x63_rtc_alarm_irq_enable(struct pcf85x63 *pcf85x63, unsigned int enabled)
{
int ret;
unsigned int alarm_flags = ALRM_SEC_A1E | ALRM_MIN_A1E | ALRM_HR_A1E | ALRM_DAY_A1E | ALRM_MON_A1E; // 配置 0x10 寄存器, 启用/关闭 时、分、秒、日、月、的闹钟功能
ret = regmap_update_bits(pcf85x63->regmap, DT_ALARM_EN, alarm_flags, enabled ? alarm_flags : 0);
if (ret){
return ret;
} // 配置 0x29 闹钟寄存器, 启用/关闭 闹钟中断, 上述月、日、时、分、秒有闹钟事件会触发中断
ret = regmap_update_bits(pcf85x63->regmap, CTRL_INTA_EN, INT_A1IE, enabled ? INT_A1IE : 0);
if (ret || enabled){
return ret;
} // 清除闹钟中断标志
return regmap_update_bits(pcf85x63->regmap, CTRL_FLAGS, FLAGS_A1F, 0);
} static int pcf85x63_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled)
{
struct pcf85x63 *pcf85x63 = dev_get_drvdata(dev);
return _pcf85x63_rtc_alarm_irq_enable(pcf85x63, enabled);
}
七、闹钟中断服务程序
  1. 读取 0x2B 寄存器, 得到所有的中断标志。
  2. 如果是闹钟的中断(FLAGS_A1F),则调用 rtc_update_irq() 唤醒应用层,并清除闹钟中断标志位。
static irqreturn_t pcf85x63_rtc_handle_irq(int irq, void *dev_id)
{
struct pcf85x63 *pcf85x63 = i2c_get_clientdata(dev_id);
unsigned int flags; // 读取 0x2B 寄存器, 得到所有的中断标志
if (regmap_read(pcf85x63->regmap, CTRL_FLAGS, &flags))
return IRQ_NONE; // 如果是闹钟的中断
if (flags & FLAGS_A1F)
{
// 通知应用层有闹钟
rtc_update_irq(pcf85x63->rtc, 1, RTC_IRQF | RTC_AF);
// 清除闹钟中断标志
regmap_update_bits(pcf85x63->regmap, CTRL_FLAGS, FLAGS_A1F, 0);
return IRQ_HANDLED;
} return IRQ_NONE;
}

【分析笔记】NXP PCF85263 设备驱动分析笔记的更多相关文章

  1. Linux下 USB设备驱动分析(原创)

    之前做过STM32的usb HID复合设备,闲来看看linux下USB设备驱动是怎么一回事, 参考资料基于韦东山JZ2440开发板,以下,有错误欢迎指出. 1.准备知识 1.1USB相关概念: USB ...

  2. Linux设备驱动学习笔记

    之前研究Linux设备驱动时做的零零散散的笔记,整理出来,方便以后复习. 1.1驱动程序的的角色 提供机制 例如:unix图形界面分为X服务器和窗口会话管理器 X服务器理解硬件及提供统一的接口给用户程 ...

  3. framebuffer设备驱动分析

    一.设备驱动相关文件 1.1. 驱动框架相关文件 1.1.1. drivers/video/fbmem.c a. 创建graphics类.注册FB的字符设备驱动 fbmem_init(void) { ...

  4. linux字符设备驱动学习笔记(一):简单的字符设备驱动

    最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...

  5. linux PMBus总线及设备驱动分析

    PMBus协议规范介绍 PMBus是一套对电源进行配置.控制和监控的通讯协议标准.其最新版本为1.3,该规范还在不断演进中,比如新标准中新增的zone PMBus.AVSBus等特性.在其官网上有详细 ...

  6. Samsung_tiny4412(驱动笔记03)----字符设备驱动基本操作及调用流程

    /*********************************************************************************** * * 字符设备驱动基本操作及 ...

  7. 《linux设备驱动开发详解》笔记——6字符设备驱动

    6.1 字符设备驱动结构 先看看字符设备驱动的架构: 6.1.1 cdev cdev结构体是字符设备的核心数据结构,用于描述一个字符设备,cdev定义如下: #include <linux/cd ...

  8. linux设备驱动学习笔记(1)

    学习了将近半个月的设备驱动程序的编写,也有一些体会,这里写下来也给学习做一个总结,为后面的学习做更好的准备. 首先,个人感觉驱动程序的设计是很有套路的,最基本的要求就是要掌握这些套路.所谓的套路就是一 ...

  9. linux设备驱动学习笔记--内核调试方法之printk

    1,printk类似于用户态的printf函数,但是比printf函数多了一个日志级别,内核中最常见的日志输出都是通过调用printk来实现的,其打印级别有8种可能的记录字串, 在头文件 <Li ...

随机推荐

  1. 2022,一个Java程序猿的外设配置

    工欲善其事,必先利其器. 是的没错,我就是个器材党,哈哈.正赶上搬家布置了新桌面,经过我的精心挑选和安装,也是凑齐了我新一套的桌面外设.写下来记录一下. 键盘 套件:腹灵MK870 轴体:佳达隆G白P ...

  2. shardingsphere-jdbc 水平分表学习记录

    放在自己博客里搬过来一份~ 前司使用的是自己魔改的TDDL,在家时间比较多就尝试学一些业内比较常用的中间件. 这里记录一下学习中遇到的一些问题. 环境 设置的比较简单(太懒了就测试了几个表), 两个分 ...

  3. KMP算法,匹配字符串模板(返回下标)

    //KMP算法,匹配字符串模板 void getNext(int[] next, String t) { int n = next.length; for (int i = 1, j = 0; i & ...

  4. idea中springboot热部署(无需重启项目)

    idea中springboot热部署(无需重启项目) 1.在pom.xml文件中导入依赖 <dependency> <groupId>org.springframework.b ...

  5. Java安全之CC6

    前言 之前三篇详细分析了CommonsCollections1利用链,两种方法,LazyMap以及TransformedMap,但是在Javaa 8u71以后,这个利⽤链不能再利⽤了,主要原因是 su ...

  6. MvvmLight框架的基本使用

    关于MvvmLight框架的介绍可以看这篇,说的很详细,在此记录下来以作复习,通过一个简单的例子说明MvvmLight的基本使用 https://www.cnblogs.com/3xiaolonglo ...

  7. CSP 记

    csp 开考建好文件夹编译器不能用搞了半天换了台电脑 四道题看完一个小时过去了 第一题不会正解写了部分分还有点悬 第二题写暴力因为一个小错误调了半天 看时间不多了已经有点慌了 也没想正解直接开了下一题 ...

  8. vim快捷键及命令大全

    定位光标: G 将光标定位到文本末尾行首 gg 将光标定位到文本启始位置 0 (这个是零)定位到光标所在行行首 $ 定位到光标所在行行尾 数字G 跳转到第n行 移动光标: h 向左移动 l 向右移动 ...

  9. 关于最新版本listen1 (2.1.6)的修改心得(添加下载功能)

    注:本文只作为技术交流 前言 再次感谢 listen1 的作者开发出如此强大的音乐播放器 项目地址 上一篇的文章没有解决跨域问题(命名不能正确命名), 上一篇文章 地址 这次解决了,并简单的美化了下载 ...

  10. python爬虫爬取网易云音乐(超详细教程,附源码)

    一. 前言 先说结论,目前无法下载无损音乐,也无法下载vip音乐. 此代码模拟web网页js加密的过程,向api接口发送参数并获取数据,仅供参考学习,如果需要下载网易云音乐,不如直接在客户端下载,客户 ...