最近需要调试一颗RTC BM6563,
按照标准Linux RTC来实现,方便驱动多平台的移植或者IC替换,
也方便app代码rtc部分的移植性.
遇到的问题
1) 设置的时间格式为无效值,导致无法设置rtc时间,排查了小半天.
2) RTC框架上时间设置和bm6563不完全对应,寄存器的读写需要细微调整转换数值.
// linux 官方rtc app 测试code
linux 下的RTC可概括为三个功能
(1) watcher(当前时间): 读取时间和设置时间 RTC_RD_TIME, RTC_SET_TIME
(2) alarm(闹钟): 24小时内的闹钟功能,可能需要打开和关闭相应中断 RTC_AIE_ON, RTC_AIE_OFF, RTC_ALM_SET, RTC_ALM_READ
(3) 用于低功耗唤醒等等,不关注
#include <stdio.h>
#include <linux/rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
/*
* This expects the new RTC class driver framework, working with
* clocks that will often not be clones of what the PC-AT had.
* Use the command line to specify another RTC if you need one.
*/
static const char default_rtc[] = "/dev/rtc0";
int main(int argc, char **argv)
{
int i, fd, retval, irqcount = 0;
unsigned long tmp, data;
struct rtc_time rtc_tm;
const char *rtc = default_rtc;
switch (argc) {
case 2:
rtc = argv[1];
/* FALLTHROUGH */A
case 1:
break;
default:
fprintf(stderr, "usage: rtctest [rtcdev]\n");
return 1;
}
fd = open(rtc, O_RDONLY);
if (fd == -1) {
perror(rtc);
exit(errno);
}
fprintf(stderr, "\n\t\t\tRTC Driver Test Example.\n\n");
/* Turn on update interrupts (one per second) */
retval = ioctl(fd, RTC_UIE_ON, 0);
if (retval == -1) {
if (errno == ENOTTY) {
fprintf(stderr,
"\n...Update IRQs not supported.\n");
goto test_READ;
}
perror("RTC_UIE_ON ioctl");
exit(errno);
}
fprintf(stderr, "Counting 5 update (1/sec) interrupts from reading %s:",
rtc);
fflush(stderr);
for (i=1; i<6; i++) {
/* This read will block */
retval = read(fd, &data, sizeof(unsigned long));
if (retval == -1) {
perror("read");
exit(errno);
}
fprintf(stderr, " %d",i);
fflush(stderr);
irqcount++;
}
fprintf(stderr, "\nAgain, from using select(2) on /dev/rtc:");
fflush(stderr);
for (i=1; i<6; i++) {
struct timeval tv = {5, 0}; /* 5 second timeout on select */
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/* The select will wait until an RTC interrupt happens. */
retval = select(fd+1, &readfds, NULL, NULL, &tv);
if (retval == -1) {
perror("select");
exit(errno);
}
/* This read won't block unlike the select-less case above. */
retval = read(fd, &data, sizeof(unsigned long));
if (retval == -1) {
perror("read");
exit(errno);
}
fprintf(stderr, " %d",i);
fflush(stderr);
irqcount++;
}
/* Turn off update interrupts */
retval = ioctl(fd, RTC_UIE_OFF, 0);
if (retval == -1) {
perror("RTC_UIE_OFF ioctl");
exit(errno);
}
test_READ:
/* Read the RTC time/date */
retval = ioctl(fd, RTC_RD_TIME, &rtc_tm);
if (retval == -1) {
perror("RTC_RD_TIME ioctl");
exit(errno);
}
fprintf(stderr, "\n\nCurrent RTC date/time is %d-%d-%d, %02d:%02d:%02d.\n",
rtc_tm.tm_mday, rtc_tm.tm_mon + 1, rtc_tm.tm_year + 1900,
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
/* Set the alarm to 5 sec in the future, and check for rollover */
rtc_tm.tm_sec += 5;
if (rtc_tm.tm_sec >= 60) {
rtc_tm.tm_sec %= 60;
rtc_tm.tm_min++;
}
if (rtc_tm.tm_min == 60) {
rtc_tm.tm_min = 0;
rtc_tm.tm_hour++;
}
if (rtc_tm.tm_hour == 24)
rtc_tm.tm_hour = 0;
retval = ioctl(fd, RTC_ALM_SET, &rtc_tm);
if (retval == -1) {
if (errno == ENOTTY) {
fprintf(stderr,
"\n...Alarm IRQs not supported.\n");
goto test_PIE;
}
perror("RTC_ALM_SET ioctl");
exit(errno);
}
/* Read the current alarm settings */
retval = ioctl(fd, RTC_ALM_READ, &rtc_tm);
if (retval == -1) {
perror("RTC_ALM_READ ioctl");
exit(errno);
}
fprintf(stderr, "Alarm time now set to %02d:%02d:%02d.\n",
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
/* Enable alarm interrupts */
retval = ioctl(fd, RTC_AIE_ON, 0);
if (retval == -1) {
perror("RTC_AIE_ON ioctl");
exit(errno);
}
fprintf(stderr, "Waiting 5 seconds for alarm...");
fflush(stderr);
/* This blocks until the alarm ring causes an interrupt */
retval = read(fd, &data, sizeof(unsigned long));
if (retval == -1) {
perror("read");
exit(errno);
}
irqcount++;
fprintf(stderr, " okay. Alarm rang.\n");
/* Disable alarm interrupts */
retval = ioctl(fd, RTC_AIE_OFF, 0);
if (retval == -1) {
perror("RTC_AIE_OFF ioctl");
exit(errno);
}
test_PIE:
/* Read periodic IRQ rate */
retval = ioctl(fd, RTC_IRQP_READ, &tmp);
if (retval == -1) {
/* not all RTCs support periodic IRQs */
if (errno == ENOTTY) {
fprintf(stderr, "\nNo periodic IRQ support\n");
goto done;
}
perror("RTC_IRQP_READ ioctl");
exit(errno);
}
fprintf(stderr, "\nPeriodic IRQ rate is %ldHz.\n", tmp);
fprintf(stderr, "Counting 20 interrupts at:");
fflush(stderr);
/* The frequencies 128Hz, 256Hz, ... 8192Hz are only allowed for root. */
for (tmp=2; tmp<=64; tmp*=2) {
retval = ioctl(fd, RTC_IRQP_SET, tmp);
if (retval == -1) {
/* not all RTCs can change their periodic IRQ rate */
if (errno == ENOTTY) {
fprintf(stderr,
"\n...Periodic IRQ rate is fixed\n");
goto done;
}
perror("RTC_IRQP_SET ioctl");
exit(errno);
}
fprintf(stderr, "\n%ldHz:\t", tmp);
fflush(stderr);
/* Enable periodic interrupts */
retval = ioctl(fd, RTC_PIE_ON, 0);
if (retval == -1) {
perror("RTC_PIE_ON ioctl");
exit(errno);
}
for (i=1; i<21; i++) {
/* This blocks */
retval = read(fd, &data, sizeof(unsigned long));
if (retval == -1) {
perror("read");
exit(errno);
}
fprintf(stderr, " %d",i);
fflush(stderr);
irqcount++;
}
/* Disable periodic interrupts */
retval = ioctl(fd, RTC_PIE_OFF, 0);
if (retval == -1) {
perror("RTC_PIE_OFF ioctl");
exit(errno);
}
}
done:
fprintf(stderr, "\n\n\t\t\t *** Test complete ***\n");
close(fd);
return 0;
}
然后是驱动部分,先参考下内核几个自带的驱动.
思路需要构造RTC结构体,然后填充RD_TIME,和SET_TIME.
rtc = rtc_device_register(em3027_driver.driver.name, &client->dev,
&em3027_rtc_ops, THIS_MODULE);
//驱动代码
/*
* An rtc/i2c driver for the BM8563
* Copyright 2018 by hecong
*
* Author: Echo <halecho@163.com>
*
* Based on rtc-bm8563.c by Alessandro Zummo <a.zummo@towertech.it>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/clk.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/kernel.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <asm/unaligned.h>
#include <asm/io.h>
#include <linux/timer.h>
#include <linux/rtc.h>
#include <linux/i2c.h>
#include <linux/rtc.h>
#include <linux/bcd.h>
/* Registers */
#define REG_CTRL_1 0x00
#define REG_CTRL_2 0x01
#define REG_WATCH_SEC 0x02
#define REG_WATCH_MIN 0x03
#define REG_WATCH_HOUR 0x04
#define REG_WATCH_DATE 0x05
#define REG_WATCH_DAY 0x06
#define REG_WATCH_MON 0x07
#define REG_WATCH_YEAR 0x08
#define RTC_I2C_NUM 1
#define RTC_ADDR 0xa2/2
static struct i2c_board_info bm8563_info = {
I2C_BOARD_INFO("bm8563", RTC_ADDR),
};
static struct i2c_client *bm8563_client;
static int bm8563_get_time(struct device *dev, struct rtc_time *tm)
{
struct i2c_client *client = to_i2c_client(dev);
unsigned char addr = REG_WATCH_SEC;
unsigned char buf[7];
struct i2c_msg msgs[] = {
{client->addr, 0, 1, &addr}, /* setup read addr */
{client->addr, I2C_M_RD, 7, buf}, /* read time/date */
};
/* read time/date registers */
if ((i2c_transfer(client->adapter, &msgs[0], 2)) != 2) {
dev_err(&client->dev, "%s: read error\n", __func__);
return -EIO;
}
//printk("kernel get time\n");
tm->tm_sec = bcd2bin(buf[0]&0x7f) ;
tm->tm_min = bcd2bin(buf[1]);
tm->tm_hour = bcd2bin(buf[2]);
tm->tm_mday = bcd2bin(buf[3]);
tm->tm_wday = bcd2bin(buf[4]);
tm->tm_mon = bcd2bin(buf[5])-1;
tm->tm_year = bcd2bin(buf[6]) + 100;
if((tm->tm_sec == 0) && (tm->tm_min == 0) && (tm->tm_hour == 0) )
rtc_time_to_tm(0, tm);
return 0;
}
static int bm8563_set_time(struct device *dev, struct rtc_time *tm)
{
struct i2c_client *client = to_i2c_client(dev);
unsigned char buf[8];
struct i2c_msg msg = {
client->addr, 0, 8, buf, /* write time/date */
};
//printk("kernel set time\n");
buf[0] = REG_WATCH_SEC;
buf[1] = bin2bcd(tm->tm_sec);
buf[2] = bin2bcd(tm->tm_min);
buf[3] = bin2bcd(tm->tm_hour);
buf[4] = bin2bcd(tm->tm_mday);
buf[5] = bin2bcd(tm->tm_wday);
buf[6] = bin2bcd(tm->tm_mon)+1;
buf[7] = bin2bcd(tm->tm_year % 100);
/* write time/date registers */
if ((i2c_transfer(client->adapter, &msg, 1)) != 1) {
dev_err(&client->dev, "%s: write error\n", __func__);
return -EIO;
}
return 0;
}
static const struct rtc_class_ops bm8563_rtc_ops = {
.read_time = bm8563_get_time,
.set_time = bm8563_set_time,
};
static int bm8563_dev_init(void)
{
struct rtc_device *rtc;
struct i2c_adapter *i2c_adap;
struct i2c_client *client;
unsigned char buf[2];
struct i2c_msg msg = {
RTC_ADDR, 0, 2, buf, /* write time/date */
};
i2c_adap = i2c_get_adapter(RTC_I2C_NUM);
bm8563_client = i2c_new_device(i2c_adap, &bm8563_info);
i2c_put_adapter(i2c_adap);
client = bm8563_client;
buf[0] = REG_CTRL_1;
buf[1] = 0;
if ((i2c_transfer(client->adapter, &msg, 1)) != 1) {
dev_err(&client->dev, "%s: write error\n", __func__);
return -EIO;
}
rtc = rtc_device_register("bm8563", &client->dev,
&bm8563_rtc_ops, THIS_MODULE);
if (IS_ERR(rtc))
return PTR_ERR(rtc);
i2c_set_clientdata(client, rtc);
return 0;
}
static void bm8563_dev_exit(void)
{
struct rtc_device *rtc = i2c_get_clientdata(bm8563_client);
if (rtc)
rtc_device_unregister(rtc);
i2c_unregister_device(bm8563_client);
}
module_init(bm8563_dev_init);
module_exit(bm8563_dev_exit);
MODULE_AUTHOR("halecho@163.com");
MODULE_DESCRIPTION(" qualvision rtc driver");
MODULE_LICENSE("GPL");
测试代码
#include <stdio.h>
#include <linux/rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
/*
* This expects the new RTC class driver framework, working with
* clocks that will often not be clones of what the PC-AT had.
* Use the command line to specify another RTC if you need one.
*/
static const char default_rtc[] = "/dev/rtc1";
int main(int argc, char **argv)
{
int i, fd, fd1, retval, irqcount = 0;
unsigned long tmp, data;
struct rtc_time rtc_tm;
const char *rtc = default_rtc;
switch (argc) {
case 2:
rtc = argv[1];
/* FALLTHROUGH */
case 1:
break;
default:
fprintf(stderr, "usage: rtctest [rtcdev]\n");
return 1;
}
fd = open(rtc, O_RDWR);
if (fd == -1) {
perror(rtc);
exit(errno);
}
/* Read the RTC time/date */
retval = ioctl(fd, RTC_RD_TIME, &rtc_tm);
if (retval == -1) {
perror("RTC_RD_TIME ioctl");
exit(errno);
}
printf("\n\nCurrent RTC date/time is %d-%d-%d, %2d:%02d:%02d.\n\n\n",
rtc_tm.tm_mday, rtc_tm.tm_mon, rtc_tm.tm_year + 1900,
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
/* set time to test */
rtc_tm.tm_mday = 30;
rtc_tm.tm_mon = 11;
rtc_tm.tm_year = 105; //应该大于100,注意转换,实际为1900+105
rtc_tm.tm_hour = 10;
rtc_tm.tm_min = 12;
rtc_tm.tm_sec = 13;
retval = ioctl(fd, RTC_SET_TIME, &rtc_tm);
if (retval == -1) {
printf("\n... not supported.\n");
} else {
printf("set ok rtc0\n");
}
for (i = 0; i < 20; i++) {
retval = ioctl(fd, RTC_RD_TIME, &rtc_tm);
printf("\n\nCurrent:%d-%d-%d, %2d:%02d:%02d.\n\n\n",
rtc_tm.tm_mday, rtc_tm.tm_mon, rtc_tm.tm_year + 1900,
//Linux标准RTC时间都要加1900
rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
sleep(1);
}
close(fd);
return 0;
}