Linux 下 RTC 驱动开发 (硬件采用 DS1302)
- 2022-10-19  重庆
- 本文字数:7565 字 - 阅读完需:约 1 分钟 
一、前言
在 Linux 系统上主要有两个时间基准,一个数是系统时间和,一个是 RTC 时间。 其中系统时间是系统运行时由定时器(滴答定时器)维护的时间,掉电不保存数据。而 RTC 时间,是由 RTC 实时时钟芯片维护的时间,一般都接了后备电源(常见表现行为就是一颗纽扣电池供电),系统掉电后它不受影响,还是会运行保证时间准确。 每次系统开机时,系统会从 RTC 芯片里读取当前时间给系统时间赋值,保证系统开机之后时间也是准确的。
那么系统开机如何读取 RTC 时间的? 这个就需要用到 1 个命令:hwclock 。 这个命令是专门读写 RTC 驱动的。当然,通过打开设备文件,利用 ioctl 的命令也可以与 RTC 设备进行交互,使用 hwclock 命令更简单些。
而在 linux 应用层,可以通过 date 和 time 命令来设置系统时间的,而刚才说到的 hwclock 命令是用来设置和读写 RTC 时间的。
下面是系统 RTC 实时时钟时间的获取与设置命令用法案例:
1. 将 RTC 时间同步到系统时间[root@XiaoLong /]# hwclock -s为了在启动时自动执行 RTC 时间同步到系统时间,可以把 hwclock -s 命令加入到 profile 或者 rcS 文件中。
2. 获取显示 RTC 时间[root@XiaoLong /]# hwclock -rSun May 1 00:09:36 2016 0.000000 seconds
3. 将系统时间同步到 RTC,用于设置时间[root@XiaoLong /]# hwclock -w 
4. 查看 RTC 的信息[root@XiaoLong /]# cat /proc/driver/rtcrtc_time : 00:09:27rtc_date : 2016-05-01alrm_time : 23:24:07alrm_date : 2016-05-01alarm_IRQ : noalrm_pending : noupdate IRQ enabled : noperiodic IRQ enabled : noperiodic IRQ frequency : 1max user IRQ frequency : 3276824hr : yesperiodic_IRQ : no
二、设计思路
为了驱动的编写规范,Linux 下规定了很多标准的驱动框架结构,而 RTC 驱动也是有一套标准结构,只有按照标准写的 RTC 驱动,才可以对接上应用层的命令。这个结构称为:RTC 子系统。 RTC 是实时时钟,一般可以采用芯片内置的 RTC 实时时钟,也可以采用外置的 RTC 实时时钟芯片,比如:DS1320。 这篇文章接下来编写 RTC 驱动案例,硬件就采用外置的 DS1302 来作为 RTC 实时时钟芯片。
在第一章里介绍了 hwclock 命令的用法,我们发现这个命令主要功能就是从驱动里读写时间,那么对于驱动而言就是要响应这两个操作。根据之前编写这么多驱动的经验,大家应该也就想到了,应用层命令访问到驱动层肯定会有两个接口:这两个接口就是设置时间和获取时间。 驱动层只要把这两个接口实现了,应用层的命令就可以正确的读写 RTC 时间了。和之间讲到的块设备驱动(实现读写扇区接口),网络设备驱动(实现网络数据收发接口)的意思差不多。
标准的 RTC 驱动安装注册之后,在/dev 目录下会生成:rtcX 这样的名字。后面的 X 是数字,系统里可以存在多个 RTC 驱动。默认情况下 hwclock 命令是读写的/dev/rtc0 作为选择的主 RTC 驱动。这个可以在内核源码的配置里去修改的。
下面是调用框图:
 
  
  
 去掉内核自带的 RTC 驱动:
将内核自带的 RTC 驱动编译成模块,方便后面动态加载测试。
配置之后,重新内核,烧写内核。
 
 三、DS1302
