2018-06-06 18:08:54 +03:00
// SPDX-License-Identifier: GPL-2.0
// RTC driver for ChromeOS Embedded Controller.
//
// Copyright (C) 2017 Google, Inc.
// Author: Stephen Barber <smbarber@chromium.org>
2017-11-11 00:55:53 +03:00
# include <linux/kernel.h>
# include <linux/mfd/cros_ec.h>
# include <linux/mfd/cros_ec_commands.h>
# include <linux/module.h>
# include <linux/platform_device.h>
# include <linux/rtc.h>
# include <linux/slab.h>
# define DRV_NAME "cros-ec-rtc"
/**
* struct cros_ec_rtc - Driver data for EC RTC
*
* @ cros_ec : Pointer to EC device
* @ rtc : Pointer to RTC device
* @ notifier : Notifier info for responding to EC events
* @ saved_alarm : Alarm to restore when interrupts are reenabled
*/
struct cros_ec_rtc {
struct cros_ec_device * cros_ec ;
struct rtc_device * rtc ;
struct notifier_block notifier ;
u32 saved_alarm ;
} ;
static int cros_ec_rtc_get ( struct cros_ec_device * cros_ec , u32 command ,
u32 * response )
{
int ret ;
struct {
struct cros_ec_command msg ;
struct ec_response_rtc data ;
} __packed msg ;
memset ( & msg , 0 , sizeof ( msg ) ) ;
msg . msg . command = command ;
msg . msg . insize = sizeof ( msg . data ) ;
ret = cros_ec_cmd_xfer_status ( cros_ec , & msg . msg ) ;
if ( ret < 0 ) {
dev_err ( cros_ec - > dev ,
" error getting %s from EC: %d \n " ,
command = = EC_CMD_RTC_GET_VALUE ? " time " : " alarm " ,
ret ) ;
return ret ;
}
* response = msg . data . time ;
return 0 ;
}
static int cros_ec_rtc_set ( struct cros_ec_device * cros_ec , u32 command ,
u32 param )
{
int ret = 0 ;
struct {
struct cros_ec_command msg ;
struct ec_response_rtc data ;
} __packed msg ;
memset ( & msg , 0 , sizeof ( msg ) ) ;
msg . msg . command = command ;
msg . msg . outsize = sizeof ( msg . data ) ;
msg . data . time = param ;
ret = cros_ec_cmd_xfer_status ( cros_ec , & msg . msg ) ;
if ( ret < 0 ) {
dev_err ( cros_ec - > dev , " error setting %s on EC: %d \n " ,
command = = EC_CMD_RTC_SET_VALUE ? " time " : " alarm " ,
ret ) ;
return ret ;
}
return 0 ;
}
/* Read the current time from the EC. */
static int cros_ec_rtc_read_time ( struct device * dev , struct rtc_time * tm )
{
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( dev ) ;
struct cros_ec_device * cros_ec = cros_ec_rtc - > cros_ec ;
int ret ;
u32 time ;
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_VALUE , & time ) ;
if ( ret ) {
dev_err ( dev , " error getting time: %d \n " , ret ) ;
return ret ;
}
rtc_time64_to_tm ( time , tm ) ;
return 0 ;
}
/* Set the current EC time. */
static int cros_ec_rtc_set_time ( struct device * dev , struct rtc_time * tm )
{
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( dev ) ;
struct cros_ec_device * cros_ec = cros_ec_rtc - > cros_ec ;
int ret ;
time64_t time ;
time = rtc_tm_to_time64 ( tm ) ;
if ( time < 0 | | time > U32_MAX )
return - EINVAL ;
ret = cros_ec_rtc_set ( cros_ec , EC_CMD_RTC_SET_VALUE , ( u32 ) time ) ;
if ( ret < 0 ) {
dev_err ( dev , " error setting time: %d \n " , ret ) ;
return ret ;
}
return 0 ;
}
/* Read alarm time from RTC. */
static int cros_ec_rtc_read_alarm ( struct device * dev , struct rtc_wkalrm * alrm )
{
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( dev ) ;
struct cros_ec_device * cros_ec = cros_ec_rtc - > cros_ec ;
int ret ;
u32 current_time , alarm_offset ;
/*
* The EC host command for getting the alarm is relative ( i . e . 5
* seconds from now ) whereas rtc_wkalrm is absolute . Get the current
* RTC time first so we can calculate the relative time .
*/
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_VALUE , & current_time ) ;
if ( ret < 0 ) {
dev_err ( dev , " error getting time: %d \n " , ret ) ;
return ret ;
}
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_ALARM , & alarm_offset ) ;
if ( ret < 0 ) {
dev_err ( dev , " error getting alarm: %d \n " , ret ) ;
return ret ;
}
rtc_time64_to_tm ( current_time + alarm_offset , & alrm - > time ) ;
return 0 ;
}
/* Set the EC's RTC alarm. */
static int cros_ec_rtc_set_alarm ( struct device * dev , struct rtc_wkalrm * alrm )
{
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( dev ) ;
struct cros_ec_device * cros_ec = cros_ec_rtc - > cros_ec ;
int ret ;
time64_t alarm_time ;
u32 current_time , alarm_offset ;
/*
* The EC host command for setting the alarm is relative
* ( i . e . 5 seconds from now ) whereas rtc_wkalrm is absolute .
* Get the current RTC time first so we can calculate the
* relative time .
*/
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_VALUE , & current_time ) ;
if ( ret < 0 ) {
dev_err ( dev , " error getting time: %d \n " , ret ) ;
return ret ;
}
alarm_time = rtc_tm_to_time64 ( & alrm - > time ) ;
if ( alarm_time < 0 | | alarm_time > U32_MAX )
return - EINVAL ;
if ( ! alrm - > enabled ) {
/*
* If the alarm is being disabled , send an alarm
* clear command .
*/
alarm_offset = EC_RTC_ALARM_CLEAR ;
cros_ec_rtc - > saved_alarm = ( u32 ) alarm_time ;
} else {
/* Don't set an alarm in the past. */
2018-02-27 05:50:03 +03:00
if ( ( u32 ) alarm_time < = current_time )
return - ETIME ;
alarm_offset = ( u32 ) alarm_time - current_time ;
2017-11-11 00:55:53 +03:00
}
ret = cros_ec_rtc_set ( cros_ec , EC_CMD_RTC_SET_ALARM , alarm_offset ) ;
if ( ret < 0 ) {
dev_err ( dev , " error setting alarm: %d \n " , ret ) ;
return ret ;
}
return 0 ;
}
static int cros_ec_rtc_alarm_irq_enable ( struct device * dev ,
unsigned int enabled )
{
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( dev ) ;
struct cros_ec_device * cros_ec = cros_ec_rtc - > cros_ec ;
int ret ;
u32 current_time , alarm_offset , alarm_value ;
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_VALUE , & current_time ) ;
if ( ret < 0 ) {
dev_err ( dev , " error getting time: %d \n " , ret ) ;
return ret ;
}
if ( enabled ) {
/* Restore saved alarm if it's still in the future. */
if ( cros_ec_rtc - > saved_alarm < current_time )
alarm_offset = EC_RTC_ALARM_CLEAR ;
else
alarm_offset = cros_ec_rtc - > saved_alarm - current_time ;
ret = cros_ec_rtc_set ( cros_ec , EC_CMD_RTC_SET_ALARM ,
alarm_offset ) ;
if ( ret < 0 ) {
dev_err ( dev , " error restoring alarm: %d \n " , ret ) ;
return ret ;
}
} else {
/* Disable alarm, saving the old alarm value. */
ret = cros_ec_rtc_get ( cros_ec , EC_CMD_RTC_GET_ALARM ,
& alarm_offset ) ;
if ( ret < 0 ) {
dev_err ( dev , " error saving alarm: %d \n " , ret ) ;
return ret ;
}
alarm_value = current_time + alarm_offset ;
/*
* If the current EC alarm is already past , we don ' t want
* to set an alarm when we go through the alarm irq enable
* path .
*/
if ( alarm_value < current_time )
cros_ec_rtc - > saved_alarm = EC_RTC_ALARM_CLEAR ;
else
cros_ec_rtc - > saved_alarm = alarm_value ;
alarm_offset = EC_RTC_ALARM_CLEAR ;
ret = cros_ec_rtc_set ( cros_ec , EC_CMD_RTC_SET_ALARM ,
alarm_offset ) ;
if ( ret < 0 ) {
dev_err ( dev , " error disabling alarm: %d \n " , ret ) ;
return ret ;
}
}
return 0 ;
}
static int cros_ec_rtc_event ( struct notifier_block * nb ,
unsigned long queued_during_suspend ,
void * _notify )
{
struct cros_ec_rtc * cros_ec_rtc ;
struct rtc_device * rtc ;
struct cros_ec_device * cros_ec ;
u32 host_event ;
cros_ec_rtc = container_of ( nb , struct cros_ec_rtc , notifier ) ;
rtc = cros_ec_rtc - > rtc ;
cros_ec = cros_ec_rtc - > cros_ec ;
host_event = cros_ec_get_host_event ( cros_ec ) ;
if ( host_event & EC_HOST_EVENT_MASK ( EC_HOST_EVENT_RTC ) ) {
rtc_update_irq ( rtc , 1 , RTC_IRQF | RTC_AF ) ;
return NOTIFY_OK ;
} else {
return NOTIFY_DONE ;
}
}
static const struct rtc_class_ops cros_ec_rtc_ops = {
. read_time = cros_ec_rtc_read_time ,
. set_time = cros_ec_rtc_set_time ,
. read_alarm = cros_ec_rtc_read_alarm ,
. set_alarm = cros_ec_rtc_set_alarm ,
. alarm_irq_enable = cros_ec_rtc_alarm_irq_enable ,
} ;
# ifdef CONFIG_PM_SLEEP
static int cros_ec_rtc_suspend ( struct device * dev )
{
struct platform_device * pdev = to_platform_device ( dev ) ;
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( & pdev - > dev ) ;
if ( device_may_wakeup ( dev ) )
enable_irq_wake ( cros_ec_rtc - > cros_ec - > irq ) ;
return 0 ;
}
static int cros_ec_rtc_resume ( struct device * dev )
{
struct platform_device * pdev = to_platform_device ( dev ) ;
struct cros_ec_rtc * cros_ec_rtc = dev_get_drvdata ( & pdev - > dev ) ;
if ( device_may_wakeup ( dev ) )
disable_irq_wake ( cros_ec_rtc - > cros_ec - > irq ) ;
return 0 ;
}
# endif
static SIMPLE_DEV_PM_OPS ( cros_ec_rtc_pm_ops , cros_ec_rtc_suspend ,
cros_ec_rtc_resume ) ;
static int cros_ec_rtc_probe ( struct platform_device * pdev )
{
struct cros_ec_dev * ec_dev = dev_get_drvdata ( pdev - > dev . parent ) ;
struct cros_ec_device * cros_ec = ec_dev - > ec_dev ;
struct cros_ec_rtc * cros_ec_rtc ;
struct rtc_time tm ;
int ret ;
cros_ec_rtc = devm_kzalloc ( & pdev - > dev , sizeof ( * cros_ec_rtc ) ,
GFP_KERNEL ) ;
if ( ! cros_ec_rtc )
return - ENOMEM ;
platform_set_drvdata ( pdev , cros_ec_rtc ) ;
cros_ec_rtc - > cros_ec = cros_ec ;
/* Get initial time */
ret = cros_ec_rtc_read_time ( & pdev - > dev , & tm ) ;
if ( ret ) {
dev_err ( & pdev - > dev , " failed to read RTC time \n " ) ;
return ret ;
}
ret = device_init_wakeup ( & pdev - > dev , 1 ) ;
if ( ret ) {
dev_err ( & pdev - > dev , " failed to initialize wakeup \n " ) ;
return ret ;
}
cros_ec_rtc - > rtc = devm_rtc_device_register ( & pdev - > dev , DRV_NAME ,
& cros_ec_rtc_ops ,
THIS_MODULE ) ;
if ( IS_ERR ( cros_ec_rtc - > rtc ) ) {
ret = PTR_ERR ( cros_ec_rtc - > rtc ) ;
dev_err ( & pdev - > dev , " failed to register rtc device \n " ) ;
return ret ;
}
/* Get RTC events from the EC. */
cros_ec_rtc - > notifier . notifier_call = cros_ec_rtc_event ;
ret = blocking_notifier_chain_register ( & cros_ec - > event_notifier ,
& cros_ec_rtc - > notifier ) ;
if ( ret ) {
dev_err ( & pdev - > dev , " failed to register notifier \n " ) ;
return ret ;
}
return 0 ;
}
static int cros_ec_rtc_remove ( struct platform_device * pdev )
{
struct cros_ec_rtc * cros_ec_rtc = platform_get_drvdata ( pdev ) ;
struct device * dev = & pdev - > dev ;
int ret ;
ret = blocking_notifier_chain_unregister (
& cros_ec_rtc - > cros_ec - > event_notifier ,
& cros_ec_rtc - > notifier ) ;
if ( ret ) {
dev_err ( dev , " failed to unregister notifier \n " ) ;
return ret ;
}
return 0 ;
}
static struct platform_driver cros_ec_rtc_driver = {
. probe = cros_ec_rtc_probe ,
. remove = cros_ec_rtc_remove ,
. driver = {
. name = DRV_NAME ,
. pm = & cros_ec_rtc_pm_ops ,
} ,
} ;
module_platform_driver ( cros_ec_rtc_driver ) ;
MODULE_DESCRIPTION ( " RTC driver for Chrome OS ECs " ) ;
MODULE_AUTHOR ( " Stephen Barber <smbarber@chromium.org> " ) ;
2018-06-06 18:08:53 +03:00
MODULE_LICENSE ( " GPL v2 " ) ;
2017-11-11 00:55:53 +03:00
MODULE_ALIAS ( " platform: " DRV_NAME ) ;