Navigation

    全志在线开发者论坛

    • Register
    • Login
    • Search
    • Categories
    • Tags
    • 在线文档
    • 社区主页

    【分析笔记】NXP PCF85263 设备驱动分析笔记

    Linux
    1
    1
    1162
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • D
      dream LV 6 last edited by

      驱动移植

      供应商无法提供相应的驱动程序,不过在 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;
      }
      
      1 Reply Last reply Reply Quote Share 0
      • 1 / 1
      • First post
        Last post

      Copyright © 2024 深圳全志在线有限公司 粤ICP备2021084185号 粤公网安备44030502007680号

      行为准则 | 用户协议 | 隐私权政策