当前 RTC 驱动的时钟芯片采用的是 DS1302,下面看看 DSS1302 的介绍(来至数据手册):
DS1302 是 DALLAS 公司推出的涓流充电时钟芯片 内含有一个实时时钟/日历和 31 字节静态 RAM 通过简单的串行接口与单片机进行通信 实时时钟/日历电路提供秒 分 时 日 日期 月 年的信息 每月的天数和闰年的天数可自动调整 时钟操作可通过 AM/PM 指示决定采用 24 或 12 小时格式 DS1302 与单片机之间能简单地采用同步串行的方式进行通信 仅需用到三个口线 1 RES 复位 2 I/O 数据线 3 SCLK串行时钟 时钟/RAM 的读/写数据以一个字节或多达 31 个字节的字符组方式通信 DS1302 工作时功耗很低 保持数据和时钟信息时功率小于 1mW。 DS1302 是由 DS1202 改进而来 增加了以下的特性 双电源管脚用于主电源和备份电源供应 Vcc1 为可编程涓流充电电源 附加七个字节存储器 它广泛应用于电话 传真 便携式仪器以及电池供电的仪器仪表等产品领域。
下面将主要的性能指标1. 实时时钟具有能计算 2100 年之前的秒 分 时 日 日期 星期 月 年的能力 还有闰年调整的能力2. 31 8 位暂存数据存储 RAM3. 串行 I/O 口方式使得管脚数量最少4. 宽范围工作电压 2.0 5.5V5. 工作电流 2.0V 时,小于 300nA6. 读/写时钟或 RAM 数据时 有两种传送方式 单字节传送和多字节传送 字符组方式7. 8 脚 DIP 封装或可选的 8 脚 SOIC 封装 根据表面装配8. 简单 3 线接口9. 与 TTL 兼容 Vcc=5V10. 可选工业级温度范围 -40 +8511. 与 DS1202 兼容12. 在 DS1202 基础上增加的特性对 Vcc1 有可选的涓流充电能力双电源管用于主电源和备份电源供应备份电源管脚可由电池或大容量电容输入附加的 7 字节暂存存储器
 
 下面是实物图:
 
  
  
 四、实现代码
