2011-09-06 14:39:44 +08:00
/*
2017-02-15 14:00:26 +02:00
* Copyright 2017 NXP
2016-09-19 12:57:29 -05:00
* Copyright 2011 , 2016 Freescale Semiconductor , Inc .
2011-09-06 14:39:44 +08:00
* Copyright 2011 Linaro Ltd .
*
* The code contained herein is licensed under the GNU General Public
* License . You may obtain a copy of the GNU General Public License
* Version 2 or later at the following locations :
*
* http : //www.opensource.org/licenses/gpl-license.html
* http : //www.gnu.org/copyleft/gpl.html
*/
2016-09-19 12:57:29 -05:00
# include <linux/hrtimer.h>
2011-09-06 14:39:44 +08:00
# include <linux/init.h>
2016-09-19 12:57:29 -05:00
# include <linux/interrupt.h>
2011-09-06 14:39:44 +08:00
# include <linux/io.h>
# include <linux/module.h>
# include <linux/of.h>
# include <linux/of_address.h>
# include <linux/of_device.h>
2016-09-19 12:57:29 -05:00
# include <linux/perf_event.h>
# include <linux/slab.h>
2011-09-06 14:39:44 +08:00
2015-04-02 19:23:32 -03:00
# include "common.h"
2011-09-06 14:39:44 +08:00
# define MMDC_MAPSR 0x404
# define BP_MMDC_MAPSR_PSD 0
# define BP_MMDC_MAPSR_PSS 4
2014-09-17 11:11:45 +08:00
# define MMDC_MDMISC 0x18
# define BM_MMDC_MDMISC_DDR_TYPE 0x18
# define BP_MMDC_MDMISC_DDR_TYPE 0x3
2016-09-19 12:57:29 -05:00
# define TOTAL_CYCLES 0x0
# define BUSY_CYCLES 0x1
# define READ_ACCESSES 0x2
# define WRITE_ACCESSES 0x3
# define READ_BYTES 0x4
# define WRITE_BYTES 0x5
/* Enables, resets, freezes, overflow profiling*/
# define DBG_DIS 0x0
# define DBG_EN 0x1
# define DBG_RST 0x2
# define PRF_FRZ 0x4
# define CYC_OVF 0x8
2016-11-07 11:30:48 -06:00
# define PROFILE_SEL 0x10
2016-09-19 12:57:29 -05:00
# define MMDC_MADPCR0 0x410
2017-02-15 14:00:26 +02:00
# define MMDC_MADPCR1 0x414
2016-09-19 12:57:29 -05:00
# define MMDC_MADPSR0 0x418
# define MMDC_MADPSR1 0x41C
# define MMDC_MADPSR2 0x420
# define MMDC_MADPSR3 0x424
# define MMDC_MADPSR4 0x428
# define MMDC_MADPSR5 0x42C
# define MMDC_NUM_COUNTERS 6
2016-11-07 11:30:48 -06:00
# define MMDC_FLAG_PROFILE_SEL 0x1
2017-02-15 14:00:26 +02:00
# define MMDC_PRF_AXI_ID_CLEAR 0x0
2016-11-07 11:30:48 -06:00
2016-09-19 12:57:29 -05:00
# define to_mmdc_pmu(p) container_of(p, struct mmdc_pmu, pmu)
2014-09-17 11:11:45 +08:00
static int ddr_type ;
2016-11-07 11:30:48 -06:00
struct fsl_mmdc_devtype_data {
unsigned int flags ;
} ;
static const struct fsl_mmdc_devtype_data imx6q_data = {
} ;
static const struct fsl_mmdc_devtype_data imx6qp_data = {
. flags = MMDC_FLAG_PROFILE_SEL ,
} ;
static const struct of_device_id imx_mmdc_dt_ids [ ] = {
{ . compatible = " fsl,imx6q-mmdc " , . data = ( void * ) & imx6q_data } ,
{ . compatible = " fsl,imx6qp-mmdc " , . data = ( void * ) & imx6qp_data } ,
{ /* sentinel */ }
} ;
2016-09-19 12:57:29 -05:00
# ifdef CONFIG_PERF_EVENTS
2017-01-10 13:19:05 +01:00
static enum cpuhp_state cpuhp_mmdc_state ;
2016-09-19 12:57:29 -05:00
static DEFINE_IDA ( mmdc_ida ) ;
PMU_EVENT_ATTR_STRING ( total - cycles , mmdc_pmu_total_cycles , " event=0x00 " )
PMU_EVENT_ATTR_STRING ( busy - cycles , mmdc_pmu_busy_cycles , " event=0x01 " )
PMU_EVENT_ATTR_STRING ( read - accesses , mmdc_pmu_read_accesses , " event=0x02 " )
2017-02-15 14:00:25 +02:00
PMU_EVENT_ATTR_STRING ( write - accesses , mmdc_pmu_write_accesses , " event=0x03 " )
2016-09-19 12:57:29 -05:00
PMU_EVENT_ATTR_STRING ( read - bytes , mmdc_pmu_read_bytes , " event=0x04 " )
PMU_EVENT_ATTR_STRING ( read - bytes . unit , mmdc_pmu_read_bytes_unit , " MB " ) ;
PMU_EVENT_ATTR_STRING ( read - bytes . scale , mmdc_pmu_read_bytes_scale , " 0.000001 " ) ;
PMU_EVENT_ATTR_STRING ( write - bytes , mmdc_pmu_write_bytes , " event=0x05 " )
PMU_EVENT_ATTR_STRING ( write - bytes . unit , mmdc_pmu_write_bytes_unit , " MB " ) ;
PMU_EVENT_ATTR_STRING ( write - bytes . scale , mmdc_pmu_write_bytes_scale , " 0.000001 " ) ;
struct mmdc_pmu {
struct pmu pmu ;
void __iomem * mmdc_base ;
cpumask_t cpu ;
struct hrtimer hrtimer ;
unsigned int active_events ;
struct device * dev ;
struct perf_event * mmdc_events [ MMDC_NUM_COUNTERS ] ;
struct hlist_node node ;
2016-11-07 11:30:48 -06:00
struct fsl_mmdc_devtype_data * devtype_data ;
2016-09-19 12:57:29 -05:00
} ;
/*
* Polling period is set to one second , overflow of total - cycles ( the fastest
* increasing counter ) takes ten seconds so one second is safe
*/
static unsigned int mmdc_pmu_poll_period_us = 1000000 ;
module_param_named ( pmu_pmu_poll_period_us , mmdc_pmu_poll_period_us , uint ,
S_IRUGO | S_IWUSR ) ;
static ktime_t mmdc_pmu_timer_period ( void )
{
return ns_to_ktime ( ( u64 ) mmdc_pmu_poll_period_us * 1000 ) ;
}
static ssize_t mmdc_pmu_cpumask_show ( struct device * dev ,
struct device_attribute * attr , char * buf )
{
struct mmdc_pmu * pmu_mmdc = dev_get_drvdata ( dev ) ;
return cpumap_print_to_pagebuf ( true , buf , & pmu_mmdc - > cpu ) ;
}
static struct device_attribute mmdc_pmu_cpumask_attr =
__ATTR ( cpumask , S_IRUGO , mmdc_pmu_cpumask_show , NULL ) ;
static struct attribute * mmdc_pmu_cpumask_attrs [ ] = {
& mmdc_pmu_cpumask_attr . attr ,
NULL ,
} ;
static struct attribute_group mmdc_pmu_cpumask_attr_group = {
. attrs = mmdc_pmu_cpumask_attrs ,
} ;
static struct attribute * mmdc_pmu_events_attrs [ ] = {
& mmdc_pmu_total_cycles . attr . attr ,
& mmdc_pmu_busy_cycles . attr . attr ,
& mmdc_pmu_read_accesses . attr . attr ,
& mmdc_pmu_write_accesses . attr . attr ,
& mmdc_pmu_read_bytes . attr . attr ,
& mmdc_pmu_read_bytes_unit . attr . attr ,
& mmdc_pmu_read_bytes_scale . attr . attr ,
& mmdc_pmu_write_bytes . attr . attr ,
& mmdc_pmu_write_bytes_unit . attr . attr ,
& mmdc_pmu_write_bytes_scale . attr . attr ,
NULL ,
} ;
static struct attribute_group mmdc_pmu_events_attr_group = {
. name = " events " ,
. attrs = mmdc_pmu_events_attrs ,
} ;
PMU_FORMAT_ATTR ( event , " config:0-63 " ) ;
2017-02-15 14:00:26 +02:00
PMU_FORMAT_ATTR ( axi_id , " config1:0-63 " ) ;
2016-09-19 12:57:29 -05:00
static struct attribute * mmdc_pmu_format_attrs [ ] = {
& format_attr_event . attr ,
2017-02-15 14:00:26 +02:00
& format_attr_axi_id . attr ,
2016-09-19 12:57:29 -05:00
NULL ,
} ;
static struct attribute_group mmdc_pmu_format_attr_group = {
. name = " format " ,
. attrs = mmdc_pmu_format_attrs ,
} ;
static const struct attribute_group * attr_groups [ ] = {
& mmdc_pmu_events_attr_group ,
& mmdc_pmu_format_attr_group ,
& mmdc_pmu_cpumask_attr_group ,
NULL ,
} ;
static u32 mmdc_pmu_read_counter ( struct mmdc_pmu * pmu_mmdc , int cfg )
{
void __iomem * mmdc_base , * reg ;
mmdc_base = pmu_mmdc - > mmdc_base ;
switch ( cfg ) {
case TOTAL_CYCLES :
reg = mmdc_base + MMDC_MADPSR0 ;
break ;
case BUSY_CYCLES :
reg = mmdc_base + MMDC_MADPSR1 ;
break ;
case READ_ACCESSES :
reg = mmdc_base + MMDC_MADPSR2 ;
break ;
case WRITE_ACCESSES :
reg = mmdc_base + MMDC_MADPSR3 ;
break ;
case READ_BYTES :
reg = mmdc_base + MMDC_MADPSR4 ;
break ;
case WRITE_BYTES :
reg = mmdc_base + MMDC_MADPSR5 ;
break ;
default :
return WARN_ONCE ( 1 ,
" invalid configuration %d for mmdc counter " , cfg ) ;
}
return readl ( reg ) ;
}
static int mmdc_pmu_offline_cpu ( unsigned int cpu , struct hlist_node * node )
{
struct mmdc_pmu * pmu_mmdc = hlist_entry_safe ( node , struct mmdc_pmu , node ) ;
int target ;
if ( ! cpumask_test_and_clear_cpu ( cpu , & pmu_mmdc - > cpu ) )
return 0 ;
target = cpumask_any_but ( cpu_online_mask , cpu ) ;
if ( target > = nr_cpu_ids )
return 0 ;
perf_pmu_migrate_context ( & pmu_mmdc - > pmu , cpu , target ) ;
cpumask_set_cpu ( target , & pmu_mmdc - > cpu ) ;
return 0 ;
}
static bool mmdc_pmu_group_event_is_valid ( struct perf_event * event ,
struct pmu * pmu ,
unsigned long * used_counters )
{
int cfg = event - > attr . config ;
if ( is_software_event ( event ) )
return true ;
if ( event - > pmu ! = pmu )
return false ;
return ! test_and_set_bit ( cfg , used_counters ) ;
}
/*
* Each event has a single fixed - purpose counter , so we can only have a
* single active event for each at any point in time . Here we just check
* for duplicates , and rely on mmdc_pmu_event_init to verify that the HW
* event numbers are valid .
*/
static bool mmdc_pmu_group_is_valid ( struct perf_event * event )
{
struct pmu * pmu = event - > pmu ;
struct perf_event * leader = event - > group_leader ;
struct perf_event * sibling ;
unsigned long counter_mask = 0 ;
set_bit ( leader - > attr . config , & counter_mask ) ;
if ( event ! = leader ) {
if ( ! mmdc_pmu_group_event_is_valid ( event , pmu , & counter_mask ) )
return false ;
}
list_for_each_entry ( sibling , & leader - > sibling_list , group_entry ) {
if ( ! mmdc_pmu_group_event_is_valid ( sibling , pmu , & counter_mask ) )
return false ;
}
return true ;
}
static int mmdc_pmu_event_init ( struct perf_event * event )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
int cfg = event - > attr . config ;
if ( event - > attr . type ! = event - > pmu - > type )
return - ENOENT ;
if ( is_sampling_event ( event ) | | event - > attach_state & PERF_ATTACH_TASK )
return - EOPNOTSUPP ;
if ( event - > cpu < 0 ) {
dev_warn ( pmu_mmdc - > dev , " Can't provide per-task data! \n " ) ;
return - EOPNOTSUPP ;
}
if ( event - > attr . exclude_user | |
event - > attr . exclude_kernel | |
event - > attr . exclude_hv | |
event - > attr . exclude_idle | |
event - > attr . exclude_host | |
event - > attr . exclude_guest | |
event - > attr . sample_period )
return - EINVAL ;
if ( cfg < 0 | | cfg > = MMDC_NUM_COUNTERS )
return - EINVAL ;
if ( ! mmdc_pmu_group_is_valid ( event ) )
return - EINVAL ;
event - > cpu = cpumask_first ( & pmu_mmdc - > cpu ) ;
return 0 ;
}
static void mmdc_pmu_event_update ( struct perf_event * event )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
struct hw_perf_event * hwc = & event - > hw ;
u64 delta , prev_raw_count , new_raw_count ;
do {
prev_raw_count = local64_read ( & hwc - > prev_count ) ;
new_raw_count = mmdc_pmu_read_counter ( pmu_mmdc ,
event - > attr . config ) ;
} while ( local64_cmpxchg ( & hwc - > prev_count , prev_raw_count ,
new_raw_count ) ! = prev_raw_count ) ;
delta = ( new_raw_count - prev_raw_count ) & 0xFFFFFFFF ;
local64_add ( delta , & event - > count ) ;
}
static void mmdc_pmu_event_start ( struct perf_event * event , int flags )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
struct hw_perf_event * hwc = & event - > hw ;
void __iomem * mmdc_base , * reg ;
2016-11-07 11:30:48 -06:00
u32 val ;
2016-09-19 12:57:29 -05:00
mmdc_base = pmu_mmdc - > mmdc_base ;
reg = mmdc_base + MMDC_MADPCR0 ;
/*
* hrtimer is required because mmdc does not provide an interrupt so
* polling is necessary
*/
hrtimer_start ( & pmu_mmdc - > hrtimer , mmdc_pmu_timer_period ( ) ,
HRTIMER_MODE_REL_PINNED ) ;
local64_set ( & hwc - > prev_count , 0 ) ;
writel ( DBG_RST , reg ) ;
2016-11-07 11:30:48 -06:00
2017-02-15 14:00:26 +02:00
/*
* Write the AXI id parameter to MADPCR1 .
*/
val = event - > attr . config1 ;
reg = mmdc_base + MMDC_MADPCR1 ;
writel ( val , reg ) ;
reg = mmdc_base + MMDC_MADPCR0 ;
2016-11-07 11:30:48 -06:00
val = DBG_EN ;
if ( pmu_mmdc - > devtype_data - > flags & MMDC_FLAG_PROFILE_SEL )
val | = PROFILE_SEL ;
writel ( val , reg ) ;
2016-09-19 12:57:29 -05:00
}
static int mmdc_pmu_event_add ( struct perf_event * event , int flags )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
struct hw_perf_event * hwc = & event - > hw ;
int cfg = event - > attr . config ;
if ( flags & PERF_EF_START )
mmdc_pmu_event_start ( event , flags ) ;
if ( pmu_mmdc - > mmdc_events [ cfg ] ! = NULL )
return - EAGAIN ;
pmu_mmdc - > mmdc_events [ cfg ] = event ;
pmu_mmdc - > active_events + + ;
local64_set ( & hwc - > prev_count , mmdc_pmu_read_counter ( pmu_mmdc , cfg ) ) ;
return 0 ;
}
static void mmdc_pmu_event_stop ( struct perf_event * event , int flags )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
void __iomem * mmdc_base , * reg ;
mmdc_base = pmu_mmdc - > mmdc_base ;
reg = mmdc_base + MMDC_MADPCR0 ;
writel ( PRF_FRZ , reg ) ;
2017-02-15 14:00:26 +02:00
reg = mmdc_base + MMDC_MADPCR1 ;
writel ( MMDC_PRF_AXI_ID_CLEAR , reg ) ;
2016-09-19 12:57:29 -05:00
mmdc_pmu_event_update ( event ) ;
}
static void mmdc_pmu_event_del ( struct perf_event * event , int flags )
{
struct mmdc_pmu * pmu_mmdc = to_mmdc_pmu ( event - > pmu ) ;
int cfg = event - > attr . config ;
pmu_mmdc - > mmdc_events [ cfg ] = NULL ;
pmu_mmdc - > active_events - - ;
if ( pmu_mmdc - > active_events = = 0 )
hrtimer_cancel ( & pmu_mmdc - > hrtimer ) ;
mmdc_pmu_event_stop ( event , PERF_EF_UPDATE ) ;
}
static void mmdc_pmu_overflow_handler ( struct mmdc_pmu * pmu_mmdc )
{
int i ;
for ( i = 0 ; i < MMDC_NUM_COUNTERS ; i + + ) {
struct perf_event * event = pmu_mmdc - > mmdc_events [ i ] ;
if ( event )
mmdc_pmu_event_update ( event ) ;
}
}
static enum hrtimer_restart mmdc_pmu_timer_handler ( struct hrtimer * hrtimer )
{
struct mmdc_pmu * pmu_mmdc = container_of ( hrtimer , struct mmdc_pmu ,
hrtimer ) ;
mmdc_pmu_overflow_handler ( pmu_mmdc ) ;
hrtimer_forward_now ( hrtimer , mmdc_pmu_timer_period ( ) ) ;
return HRTIMER_RESTART ;
}
static int mmdc_pmu_init ( struct mmdc_pmu * pmu_mmdc ,
void __iomem * mmdc_base , struct device * dev )
{
int mmdc_num ;
* pmu_mmdc = ( struct mmdc_pmu ) {
. pmu = ( struct pmu ) {
. task_ctx_nr = perf_invalid_context ,
. attr_groups = attr_groups ,
. event_init = mmdc_pmu_event_init ,
. add = mmdc_pmu_event_add ,
. del = mmdc_pmu_event_del ,
. start = mmdc_pmu_event_start ,
. stop = mmdc_pmu_event_stop ,
. read = mmdc_pmu_event_update ,
} ,
. mmdc_base = mmdc_base ,
. dev = dev ,
. active_events = 0 ,
} ;
mmdc_num = ida_simple_get ( & mmdc_ida , 0 , 0 , GFP_KERNEL ) ;
return mmdc_num ;
}
static int imx_mmdc_remove ( struct platform_device * pdev )
{
struct mmdc_pmu * pmu_mmdc = platform_get_drvdata ( pdev ) ;
2016-12-21 20:19:48 +01:00
cpuhp_state_remove_instance_nocalls ( cpuhp_mmdc_state , & pmu_mmdc - > node ) ;
2016-09-19 12:57:29 -05:00
perf_pmu_unregister ( & pmu_mmdc - > pmu ) ;
kfree ( pmu_mmdc ) ;
return 0 ;
}
static int imx_mmdc_perf_init ( struct platform_device * pdev , void __iomem * mmdc_base )
{
struct mmdc_pmu * pmu_mmdc ;
char * name ;
int mmdc_num ;
int ret ;
2016-11-07 11:30:48 -06:00
const struct of_device_id * of_id =
of_match_device ( imx_mmdc_dt_ids , & pdev - > dev ) ;
2016-09-19 12:57:29 -05:00
pmu_mmdc = kzalloc ( sizeof ( * pmu_mmdc ) , GFP_KERNEL ) ;
if ( ! pmu_mmdc ) {
pr_err ( " failed to allocate PMU device! \n " ) ;
return - ENOMEM ;
}
2016-12-21 20:19:48 +01:00
/* The first instance registers the hotplug state */
if ( ! cpuhp_mmdc_state ) {
ret = cpuhp_setup_state_multi ( CPUHP_AP_ONLINE_DYN ,
" perf/arm/mmdc:online " , NULL ,
mmdc_pmu_offline_cpu ) ;
if ( ret < 0 ) {
pr_err ( " cpuhp_setup_state_multi failed \n " ) ;
goto pmu_free ;
}
cpuhp_mmdc_state = ret ;
}
2016-09-19 12:57:29 -05:00
mmdc_num = mmdc_pmu_init ( pmu_mmdc , mmdc_base , & pdev - > dev ) ;
if ( mmdc_num = = 0 )
name = " mmdc " ;
else
name = devm_kasprintf ( & pdev - > dev ,
GFP_KERNEL , " mmdc%d " , mmdc_num ) ;
2016-11-07 11:30:48 -06:00
pmu_mmdc - > devtype_data = ( struct fsl_mmdc_devtype_data * ) of_id - > data ;
2016-09-19 12:57:29 -05:00
hrtimer_init ( & pmu_mmdc - > hrtimer , CLOCK_MONOTONIC ,
HRTIMER_MODE_REL ) ;
pmu_mmdc - > hrtimer . function = mmdc_pmu_timer_handler ;
2016-12-21 20:19:48 +01:00
cpumask_set_cpu ( raw_smp_processor_id ( ) , & pmu_mmdc - > cpu ) ;
/* Register the pmu instance for cpu hotplug */
cpuhp_state_add_instance_nocalls ( cpuhp_mmdc_state , & pmu_mmdc - > node ) ;
2016-09-19 12:57:29 -05:00
ret = perf_pmu_register ( & ( pmu_mmdc - > pmu ) , name , - 1 ) ;
if ( ret )
goto pmu_register_err ;
2016-12-21 20:19:48 +01:00
platform_set_drvdata ( pdev , pmu_mmdc ) ;
2016-09-19 12:57:29 -05:00
return 0 ;
pmu_register_err :
pr_warn ( " MMDC Perf PMU failed (%d), disabled \n " , ret ) ;
2016-12-21 20:19:48 +01:00
cpuhp_state_remove_instance_nocalls ( cpuhp_mmdc_state , & pmu_mmdc - > node ) ;
2016-09-19 12:57:29 -05:00
hrtimer_cancel ( & pmu_mmdc - > hrtimer ) ;
2016-12-21 20:19:48 +01:00
pmu_free :
2016-09-19 12:57:29 -05:00
kfree ( pmu_mmdc ) ;
return ret ;
}
# else
# define imx_mmdc_remove NULL
# define imx_mmdc_perf_init(pdev, mmdc_base) 0
# endif
2012-12-21 14:02:24 -08:00
static int imx_mmdc_probe ( struct platform_device * pdev )
2011-09-06 14:39:44 +08:00
{
struct device_node * np = pdev - > dev . of_node ;
void __iomem * mmdc_base , * reg ;
u32 val ;
int timeout = 0x400 ;
mmdc_base = of_iomap ( np , 0 ) ;
WARN_ON ( ! mmdc_base ) ;
2014-09-17 11:11:45 +08:00
reg = mmdc_base + MMDC_MDMISC ;
/* Get ddr type */
val = readl_relaxed ( reg ) ;
ddr_type = ( val & BM_MMDC_MDMISC_DDR_TYPE ) > >
BP_MMDC_MDMISC_DDR_TYPE ;
2011-09-06 14:39:44 +08:00
reg = mmdc_base + MMDC_MAPSR ;
/* Enable automatic power saving */
val = readl_relaxed ( reg ) ;
val & = ~ ( 1 < < BP_MMDC_MAPSR_PSD ) ;
writel_relaxed ( val , reg ) ;
/* Ensure it's successfully enabled */
while ( ! ( readl_relaxed ( reg ) & 1 < < BP_MMDC_MAPSR_PSS ) & & - - timeout )
cpu_relax ( ) ;
if ( unlikely ( ! timeout ) ) {
pr_warn ( " %s: failed to enable automatic power saving \n " ,
__func__ ) ;
return - EBUSY ;
}
2016-09-19 12:57:29 -05:00
return imx_mmdc_perf_init ( pdev , mmdc_base ) ;
2011-09-06 14:39:44 +08:00
}
2014-09-17 11:11:45 +08:00
int imx_mmdc_get_ddr_type ( void )
{
return ddr_type ;
}
2011-09-06 14:39:44 +08:00
static struct platform_driver imx_mmdc_driver = {
. driver = {
. name = " imx-mmdc " ,
. of_match_table = imx_mmdc_dt_ids ,
} ,
. probe = imx_mmdc_probe ,
2016-09-19 12:57:29 -05:00
. remove = imx_mmdc_remove ,
2011-09-06 14:39:44 +08:00
} ;
static int __init imx_mmdc_init ( void )
{
return platform_driver_register ( & imx_mmdc_driver ) ;
}
postcore_initcall ( imx_mmdc_init ) ;