2012-05-11 18:25:46 +04:00
/*
* core . c - ChipIdea USB IP core family device controller
*
* Copyright ( C ) 2008 Chipidea - MIPS Technologies , Inc . All rights reserved .
*
* Author : David Lopo
*
* 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 .
*/
/*
* Description : ChipIdea USB IP core family device controller
*
* This driver is composed of several blocks :
* - HW : hardware interface
* - DBG : debug facilities ( optional )
* - UTIL : utilities
* - ISR : interrupts handling
* - ENDPT : endpoint operations ( Gadget API )
* - GADGET : gadget operations ( Gadget API )
* - BUS : bus glue code , bus abstraction layer
*
* Compile Options
* - CONFIG_USB_GADGET_DEBUG_FILES : enable debug facilities
* - STALL_IN : non - empty bulk - in pipes cannot be halted
* if defined mass storage compliance succeeds but with warnings
* = > case 4 : Hi > Dn
* = > case 5 : Hi > Di
* = > case 8 : Hi < > Do
* if undefined usbtest 13 fails
* - TRACE : enable function tracing ( depends on DEBUG )
*
* Main Features
* - Chapter 9 & Mass Storage Compliance with Gadget File Storage
* - Chapter 9 Compliance with Gadget Zero ( STALL_IN undefined )
* - Normal & LPM support
*
* USBTEST Report
* - OK : 0 - 12 , 13 ( STALL_IN defined ) & 14
* - Not Supported : 15 & 16 ( ISO )
*
* TODO List
* - OTG
* - Isochronous & Interrupt Traffic
* - Handle requests which spawns into several TDs
* - GET_STATUS ( device ) - always reports 0
* - Gadget API ( majority of optional features )
* - Suspend & Remote Wakeup
*/
# include <linux/delay.h>
# include <linux/device.h>
# include <linux/dmapool.h>
# include <linux/dma-mapping.h>
# include <linux/init.h>
# include <linux/platform_device.h>
# include <linux/module.h>
2012-07-07 18:56:42 +04:00
# include <linux/idr.h>
2012-05-11 18:25:46 +04:00
# include <linux/interrupt.h>
# include <linux/io.h>
# include <linux/irq.h>
# include <linux/kernel.h>
# include <linux/slab.h>
# include <linux/pm_runtime.h>
# include <linux/usb/ch9.h>
# include <linux/usb/gadget.h>
# include <linux/usb/otg.h>
# include <linux/usb/chipidea.h>
# include "ci.h"
# include "udc.h"
# include "bits.h"
2012-05-11 18:25:54 +04:00
# include "host.h"
2012-05-11 18:25:46 +04:00
# include "debug.h"
2012-05-11 18:25:47 +04:00
/* Controller register map */
2012-05-11 18:25:46 +04:00
static uintptr_t ci_regs_nolpm [ ] = {
[ CAP_CAPLENGTH ] = 0x000UL ,
[ CAP_HCCPARAMS ] = 0x008UL ,
[ CAP_DCCPARAMS ] = 0x024UL ,
[ CAP_TESTMODE ] = 0x038UL ,
[ OP_USBCMD ] = 0x000UL ,
[ OP_USBSTS ] = 0x004UL ,
[ OP_USBINTR ] = 0x008UL ,
[ OP_DEVICEADDR ] = 0x014UL ,
[ OP_ENDPTLISTADDR ] = 0x018UL ,
[ OP_PORTSC ] = 0x044UL ,
[ OP_DEVLC ] = 0x084UL ,
2012-05-11 18:25:47 +04:00
[ OP_OTGSC ] = 0x064UL ,
2012-05-11 18:25:46 +04:00
[ OP_USBMODE ] = 0x068UL ,
[ OP_ENDPTSETUPSTAT ] = 0x06CUL ,
[ OP_ENDPTPRIME ] = 0x070UL ,
[ OP_ENDPTFLUSH ] = 0x074UL ,
[ OP_ENDPTSTAT ] = 0x078UL ,
[ OP_ENDPTCOMPLETE ] = 0x07CUL ,
[ OP_ENDPTCTRL ] = 0x080UL ,
} ;
static uintptr_t ci_regs_lpm [ ] = {
[ CAP_CAPLENGTH ] = 0x000UL ,
[ CAP_HCCPARAMS ] = 0x008UL ,
[ CAP_DCCPARAMS ] = 0x024UL ,
[ CAP_TESTMODE ] = 0x0FCUL ,
[ OP_USBCMD ] = 0x000UL ,
[ OP_USBSTS ] = 0x004UL ,
[ OP_USBINTR ] = 0x008UL ,
[ OP_DEVICEADDR ] = 0x014UL ,
[ OP_ENDPTLISTADDR ] = 0x018UL ,
[ OP_PORTSC ] = 0x044UL ,
[ OP_DEVLC ] = 0x084UL ,
2012-05-11 18:25:47 +04:00
[ OP_OTGSC ] = 0x0C4UL ,
2012-05-11 18:25:46 +04:00
[ OP_USBMODE ] = 0x0C8UL ,
[ OP_ENDPTSETUPSTAT ] = 0x0D8UL ,
[ OP_ENDPTPRIME ] = 0x0DCUL ,
[ OP_ENDPTFLUSH ] = 0x0E0UL ,
[ OP_ENDPTSTAT ] = 0x0E4UL ,
[ OP_ENDPTCOMPLETE ] = 0x0E8UL ,
[ OP_ENDPTCTRL ] = 0x0ECUL ,
} ;
2012-05-11 18:25:47 +04:00
static int hw_alloc_regmap ( struct ci13xxx * ci , bool is_lpm )
2012-05-11 18:25:46 +04:00
{
int i ;
2012-05-11 18:25:47 +04:00
kfree ( ci - > hw_bank . regmap ) ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
ci - > hw_bank . regmap = kzalloc ( ( OP_LAST + 1 ) * sizeof ( void * ) ,
GFP_KERNEL ) ;
if ( ! ci - > hw_bank . regmap )
2012-05-11 18:25:46 +04:00
return - ENOMEM ;
for ( i = 0 ; i < OP_ENDPTCTRL ; i + + )
2012-05-11 18:25:47 +04:00
ci - > hw_bank . regmap [ i ] =
( i < = CAP_LAST ? ci - > hw_bank . cap : ci - > hw_bank . op ) +
2012-05-11 18:25:46 +04:00
( is_lpm ? ci_regs_lpm [ i ] : ci_regs_nolpm [ i ] ) ;
for ( ; i < = OP_LAST ; i + + )
2012-05-11 18:25:47 +04:00
ci - > hw_bank . regmap [ i ] = ci - > hw_bank . op +
2012-05-11 18:25:46 +04:00
4 * ( i - OP_ENDPTCTRL ) +
( is_lpm
? ci_regs_lpm [ OP_ENDPTCTRL ]
: ci_regs_nolpm [ OP_ENDPTCTRL ] ) ;
return 0 ;
}
/**
* hw_port_test_set : writes port test mode ( execute without interruption )
* @ mode : new value
*
* This function returns an error code
*/
int hw_port_test_set ( struct ci13xxx * ci , u8 mode )
{
const u8 TEST_MODE_MAX = 7 ;
if ( mode > TEST_MODE_MAX )
return - EINVAL ;
hw_write ( ci , OP_PORTSC , PORTSC_PTC , mode < < ffs_nr ( PORTSC_PTC ) ) ;
return 0 ;
}
/**
* hw_port_test_get : reads port test mode value
*
* This function returns port test mode value
*/
u8 hw_port_test_get ( struct ci13xxx * ci )
{
return hw_read ( ci , OP_PORTSC , PORTSC_PTC ) > > ffs_nr ( PORTSC_PTC ) ;
}
2012-05-11 18:25:47 +04:00
static int hw_device_init ( struct ci13xxx * ci , void __iomem * base )
2012-05-11 18:25:46 +04:00
{
u32 reg ;
/* bank is a module variable */
2012-05-11 18:25:47 +04:00
ci - > hw_bank . abs = base ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
ci - > hw_bank . cap = ci - > hw_bank . abs ;
2012-06-29 13:48:53 +04:00
ci - > hw_bank . cap + = ci - > platdata - > capoffset ;
2012-05-11 18:25:47 +04:00
ci - > hw_bank . op = ci - > hw_bank . cap + ioread8 ( ci - > hw_bank . cap ) ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
hw_alloc_regmap ( ci , false ) ;
reg = hw_read ( ci , CAP_HCCPARAMS , HCCPARAMS_LEN ) > >
2012-05-11 18:25:46 +04:00
ffs_nr ( HCCPARAMS_LEN ) ;
2012-05-11 18:25:47 +04:00
ci - > hw_bank . lpm = reg ;
hw_alloc_regmap ( ci , ! ! reg ) ;
ci - > hw_bank . size = ci - > hw_bank . op - ci - > hw_bank . abs ;
ci - > hw_bank . size + = OP_LAST ;
ci - > hw_bank . size / = sizeof ( u32 ) ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
reg = hw_read ( ci , CAP_DCCPARAMS , DCCPARAMS_DEN ) > >
2012-05-11 18:25:46 +04:00
ffs_nr ( DCCPARAMS_DEN ) ;
2012-05-11 18:25:47 +04:00
ci - > hw_ep_max = reg * 2 ; /* cache hw ENDPT_MAX */
2012-05-11 18:25:46 +04:00
2012-05-15 17:58:18 +04:00
if ( ci - > hw_ep_max > ENDPT_MAX )
2012-05-11 18:25:46 +04:00
return - ENODEV ;
2012-05-11 18:25:47 +04:00
dev_dbg ( ci - > dev , " ChipIdea HDRC found, lpm: %d; cap: %p op: %p \n " ,
ci - > hw_bank . lpm , ci - > hw_bank . cap , ci - > hw_bank . op ) ;
2012-05-11 18:25:46 +04:00
/* setup lock mode ? */
/* ENDPTSETUPSTAT is '0' by default */
/* HCSPARAMS.bf.ppc SHOULD BE zero for device */
return 0 ;
}
/**
* hw_device_reset : resets chip ( execute without interruption )
* @ ci : the controller
*
* This function returns an error code
*/
2012-05-11 18:25:54 +04:00
int hw_device_reset ( struct ci13xxx * ci , u32 mode )
2012-05-11 18:25:46 +04:00
{
/* should flush & stop before reset */
hw_write ( ci , OP_ENDPTFLUSH , ~ 0 , ~ 0 ) ;
hw_write ( ci , OP_USBCMD , USBCMD_RS , 0 ) ;
hw_write ( ci , OP_USBCMD , USBCMD_RST , USBCMD_RST ) ;
while ( hw_read ( ci , OP_USBCMD , USBCMD_RST ) )
udelay ( 10 ) ; /* not RTOS friendly */
2012-06-29 13:48:53 +04:00
if ( ci - > platdata - > notify_event )
ci - > platdata - > notify_event ( ci ,
2012-05-11 18:25:46 +04:00
CI13XXX_CONTROLLER_RESET_EVENT ) ;
2012-06-29 13:48:53 +04:00
if ( ci - > platdata - > flags & CI13XXX_DISABLE_STREAMING )
2012-05-11 18:25:53 +04:00
hw_write ( ci , OP_USBMODE , USBMODE_CI_SDIS , USBMODE_CI_SDIS ) ;
2012-05-11 18:25:46 +04:00
/* USBMODE should be configured step by step */
hw_write ( ci , OP_USBMODE , USBMODE_CM , USBMODE_CM_IDLE ) ;
2012-05-11 18:25:54 +04:00
hw_write ( ci , OP_USBMODE , USBMODE_CM , mode ) ;
2012-05-11 18:25:46 +04:00
/* HW >= 2.3 */
hw_write ( ci , OP_USBMODE , USBMODE_SLOM , USBMODE_SLOM ) ;
2012-05-11 18:25:54 +04:00
if ( hw_read ( ci , OP_USBMODE , USBMODE_CM ) ! = mode ) {
pr_err ( " cannot enter in %s mode " , ci_role ( ci ) - > name ) ;
2012-05-11 18:25:46 +04:00
pr_err ( " lpm = %i " , ci - > hw_bank . lpm ) ;
return - ENODEV ;
}
return 0 ;
}
2012-05-11 18:25:47 +04:00
/**
* ci_otg_role - pick role based on ID pin state
* @ ci : the controller
*/
static enum ci_role ci_otg_role ( struct ci13xxx * ci )
{
u32 sts = hw_read ( ci , OP_OTGSC , ~ 0 ) ;
enum ci_role role = sts & OTGSC_ID
? CI_ROLE_GADGET
: CI_ROLE_HOST ;
return role ;
}
/**
* ci_role_work - perform role changing based on ID pin
* @ work : work struct
*/
static void ci_role_work ( struct work_struct * work )
{
struct ci13xxx * ci = container_of ( work , struct ci13xxx , work ) ;
enum ci_role role = ci_otg_role ( ci ) ;
if ( role ! = ci - > role ) {
dev_dbg ( ci - > dev , " switching from %s to %s \n " ,
ci_role ( ci ) - > name , ci - > roles [ role ] - > name ) ;
ci_role_stop ( ci ) ;
ci_role_start ( ci , role ) ;
2012-09-12 15:58:11 +04:00
enable_irq ( ci - > irq ) ;
2012-05-11 18:25:47 +04:00
}
}
static ssize_t show_role ( struct device * dev , struct device_attribute * attr ,
char * buf )
{
struct ci13xxx * ci = dev_get_drvdata ( dev ) ;
return sprintf ( buf , " %s \n " , ci_role ( ci ) - > name ) ;
}
static ssize_t store_role ( struct device * dev , struct device_attribute * attr ,
const char * buf , size_t count )
{
struct ci13xxx * ci = dev_get_drvdata ( dev ) ;
enum ci_role role ;
int ret ;
for ( role = CI_ROLE_HOST ; role < CI_ROLE_END ; role + + )
if ( ci - > roles [ role ] & & ! strcmp ( buf , ci - > roles [ role ] - > name ) )
break ;
if ( role = = CI_ROLE_END | | role = = ci - > role )
return - EINVAL ;
ci_role_stop ( ci ) ;
ret = ci_role_start ( ci , role ) ;
if ( ret )
return ret ;
return count ;
}
static DEVICE_ATTR ( role , S_IRUSR | S_IWUSR , show_role , store_role ) ;
static irqreturn_t ci_irq ( int irq , void * data )
{
struct ci13xxx * ci = data ;
irqreturn_t ret = IRQ_NONE ;
2012-09-12 15:58:11 +04:00
u32 otgsc = 0 ;
2012-05-11 18:25:47 +04:00
2012-09-12 15:58:11 +04:00
if ( ci - > is_otg )
otgsc = hw_read ( ci , OP_OTGSC , ~ 0 ) ;
2012-05-11 18:25:47 +04:00
2012-09-12 15:58:11 +04:00
if ( ci - > role ! = CI_ROLE_END )
ret = ci_role ( ci ) - > irq ( ci ) ;
if ( ci - > is_otg & & ( otgsc & OTGSC_IDIS ) ) {
hw_write ( ci , OP_OTGSC , OTGSC_IDIS , OTGSC_IDIS ) ;
disable_irq_nosync ( ci - > irq ) ;
queue_work ( ci - > wq , & ci - > work ) ;
ret = IRQ_HANDLED ;
2012-05-11 18:25:47 +04:00
}
2012-09-12 15:58:11 +04:00
return ret ;
2012-05-11 18:25:47 +04:00
}
2012-07-07 18:56:42 +04:00
static DEFINE_IDA ( ci_ida ) ;
2012-07-07 18:56:41 +04:00
struct platform_device * ci13xxx_add_device ( struct device * dev ,
struct resource * res , int nres ,
struct ci13xxx_platform_data * platdata )
{
struct platform_device * pdev ;
2012-07-07 18:56:42 +04:00
int id , ret ;
2012-07-07 18:56:41 +04:00
2012-07-07 18:56:42 +04:00
id = ida_simple_get ( & ci_ida , 0 , 0 , GFP_KERNEL ) ;
if ( id < 0 )
return ERR_PTR ( id ) ;
pdev = platform_device_alloc ( " ci_hdrc " , id ) ;
if ( ! pdev ) {
ret = - ENOMEM ;
goto put_id ;
}
2012-07-07 18:56:41 +04:00
pdev - > dev . parent = dev ;
pdev - > dev . dma_mask = dev - > dma_mask ;
pdev - > dev . dma_parms = dev - > dma_parms ;
dma_set_coherent_mask ( & pdev - > dev , dev - > coherent_dma_mask ) ;
ret = platform_device_add_resources ( pdev , res , nres ) ;
if ( ret )
goto err ;
ret = platform_device_add_data ( pdev , platdata , sizeof ( * platdata ) ) ;
if ( ret )
goto err ;
ret = platform_device_add ( pdev ) ;
if ( ret )
goto err ;
return pdev ;
err :
platform_device_put ( pdev ) ;
2012-07-07 18:56:42 +04:00
put_id :
ida_simple_remove ( & ci_ida , id ) ;
2012-07-07 18:56:41 +04:00
return ERR_PTR ( ret ) ;
}
EXPORT_SYMBOL_GPL ( ci13xxx_add_device ) ;
void ci13xxx_remove_device ( struct platform_device * pdev )
{
2012-11-22 13:11:25 +04:00
int id = pdev - > id ;
2012-07-07 18:56:41 +04:00
platform_device_unregister ( pdev ) ;
2012-11-22 13:11:25 +04:00
ida_simple_remove ( & ci_ida , id ) ;
2012-07-07 18:56:41 +04:00
}
EXPORT_SYMBOL_GPL ( ci13xxx_remove_device ) ;
2012-11-19 22:21:48 +04:00
static int ci_hdrc_probe ( struct platform_device * pdev )
2012-05-11 18:25:46 +04:00
{
struct device * dev = & pdev - > dev ;
2012-05-11 18:25:47 +04:00
struct ci13xxx * ci ;
2012-05-11 18:25:46 +04:00
struct resource * res ;
void __iomem * base ;
int ret ;
2012-05-11 18:25:47 +04:00
if ( ! dev - > platform_data ) {
2012-05-11 18:25:46 +04:00
dev_err ( dev , " platform data missing \n " ) ;
return - ENODEV ;
}
res = platform_get_resource ( pdev , IORESOURCE_MEM , 0 ) ;
if ( ! res ) {
dev_err ( dev , " missing resource \n " ) ;
return - ENODEV ;
}
base = devm_request_and_ioremap ( dev , res ) ;
2013-01-21 17:02:46 +04:00
if ( ! base ) {
2012-05-11 18:25:46 +04:00
dev_err ( dev , " can't request and ioremap resource \n " ) ;
return - ENOMEM ;
}
2012-05-11 18:25:47 +04:00
ci = devm_kzalloc ( dev , sizeof ( * ci ) , GFP_KERNEL ) ;
if ( ! ci ) {
dev_err ( dev , " can't allocate device \n " ) ;
return - ENOMEM ;
}
ci - > dev = dev ;
2012-06-29 13:48:53 +04:00
ci - > platdata = dev - > platform_data ;
2012-07-07 18:56:46 +04:00
if ( ci - > platdata - > phy )
ci - > transceiver = ci - > platdata - > phy ;
else
ci - > global_phy = true ;
2012-05-11 18:25:47 +04:00
ret = hw_device_init ( ci , base ) ;
if ( ret < 0 ) {
dev_err ( dev , " can't initialize hardware \n " ) ;
return - ENODEV ;
}
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:54 +04:00
ci - > hw_bank . phys = res - > start ;
2012-05-11 18:25:47 +04:00
ci - > irq = platform_get_irq ( pdev , 0 ) ;
if ( ci - > irq < 0 ) {
2012-05-11 18:25:46 +04:00
dev_err ( dev , " missing IRQ \n " ) ;
2012-05-11 18:25:47 +04:00
return - ENODEV ;
}
INIT_WORK ( & ci - > work , ci_role_work ) ;
ci - > wq = create_singlethread_workqueue ( " ci_otg " ) ;
if ( ! ci - > wq ) {
dev_err ( dev , " can't create workqueue \n " ) ;
return - ENODEV ;
}
/* initialize role(s) before the interrupt is requested */
2012-05-11 18:25:54 +04:00
ret = ci_hdrc_host_init ( ci ) ;
if ( ret )
dev_info ( dev , " doesn't support host \n " ) ;
2012-05-11 18:25:47 +04:00
ret = ci_hdrc_gadget_init ( ci ) ;
if ( ret )
dev_info ( dev , " doesn't support gadget \n " ) ;
if ( ! ci - > roles [ CI_ROLE_HOST ] & & ! ci - > roles [ CI_ROLE_GADGET ] ) {
dev_err ( dev , " no supported roles \n " ) ;
ret = - ENODEV ;
goto rm_wq ;
}
if ( ci - > roles [ CI_ROLE_HOST ] & & ci - > roles [ CI_ROLE_GADGET ] ) {
ci - > is_otg = true ;
2012-09-12 15:58:07 +04:00
/* ID pin needs 1ms debouce time, we delay 2ms for safe */
mdelay ( 2 ) ;
2012-05-11 18:25:47 +04:00
ci - > role = ci_otg_role ( ci ) ;
} else {
ci - > role = ci - > roles [ CI_ROLE_HOST ]
? CI_ROLE_HOST
: CI_ROLE_GADGET ;
}
ret = ci_role_start ( ci , ci - > role ) ;
if ( ret ) {
dev_err ( dev , " can't start %s role \n " , ci_role ( ci ) - > name ) ;
2012-05-11 18:25:46 +04:00
ret = - ENODEV ;
2012-05-11 18:25:47 +04:00
goto rm_wq ;
2012-05-11 18:25:46 +04:00
}
2012-05-11 18:25:47 +04:00
platform_set_drvdata ( pdev , ci ) ;
2012-06-29 13:48:53 +04:00
ret = request_irq ( ci - > irq , ci_irq , IRQF_SHARED , ci - > platdata - > name ,
2012-05-11 18:25:47 +04:00
ci ) ;
if ( ret )
goto stop ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
ret = device_create_file ( dev , & dev_attr_role ) ;
2012-05-11 18:25:46 +04:00
if ( ret )
2012-05-11 18:25:47 +04:00
goto rm_attr ;
if ( ci - > is_otg )
hw_write ( ci , OP_OTGSC , OTGSC_IDIE , OTGSC_IDIE ) ;
return ret ;
rm_attr :
device_remove_file ( dev , & dev_attr_role ) ;
stop :
ci_role_stop ( ci ) ;
rm_wq :
flush_workqueue ( ci - > wq ) ;
destroy_workqueue ( ci - > wq ) ;
2012-05-11 18:25:46 +04:00
return ret ;
}
2012-11-19 22:26:20 +04:00
static int ci_hdrc_remove ( struct platform_device * pdev )
2012-05-11 18:25:46 +04:00
{
2012-05-11 18:25:47 +04:00
struct ci13xxx * ci = platform_get_drvdata ( pdev ) ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
flush_workqueue ( ci - > wq ) ;
destroy_workqueue ( ci - > wq ) ;
device_remove_file ( ci - > dev , & dev_attr_role ) ;
free_irq ( ci - > irq , ci ) ;
ci_role_stop ( ci ) ;
2012-05-11 18:25:46 +04:00
return 0 ;
}
2012-05-11 18:25:47 +04:00
static struct platform_driver ci_hdrc_driver = {
. probe = ci_hdrc_probe ,
2012-11-19 22:21:08 +04:00
. remove = ci_hdrc_remove ,
2012-05-11 18:25:46 +04:00
. driver = {
2012-05-11 18:25:47 +04:00
. name = " ci_hdrc " ,
2012-05-11 18:25:46 +04:00
} ,
} ;
2012-05-11 18:25:47 +04:00
module_platform_driver ( ci_hdrc_driver ) ;
2012-05-11 18:25:46 +04:00
2012-05-11 18:25:47 +04:00
MODULE_ALIAS ( " platform:ci_hdrc " ) ;
2012-05-11 18:25:46 +04:00
MODULE_ALIAS ( " platform:ci13xxx " ) ;
MODULE_LICENSE ( " GPL v2 " ) ;
MODULE_AUTHOR ( " David Lopo <dlopo@chipidea.mips.com> " ) ;
2012-05-11 18:25:47 +04:00
MODULE_DESCRIPTION ( " ChipIdea HDRC Driver " ) ;