4.1 内核提供的 rtc 底层注册与注销函数
1. RTC 框架注册函数struct rtc_device *rtc_device_register(const char *name, //RTC 时钟名称struct device *dev, //设备指针。该指针需要需要通过平台设备获取。const struct rtc_class_ops *ops, //rtc 文件操作集合struct module *owner) //驱动所有者。填: THIS_MODULE
使用示例: rtc_device_register("tiny4412_rtc",&pdev->dev, &tiny4412_rtcops,THIS_MODULE);使用 rtc_device_register 函数注册成功之后,在/dev/下可以看到 rtcx 的设备节点(x 是 rtc 的顺序编号)。
2. RTC 框架注销函数void rtc_device_unregister(struct rtc_device *rtc)经过 RTC 注册函数形参分析,RTC 子系统的注册需要通过平台设备框架完成,在平台设备的驱动端的 probe函数里进行 rtc 注册,remove 函数里进行注销,在 rtc 设备端向驱动端传递 RTC 硬件需要的一些信息。
4.2 驱动代码(平台设备层)
#include "linux/module.h"#include "linux/init.h"#include <linux/platform_device.h>/* * device  设备端 */
//释放平台总线static void pdev_release(struct device *dev){  printk("rtc_pdev:the rtc_pdev is close!!!\n");}
/*设备端结构体*/struct platform_device  rtc_pdev= /*设备结构体,设备名字很重要!*/{  .name = "DS1302rtc",  /*设备名*/  .id = -1,         /*-1表示创建成功后这边设备的名字就叫myled,若该值为0,1则设备名是myled.0,myled.1...*/  .dev =            /*驱动卸载时调用*/  {    .release = pdev_release,/*释放资源*/  },};
/*平台设备端入口函数*/static int __init plat_dev_init(void){  platform_device_register(&rtc_pdev);/*注册平台设备端*/  return 0;}
/*平台设备端出口函数*/static void __exit plat_dev_exit(void){  platform_device_unregister(&rtc_pdev);/*注销平台设备端*/}
module_init(plat_dev_init);module_exit(plat_dev_exit);MODULE_LICENSE("GPL");
4.3 驱动代码(平台应用层)
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <sys/ioctl.h>#include <linux/rtc.h>struct rtc_time time; //保存时间值
int main(int argc,char **argv){  if(argc!=2)  {    printf("传参格式:/dev/rtc\r\n");    return;  }  int fd=open(argv[1],O_RDWR); // 2==O_RDWR  if(fd<0)  {    printf("驱动设备文件打开失败!\r\n");    return 0;  }    time.tm_year=2017;  time.tm_mon=10;  time.tm_mday=13;  time.tm_hour=21;  time.tm_min=10;  time.tm_sec=10;    //注意:年月日必须填写正常,否则会导致底层函数无法调用成功    ioctl(fd,RTC_SET_TIME,&time);  while(1)  {    ioctl(fd,RTC_RD_TIME,&time);    printf("%d-%d-%d %d:%d:%d\r\n",time.tm_year,time.tm_mon,time.tm_mday,time.tm_hour,time.tm_min,time.tm_sec);    sleep(1);  }}
4.4 驱动代码(平台驱动层)
#include <linux/module.h>             /*驱动模块相关*/#include <linux/init.h>#include <linux/fs.h>                 /*文件操作集合*/#include <linux/cdev.h>#include <linux/device.h>#include <linux/miscdevice.h>#include <asm/io.h>#include <asm/uaccess.h>#include <linux/interrupt.h>          /*中断相关头文件*/#include <linux/irq.h>                /*中断相关头文件*/#include <linux/gpio.h>               /*硬件相关->定义了寄存器名字与地址*/#include <linux/wait.h>              #include <linux/sched.h>#include <linux/timer.h>              /*内核定时器*/#include <asm-generic/poll.h>         #include <linux/poll.h>               /* poll机制*/#include <linux/platform_device.h>    /* 平台设备驱动相关头文件*/#include <linux/rtc.h>#include <linux/gpio.h>#include <mach/gpio.h>#include <plat/gpio-cfg.h>#include <linux/delay.h>
/*--------------------------------DS1302相关操作代码---------------------------------------------*/
static unsigned char RTC_bin2bcd(unsigned val){  return ((val/10)<<4)+val%10;}
static unsigned RTC_bcd2bin(unsigned char val){  return (val&0x0f)+(val>>4)*10;}
/*函数功能:DS1302初始化Tiny4412硬件连接:  CLK :GPB_4  DAT :GPB_5  RST :GPB_6*/void DS1302IO_Init(void){  /*1. 注册GPIO*/  gpio_request(EXYNOS4_GPB(4), "DS1302_CLK");  gpio_request(EXYNOS4_GPB(5), "DS1302_DAT");  gpio_request(EXYNOS4_GPB(6), "DS1302_RST");    /*2. 配置GPIO口模式*/  s3c_gpio_cfgpin(EXYNOS4_GPB(4), S3C_GPIO_OUTPUT);  //时钟  s3c_gpio_cfgpin(EXYNOS4_GPB(5), S3C_GPIO_OUTPUT);  //数据//  s3c_gpio_cfgpin(EXYNOS4_GPB(2), S3C_GPIO_INPUT);   //输入模式  s3c_gpio_cfgpin(EXYNOS4_GPB(6), S3C_GPIO_OUTPUT);  //复位    /*3. 上拉GPIO口*/  gpio_set_value(EXYNOS4_GPB(4), 1); //CLK  gpio_set_value(EXYNOS4_GPB(5), 1); //DAT   gpio_set_value(EXYNOS4_GPB(6), 1); //RST    gpio_set_value(EXYNOS4_GPB(6), 0);      //RST脚置低  gpio_set_value(EXYNOS4_GPB(4), 0);      //SCK脚置低}
//#define  RTC_CMD_READ  0x81    /* Read command *///#define  RTC_CMD_WRITE  0x80    /* Write command *///#define RTC_ADDR_RAM0  0x20      /* Address of RAM0 *///#define RTC_ADDR_TCR  0x08      /* Address of trickle charge register *///#define  RTC_ADDR_YEAR  0x06    /* Address of year register *///#define  RTC_ADDR_DAY  0x05    /* Address of day of week register *///#define  RTC_ADDR_MON  0x04    /* Address of month register *///#define  RTC_ADDR_DATE  0x03    /* Address of day of month register *///#define  RTC_ADDR_HOUR  0x02    /* Address of hour register *///#define  RTC_ADDR_MIN  0x01    /* Address of minute register *///#define  RTC_ADDR_SEC  0x00    /* Address of second register */
//DS1302地址定义#define ds1302_sec_add      0x80    //秒数据地址#define ds1302_min_add      0x82    //分数据地址#define ds1302_hr_add      0x84    //时数据地址#define ds1302_date_add      0x86    //日数据地址#define ds1302_month_add    0x88    //月数据地址#define ds1302_day_add      0x8a    //星期数据地址#define ds1302_year_add      0x8c    //年数据地址#define ds1302_control_add    0x8e    //控制数据地址#define ds1302_charger_add    0x90            #define ds1302_clkburst_add    0xbe
//初始时间定义static unsigned char time_buf[8] = {0x20,0x10,0x06,0x01,0x23,0x59,0x55,0x02};//初始时间2010年6月1号23点59分55秒 星期二
static unsigned char readtime[14];//当前时间static unsigned char sec_buf=0;   //秒缓存static unsigned char sec_flag=0;  //秒标志位
//向DS1302写入一字节数据static void ds1302_write_byte(unsigned char addr, unsigned char d) {  unsigned char i;  gpio_set_value(EXYNOS4_GPB(6), 1);          //启动DS1302总线    //写入目标地址:addr  addr = addr & 0xFE;   //最低位置零,寄存器0位为0时写,为1时读  for(i=0;i<8;i++)  {    if(addr&0x01){gpio_set_value(EXYNOS4_GPB(5), 1);}    else{gpio_set_value(EXYNOS4_GPB(5), 0);}    gpio_set_value(EXYNOS4_GPB(4), 1);      //产生时钟    gpio_set_value(EXYNOS4_GPB(4), 0);    addr=addr >> 1;  }    //写入数据:d  for(i=0;i<8;i++)  {    if(d & 0x01) {gpio_set_value(EXYNOS4_GPB(5), 1);}    else {gpio_set_value(EXYNOS4_GPB(5), 0);}    gpio_set_value(EXYNOS4_GPB(4), 1);    //产生时钟    gpio_set_value(EXYNOS4_GPB(4), 0);    d = d >> 1;  }  gpio_set_value(EXYNOS4_GPB(6), 0);    //停止DS1302总线}
//从DS1302读出一字节数据static unsigned char ds1302_read_byte(unsigned char addr){  unsigned char i,temp;    gpio_set_value(EXYNOS4_GPB(6), 1);//启动DS1302总线  //写入目标地址:addr  addr=addr | 0x01;    //最低位置高,寄存器0位为0时写,为1时读  for(i=0; i<8; i++)  {    if(addr & 0x01){gpio_set_value(EXYNOS4_GPB(5), 1);}    else {gpio_set_value(EXYNOS4_GPB(5), 0);}    gpio_set_value(EXYNOS4_GPB(4), 1);    gpio_set_value(EXYNOS4_GPB(4), 0);    addr=addr >> 1;  }        s3c_gpio_cfgpin(EXYNOS4_GPB(5), S3C_GPIO_INPUT);   //输入模式  //输出数据:temp  for(i=0; i<8; i++)  {    temp=temp>>1;    if(gpio_get_value(EXYNOS4_GPB(5))){temp |= 0x80;}    else{temp&=0x7F;}    gpio_set_value(EXYNOS4_GPB(4), 1);    gpio_set_value(EXYNOS4_GPB(4), 0);  }  s3c_gpio_cfgpin(EXYNOS4_GPB(5), S3C_GPIO_OUTPUT);  //输出模式  gpio_set_value(EXYNOS4_GPB(6), 0);          //停止DS1302总线  return temp;}
//向DS302写入时钟数据static void ds1302_write_time(struct rtc_time *time) {  ds1302_write_byte(ds1302_control_add,0x00);        //关闭写保护   ds1302_write_byte(ds1302_sec_add,0x80);          //暂停时钟   //ds1302_write_byte(ds1302_charger_add,0xa9);        //涓流充电  /*设置RTC时间*/    //因为DS1302的年份只能设置后两位,所有需要使用正常的年份减去2000,得到实际的后两位  ds1302_write_byte(ds1302_year_add,RTC_bin2bcd(time->tm_year-2000));    //年   ds1302_write_byte(ds1302_month_add,RTC_bin2bcd(time->tm_mon));    //月   ds1302_write_byte(ds1302_date_add,RTC_bin2bcd(time->tm_mday));    //日   ds1302_write_byte(ds1302_hr_add,RTC_bin2bcd(time->tm_hour));    //时   ds1302_write_byte(ds1302_min_add,RTC_bin2bcd(time->tm_min));    //分  ds1302_write_byte(ds1302_sec_add,RTC_bin2bcd(time->tm_sec));    //秒  //ds1302_write_byte(ds1302_day_add,RTC_bin2bcd(time->tm_wday));    //周  time->tm_wday一周中的某一天  ds1302_write_byte(ds1302_control_add,0x80);         //打开写保护     }
static int DS1302_rtc_ioctl(struct device *dev, unsigned int cmd,unsigned long arg){  /*设置RTC时间*/  struct rtc_time time;  copy_from_user(&time,(const void __user *)arg,sizeof(struct rtc_time));  ds1302_write_time(&time);  return 0;}
//此函数通过应用层的ioctl的RTC_RD_TIME命令进行调用static int tiny4412_rtc_gettime(struct device *dev, struct rtc_time *rtc_tm){  rtc_tm->tm_year=RTC_bcd2bin(ds1302_read_byte(ds1302_year_add))+2000;   //年   rtc_tm->tm_mon=RTC_bcd2bin(ds1302_read_byte(ds1302_month_add));   //月   rtc_tm->tm_mday=RTC_bcd2bin(ds1302_read_byte(ds1302_date_add));   //日   rtc_tm->tm_hour=RTC_bcd2bin(ds1302_read_byte(ds1302_hr_add));    //时   rtc_tm->tm_min=RTC_bcd2bin(ds1302_read_byte(ds1302_min_add));    //分   rtc_tm->tm_sec=RTC_bcd2bin((ds1302_read_byte(ds1302_sec_add))&0x7f);//秒,屏蔽秒的第7位,避免超出59  //time_buf[7]=ds1302_read_byte(ds1302_day_add);    //周   return 0;}
//此函数通过应用层的ioctl的RTC_SET_TIME命令进行调用static int tiny4412_rtc_settime(struct device *dev, struct rtc_time *tm){  ds1302_write_time(tm);   return 0;  }
/*RTC文件操作*/static const struct rtc_class_ops DS1302_rtcops = {  .ioctl=DS1302_rtc_ioctl,  .read_time  = tiny4412_rtc_gettime,  .set_time  = tiny4412_rtc_settime};
static struct rtc_device *rtc=NULL;/*当设备匹配成功执行的函数-资源探查函数*/static int drv_probe(struct platform_device *pdev){    rtc = rtc_device_register("DS1302RTC",&pdev->dev, &DS1302_rtcops,THIS_MODULE);  if(rtc==NULL)  printk("RTC驱动注册失败1\n");  else    {      printk("RTC驱动注册成功1\n");    }    /*1. 初始化GPIO口*/  DS1302IO_Init();  msleep(10);    return 0;}
static int drv_remove(struct platform_device *dev)/*当设备卸载后调用这条函数*/{  /*释放GPIO口*/  gpio_free(EXYNOS4_GPB(4));  gpio_free(EXYNOS4_GPB(5));  gpio_free(EXYNOS4_GPB(6));    rtc_device_unregister(rtc);  printk("RTC驱动卸载成功\n");  return 0;}
/*平台设备驱动端结构体-包含和probe匹配的设备名字*/struct platform_driver  drv= {  .probe = drv_probe,    /*需要创建一个probe函数,这个函数是对设备进行操作*/  .remove = drv_remove,  /*创建一个remove函数,用于设备退出*/  .driver =   {    .name = "DS1302rtc",    /*设备名称,用来与设备端匹配(非常重要)*/  },};
/*平台驱动端的入口函数*/static int __init plat_drv_init(void){  platform_driver_register(&drv);/*注册平台驱动*/    return 0;}
/*平台驱动端的出口函数*/static void __exit plat_drv_exit(void){  platform_driver_unregister(&drv);/*释放平台驱动*/}
module_init(plat_drv_init);  /*驱动模块的入口*/module_exit(plat_drv_exit);  /*驱动模块的出口*/MODULE_LICENSE("GPL");       /*驱动的许可证-声明*/
版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/32b1e80878aaa2080b9d046a4】。文章转载请联系作者。

DS小龙哥
之所以觉得累,是因为说的比做的多。 2022-01-06 加入
熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域










 
    
评论