2019-11-23 00:45:03 +03:00
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright 2019 NXP
*/
# include <linux/module.h>
# include <linux/device.h>
# include <linux/of_device.h>
# include <linux/platform_device.h>
# include <linux/devfreq.h>
# include <linux/pm_opp.h>
# include <linux/clk.h>
# include <linux/clk-provider.h>
# include <linux/arm-smccc.h>
# define IMX_SIP_DDR_DVFS 0xc2000004
/* Query available frequencies. */
# define IMX_SIP_DDR_DVFS_GET_FREQ_COUNT 0x10
# define IMX_SIP_DDR_DVFS_GET_FREQ_INFO 0x11
/*
* This should be in a 1 : 1 mapping with devicetree OPPs but
* firmware provides additional info .
*/
struct imx8m_ddrc_freq {
unsigned long rate ;
unsigned long smcarg ;
int dram_core_parent_index ;
int dram_alt_parent_index ;
int dram_apb_parent_index ;
} ;
/* Hardware limitation */
# define IMX8M_DDRC_MAX_FREQ_COUNT 4
/*
* i . MX8M DRAM Controller clocks have the following structure ( abridged ) :
*
* + - - - - - - - - - - + | \ + - - - - - - +
* | dram_pll | - - - - - - - | M | dram_core | |
* + - - - - - - - - - - + | U | - - - - - - - - - - > | D |
* / - - | X | | D |
* dram_alt_root | | / | R |
* | | C |
* + - - - - - - - - - + | |
* | FIX DIV / 4 | | |
* + - - - - - - - - - + | |
* composite : | | |
* + - - - - - - - - - - + | | |
* | dram_alt | - - - - / | |
* + - - - - - - - - - - + | |
* | dram_apb | - - - - - - - - - - - - - - - - - - - - > | |
* + - - - - - - - - - - + + - - - - - - +
*
* The dram_pll is used for higher rates and dram_alt is used for lower rates .
*
* Frequency switching is implemented in TF - A ( via SMC call ) and can change the
* configuration of the clocks , including mux parents . The dram_alt and
* dram_apb clocks are " imx composite " and their parent can change too .
*
* We need to prepare / enable the new mux parents head of switching and update
* their information afterwards .
*/
struct imx8m_ddrc {
struct devfreq_dev_profile profile ;
struct devfreq * devfreq ;
/* For frequency switching: */
struct clk * dram_core ;
struct clk * dram_pll ;
struct clk * dram_alt ;
struct clk * dram_apb ;
int freq_count ;
struct imx8m_ddrc_freq freq_table [ IMX8M_DDRC_MAX_FREQ_COUNT ] ;
} ;
static struct imx8m_ddrc_freq * imx8m_ddrc_find_freq ( struct imx8m_ddrc * priv ,
unsigned long rate )
{
struct imx8m_ddrc_freq * freq ;
int i ;
/*
* Firmware reports values in MT / s , so we round - down from Hz
* Rounding is extra generous to ensure a match .
*/
rate = DIV_ROUND_CLOSEST ( rate , 250000 ) ;
for ( i = 0 ; i < priv - > freq_count ; + + i ) {
freq = & priv - > freq_table [ i ] ;
if ( freq - > rate = = rate | |
freq - > rate + 1 = = rate | |
freq - > rate - 1 = = rate )
return freq ;
}
return NULL ;
}
static void imx8m_ddrc_smc_set_freq ( int target_freq )
{
struct arm_smccc_res res ;
u32 online_cpus = 0 ;
int cpu ;
local_irq_disable ( ) ;
for_each_online_cpu ( cpu )
online_cpus | = ( 1 < < ( cpu * 8 ) ) ;
/* change the ddr freqency */
arm_smccc_smc ( IMX_SIP_DDR_DVFS , target_freq , online_cpus ,
0 , 0 , 0 , 0 , 0 , & res ) ;
local_irq_enable ( ) ;
}
static struct clk * clk_get_parent_by_index ( struct clk * clk , int index )
{
struct clk_hw * hw ;
hw = clk_hw_get_parent_by_index ( __clk_get_hw ( clk ) , index ) ;
return hw ? hw - > clk : NULL ;
}
static int imx8m_ddrc_set_freq ( struct device * dev , struct imx8m_ddrc_freq * freq )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
struct clk * new_dram_core_parent ;
struct clk * new_dram_alt_parent ;
struct clk * new_dram_apb_parent ;
int ret ;
/*
* Fetch new parents
*
* new_dram_alt_parent and new_dram_apb_parent are optional but
* new_dram_core_parent is not .
*/
new_dram_core_parent = clk_get_parent_by_index (
priv - > dram_core , freq - > dram_core_parent_index - 1 ) ;
if ( ! new_dram_core_parent ) {
dev_err ( dev , " failed to fetch new dram_core parent \n " ) ;
return - EINVAL ;
}
if ( freq - > dram_alt_parent_index ) {
new_dram_alt_parent = clk_get_parent_by_index (
priv - > dram_alt ,
freq - > dram_alt_parent_index - 1 ) ;
if ( ! new_dram_alt_parent ) {
dev_err ( dev , " failed to fetch new dram_alt parent \n " ) ;
return - EINVAL ;
}
} else
new_dram_alt_parent = NULL ;
if ( freq - > dram_apb_parent_index ) {
new_dram_apb_parent = clk_get_parent_by_index (
priv - > dram_apb ,
freq - > dram_apb_parent_index - 1 ) ;
if ( ! new_dram_apb_parent ) {
dev_err ( dev , " failed to fetch new dram_apb parent \n " ) ;
return - EINVAL ;
}
} else
new_dram_apb_parent = NULL ;
/* increase reference counts and ensure clks are ON before switch */
ret = clk_prepare_enable ( new_dram_core_parent ) ;
if ( ret ) {
dev_err ( dev , " failed to enable new dram_core parent: %d \n " ,
ret ) ;
goto out ;
}
ret = clk_prepare_enable ( new_dram_alt_parent ) ;
if ( ret ) {
dev_err ( dev , " failed to enable new dram_alt parent: %d \n " ,
ret ) ;
goto out_disable_core_parent ;
}
ret = clk_prepare_enable ( new_dram_apb_parent ) ;
if ( ret ) {
dev_err ( dev , " failed to enable new dram_apb parent: %d \n " ,
ret ) ;
goto out_disable_alt_parent ;
}
imx8m_ddrc_smc_set_freq ( freq - > smcarg ) ;
/* update parents in clk tree after switch. */
ret = clk_set_parent ( priv - > dram_core , new_dram_core_parent ) ;
if ( ret )
dev_warn ( dev , " failed to set dram_core parent: %d \n " , ret ) ;
if ( new_dram_alt_parent ) {
ret = clk_set_parent ( priv - > dram_alt , new_dram_alt_parent ) ;
if ( ret )
dev_warn ( dev , " failed to set dram_alt parent: %d \n " ,
ret ) ;
}
if ( new_dram_apb_parent ) {
ret = clk_set_parent ( priv - > dram_apb , new_dram_apb_parent ) ;
if ( ret )
dev_warn ( dev , " failed to set dram_apb parent: %d \n " ,
ret ) ;
}
/*
* Explicitly refresh dram PLL rate .
*
* Even if it ' s marked with CLK_GET_RATE_NOCACHE the rate will not be
* automatically refreshed when clk_get_rate is called on children .
*/
clk_get_rate ( priv - > dram_pll ) ;
/*
* clk_set_parent transfer the reference count from old parent .
* now we drop extra reference counts used during the switch
*/
clk_disable_unprepare ( new_dram_apb_parent ) ;
out_disable_alt_parent :
clk_disable_unprepare ( new_dram_alt_parent ) ;
out_disable_core_parent :
clk_disable_unprepare ( new_dram_core_parent ) ;
out :
return ret ;
}
static int imx8m_ddrc_target ( struct device * dev , unsigned long * freq , u32 flags )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
struct imx8m_ddrc_freq * freq_info ;
struct dev_pm_opp * new_opp ;
unsigned long old_freq , new_freq ;
int ret ;
new_opp = devfreq_recommended_opp ( dev , freq , flags ) ;
if ( IS_ERR ( new_opp ) ) {
ret = PTR_ERR ( new_opp ) ;
dev_err ( dev , " failed to get recommended opp: %d \n " , ret ) ;
return ret ;
}
dev_pm_opp_put ( new_opp ) ;
old_freq = clk_get_rate ( priv - > dram_core ) ;
if ( * freq = = old_freq )
return 0 ;
freq_info = imx8m_ddrc_find_freq ( priv , * freq ) ;
if ( ! freq_info )
return - EINVAL ;
/*
* Read back the clk rate to verify switch was correct and so that
* we can report it on all error paths .
*/
ret = imx8m_ddrc_set_freq ( dev , freq_info ) ;
new_freq = clk_get_rate ( priv - > dram_core ) ;
if ( ret )
dev_err ( dev , " ddrc failed freq switch to %lu from %lu: error %d. now at %lu \n " ,
* freq , old_freq , ret , new_freq ) ;
else if ( * freq ! = new_freq )
dev_err ( dev , " ddrc failed freq update to %lu from %lu, now at %lu \n " ,
* freq , old_freq , new_freq ) ;
else
dev_dbg ( dev , " ddrc freq set to %lu (was %lu) \n " ,
* freq , old_freq ) ;
return ret ;
}
static int imx8m_ddrc_get_cur_freq ( struct device * dev , unsigned long * freq )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
* freq = clk_get_rate ( priv - > dram_core ) ;
return 0 ;
}
static int imx8m_ddrc_get_dev_status ( struct device * dev ,
struct devfreq_dev_status * stat )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
stat - > busy_time = 0 ;
stat - > total_time = 0 ;
stat - > current_frequency = clk_get_rate ( priv - > dram_core ) ;
return 0 ;
}
static int imx8m_ddrc_init_freq_info ( struct device * dev )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
struct arm_smccc_res res ;
int index ;
/* An error here means DDR DVFS API not supported by firmware */
arm_smccc_smc ( IMX_SIP_DDR_DVFS , IMX_SIP_DDR_DVFS_GET_FREQ_COUNT ,
0 , 0 , 0 , 0 , 0 , 0 , & res ) ;
priv - > freq_count = res . a0 ;
if ( priv - > freq_count < = 0 | |
priv - > freq_count > IMX8M_DDRC_MAX_FREQ_COUNT )
return - ENODEV ;
for ( index = 0 ; index < priv - > freq_count ; + + index ) {
struct imx8m_ddrc_freq * freq = & priv - > freq_table [ index ] ;
arm_smccc_smc ( IMX_SIP_DDR_DVFS , IMX_SIP_DDR_DVFS_GET_FREQ_INFO ,
index , 0 , 0 , 0 , 0 , 0 , & res ) ;
/* Result should be strictly positive */
if ( ( long ) res . a0 < = 0 )
return - ENODEV ;
freq - > rate = res . a0 ;
freq - > smcarg = index ;
freq - > dram_core_parent_index = res . a1 ;
freq - > dram_alt_parent_index = res . a2 ;
freq - > dram_apb_parent_index = res . a3 ;
/* dram_core has 2 options: dram_pll or dram_alt_root */
if ( freq - > dram_core_parent_index ! = 1 & &
freq - > dram_core_parent_index ! = 2 )
return - ENODEV ;
/* dram_apb and dram_alt have exactly 8 possible parents */
if ( freq - > dram_alt_parent_index > 8 | |
freq - > dram_apb_parent_index > 8 )
return - ENODEV ;
/* dram_core from alt requires explicit dram_alt parent */
if ( freq - > dram_core_parent_index = = 2 & &
freq - > dram_alt_parent_index = = 0 )
return - ENODEV ;
}
return 0 ;
}
static int imx8m_ddrc_check_opps ( struct device * dev )
{
struct imx8m_ddrc * priv = dev_get_drvdata ( dev ) ;
struct imx8m_ddrc_freq * freq_info ;
struct dev_pm_opp * opp ;
unsigned long freq ;
int i , opp_count ;
/* Enumerate DT OPPs and disable those not supported by firmware */
opp_count = dev_pm_opp_get_opp_count ( dev ) ;
if ( opp_count < 0 )
return opp_count ;
for ( i = 0 , freq = 0 ; i < opp_count ; + + i , + + freq ) {
opp = dev_pm_opp_find_freq_ceil ( dev , & freq ) ;
if ( IS_ERR ( opp ) ) {
dev_err ( dev , " Failed enumerating OPPs: %ld \n " ,
PTR_ERR ( opp ) ) ;
return PTR_ERR ( opp ) ;
}
dev_pm_opp_put ( opp ) ;
freq_info = imx8m_ddrc_find_freq ( priv , freq ) ;
if ( ! freq_info ) {
dev_info ( dev , " Disable unsupported OPP %luHz %luMT/s \n " ,
freq , DIV_ROUND_CLOSEST ( freq , 250000 ) ) ;
dev_pm_opp_disable ( dev , freq ) ;
}
}
return 0 ;
}
static void imx8m_ddrc_exit ( struct device * dev )
{
dev_pm_opp_of_remove_table ( dev ) ;
}
static int imx8m_ddrc_probe ( struct platform_device * pdev )
{
struct device * dev = & pdev - > dev ;
struct imx8m_ddrc * priv ;
const char * gov = DEVFREQ_GOV_USERSPACE ;
int ret ;
priv = devm_kzalloc ( dev , sizeof ( * priv ) , GFP_KERNEL ) ;
if ( ! priv )
return - ENOMEM ;
platform_set_drvdata ( pdev , priv ) ;
ret = imx8m_ddrc_init_freq_info ( dev ) ;
if ( ret ) {
dev_err ( dev , " failed to init firmware freq info: %d \n " , ret ) ;
return ret ;
}
priv - > dram_core = devm_clk_get ( dev , " core " ) ;
2019-12-30 11:47:31 +03:00
if ( IS_ERR ( priv - > dram_core ) ) {
ret = PTR_ERR ( priv - > dram_core ) ;
dev_err ( dev , " failed to fetch core clock: %d \n " , ret ) ;
return ret ;
}
2019-11-23 00:45:03 +03:00
priv - > dram_pll = devm_clk_get ( dev , " pll " ) ;
2019-12-30 11:47:31 +03:00
if ( IS_ERR ( priv - > dram_pll ) ) {
ret = PTR_ERR ( priv - > dram_pll ) ;
dev_err ( dev , " failed to fetch pll clock: %d \n " , ret ) ;
return ret ;
}
2019-11-23 00:45:03 +03:00
priv - > dram_alt = devm_clk_get ( dev , " alt " ) ;
2019-12-30 11:47:31 +03:00
if ( IS_ERR ( priv - > dram_alt ) ) {
ret = PTR_ERR ( priv - > dram_alt ) ;
dev_err ( dev , " failed to fetch alt clock: %d \n " , ret ) ;
return ret ;
}
2019-11-23 00:45:03 +03:00
priv - > dram_apb = devm_clk_get ( dev , " apb " ) ;
2019-12-30 11:47:31 +03:00
if ( IS_ERR ( priv - > dram_apb ) ) {
ret = PTR_ERR ( priv - > dram_apb ) ;
dev_err ( dev , " failed to fetch apb clock: %d \n " , ret ) ;
2019-11-23 00:45:03 +03:00
return ret ;
}
ret = dev_pm_opp_of_add_table ( dev ) ;
if ( ret < 0 ) {
dev_err ( dev , " failed to get OPP table \n " ) ;
return ret ;
}
ret = imx8m_ddrc_check_opps ( dev ) ;
if ( ret < 0 )
goto err ;
priv - > profile . polling_ms = 1000 ;
priv - > profile . target = imx8m_ddrc_target ;
priv - > profile . get_dev_status = imx8m_ddrc_get_dev_status ;
priv - > profile . exit = imx8m_ddrc_exit ;
priv - > profile . get_cur_freq = imx8m_ddrc_get_cur_freq ;
priv - > profile . initial_freq = clk_get_rate ( priv - > dram_core ) ;
priv - > devfreq = devm_devfreq_add_device ( dev , & priv - > profile ,
gov , NULL ) ;
if ( IS_ERR ( priv - > devfreq ) ) {
ret = PTR_ERR ( priv - > devfreq ) ;
dev_err ( dev , " failed to add devfreq device: %d \n " , ret ) ;
goto err ;
}
return 0 ;
err :
dev_pm_opp_of_remove_table ( dev ) ;
return ret ;
}
static const struct of_device_id imx8m_ddrc_of_match [ ] = {
{ . compatible = " fsl,imx8m-ddrc " , } ,
{ /* sentinel */ } ,
} ;
MODULE_DEVICE_TABLE ( of , imx8m_ddrc_of_match ) ;
static struct platform_driver imx8m_ddrc_platdrv = {
. probe = imx8m_ddrc_probe ,
. driver = {
. name = " imx8m-ddrc-devfreq " ,
. of_match_table = of_match_ptr ( imx8m_ddrc_of_match ) ,
} ,
} ;
module_platform_driver ( imx8m_ddrc_platdrv ) ;
MODULE_DESCRIPTION ( " i.MX8M DDR Controller frequency driver " ) ;
MODULE_AUTHOR ( " Leonard Crestez <leonard.crestez@nxp.com> " ) ;
MODULE_LICENSE ( " GPL v2 " ) ;