USB OTG驱动分析(一)

前一段时间弄了 2 个礼拜的 OTG 驱动调试,感觉精神疲惫啊。主要原因还是自己对 OTG 功能不了解造成的。现在终于完成但是对实质原理还有些模糊。所以 自己重新总结一下。因为自己是菜鸟,所以用菜鸟的白话方式分析。高手滤过吧。 所谓 OTG 功能 就是具备该功能的设备即可当主设备 (host) 去轮询别人,也可以当从设备 (device) 去被别人轮~~(双性人?)。正所谓所有的产品和功能都是因为需求 存在的,举个最简单的需求,原来 MP3 想传送一个歌曲都得通过电脑。现在只要两个 MP3 链接,其中一个 MP3 OTG 功能作为主设备(相当于电脑主机),然后另外一个是从设备就可以 实现数据的传送了。 那么话说回来,具有 OTG 功能的设备如何确定自己是主还是从设备那。原来原来 USB 接口上有 4 个管脚, OTG 功能有 5 个。原来 4 个分别是电 D+ D- 地。 现在增加了一个 ID 。这个 ID 线就决定了自己做主设备还是从设备。如果 ID 线是高则自己是从设备,反之是主设备。

 

 

下面开始分析代码。
 向平时一样定义platform_device资源等信息。
定义platform_device结构
 static struct platform_device __maybe_unused dr_otg_device =
{ . name = "fsl-usb2-otg" , //设备的名称 日后匹配用

. id = - 1, //只有一个这样的设备

. dev = { . release = dr_otg_release,
. dma_mask = & dr_otg_dmamask,
. coherent_dma_mask = 0xffffffff,
 } ,
 . resource = otg_resources, //设备的资源 看下面

 . num_resources = ARRAY_SIZE( otg_resources) ,
} ;

定义platform_device下的struct resource设备资源结构
static struct resource otg_resources[ ] = {
[ 0] = {
. start = ( u32) ( USB_OTGREGS_BASE) , //描述设备实体在cpu总线上的线 性起始物理地址

. end = ( u32) ( USB_OTGREGS_BASE + 0x1ff) , //描述设备实体在cpu总线上的线性结尾物理地址

. flags = IORESOURCE_MEM, } ,
[ 1] = {
. start = MXC_INT_USB_OTG, //中断号

. flags = IORESOURCE_IRQ, } ,
} ;

 

定义平台设备私 有数据,以后驱动要使用
static struct fsl_usb2_platform_data __maybe_unused dr_utmi_config = {
. name = "DR" ,
. platform_init = usbotg_init,
. platform_uninit = usbotg_uninit,
. phy_mode = FSL_USB2_PHY_UTMI_WIDE,
. power_budget = 500, /* via RT9706 */
. gpio_usb_active = gpio_usbotg_utmi_active,
. gpio_usb_inactive = gpio_usbotg_utmi_inactive,
. transceiver = "utmi" ,
. wake_up_enable = _wake_up_enable,
} ;
# define PDATA ( & dr_utmi_config) 定义platform_device下的DEV设备下的平台私有数据(就是该设备私有的数据)


static inline void dr_register_otg( void ) {
 PDATA- > operating_mode = FSL_USB2_DR_OTG; //将模式更改(上面定义的时候定义的是FSL_USB2_PHY_UTMI_WIDE,不知道为什么开始不定义这个,可能是为了兼 容)

dr_otg_device. dev. platform_data = PDATA; //该设备的私有数据赋值,就 是上面定义的dr_utmi_config

if ( platform_device_register( & dr_otg_device) )
 printk( KERN_ERR "usb: can't register otg device/n" ) ;
else
printk( KERN_INFO "usb: DR OTG registered/n" ) ;
}

 

上面几个过程主要是完成了设备的注册。这个过程是:
1. 定 义platform_device结构。
2. 定义 platform_device下的struct resource设备资源结构
3. 定义platform_device下的DEV设备下的平台私有数据(就是该设备私有的数据)
4. 调用platform_device_register将 platform_device结构
注册上面4个过程调用结束后,设备的信息就被注册到系统中,等待驱动的使用

 

下 面分析驱动和设备的链接过程

定义platform_driver结构
struct platform_driver fsl_otg_driver = {
. probe = fsl_otg_probe, //定义处理函数,该函数在设备名字匹配到后调用,也就是发现该驱动 对应的设备在系统中注册过。

. remove = fsl_otg_remove,
. driver = {
. name = "fsl-usb2-otg" , //通过该名字匹配开始注册进系统的设备

. owner = THIS_MODULE,
} ,
} ;
将platform_driver结构注册进系统,系统通 过注册名字匹配该设备是否已经在系统中,如果在调用注册的probe = fsl_otg_probe函数
static int __init fsl_usb_otg_init( void )
{
printk( KERN_INFO DRIVER_DESC " loaded, %s/n" , DRIVER_VERSION) ;
return platform_driver_register( & fsl_otg_driver) ;
}


 


调用 fsl_otg_probe 函数,函数参数 platform_device *pdev ,就是我们上面注册进系统 的 platform_device 结构,现在由系统赋值调用 fsl_otg_probe

 

static int __init fsl_otg_probe( struct platform_device * pdev)
{
    int status;
    struct fsl_usb2_platform_data * pdata;

    DBG( "pdev=0x%p/n" , pdev) ;

    if ( ! pdev)
        return - ENODEV;
/* 判断是否有设备自己的数据,就是检查我们上面定义的 3 的过程*/
    if ( ! pdev- > dev. platform_data)
        return - ENOMEM;

    pdata = pdev- > dev. platform_data;

    /* configure the OTG */
    status = fsl_otg_conf( pdev) ;
    if ( status) {
        printk( KERN_INFO "Couldn't init OTG module/n" ) ;
        return - status;
    }

    /* start OTG */
    status = usb_otg_start( pdev) ;

    if ( register_chrdev( FSL_OTG_MAJOR, FSL_OTG_NAME, & otg_fops) ) {
        printk( KERN_WARNING FSL_OTG_NAME
         ": unable to register FSL OTG device/n" ) ;
        return - EIO;
    }

    create_proc_file( ) ;
    return status;
}


上面函数中调用了 fsl_otg_conf ,我们来看看他干了什么。

 

static int fsl_otg_conf( struct platform_device * pdev)
{
    int status;
    struct fsl_otg * fsl_otg_tc;
    struct fsl_usb2_platform_data * pdata;

    pdata = pdev- > dev. platform_data;

    DBG( ) ;
/**************************************************************/

struct fsl_otg {
 struct otg_transceiver otg;
 struct otg_fsm fsm;
 struct usb_dr_mmap *dr_mem_map;
 struct delayed_work otg_event;

 /*used for usb host */
 struct work_struct work_wq;
 u8 host_working;

 int irq;
};

/**************************************************************/
    if ( fsl_otg_dev)
        return 0;

    /* allocate space to fsl otg device */
    fsl_otg_tc = kzalloc( sizeof ( struct fsl_otg) , GFP_KERNEL) ;
    if ( ! fsl_otg_tc)
        return - ENODEV;

    INIT_DELAYED_WORK( & fsl_otg_tc- > otg_event, fsl_otg_event) ;

    INIT_LIST_HEAD( & active_timers) ;
    status = fsl_otg_init_timers( & fsl_otg_tc- > fsm) ;
    if ( status) {
        printk( KERN_INFO "Couldn't init OTG timers/n" ) ;
        fsl_otg_uninit_timers( ) ;
        kfree( fsl_otg_tc) ;
        return status;
    }
    spin_lock_init( & fsl_otg_tc- > fsm. lock) ;

    /* Set OTG state machine operations */

/**************************************************************/

static struct otg_fsm_ops fsl_otg_ops = {
 .chrg_vbus = fsl_otg_chrg_vbus,
 .drv_vbus = fsl_otg_drv_vbus,
 .loc_conn = fsl_otg_loc_conn,
 .loc_sof = fsl_otg_loc_sof,
 .start_pulse = fsl_otg_start_pulse,

 .add_timer = fsl_otg_add_timer,
 .del_timer = fsl_otg_del_timer,

 .start_host = fsl_otg_start_host,
 .start_gadget = fsl_otg_start_gadget,
};

/**************************************************************/
    fsl_otg_tc- > fsm. ops = & fsl_otg_ops;

    /* initialize the otg structure */
    fsl_otg_tc- > otg. label = DRIVER_DESC;
    fsl_otg_tc- > otg. set_host = fsl_otg_set_host;
    fsl_otg_tc- > otg. set_peripheral = fsl_otg_set_peripheral;
    fsl_otg_tc- > otg. set_power = fsl_otg_set_power;
    fsl_otg_tc- > otg. start_hnp = fsl_otg_start_hnp;
    fsl_otg_tc- > otg. start_srp = fsl_otg_start_srp;

    fsl_otg_dev = fsl_otg_tc;

    /* Store the otg transceiver */

/***************************************************************/

int otg_set_transceiver(struct otg_transceiver *x)
{
 if (xceiv && x)
  return -EBUSY;
 xceiv = x;
 return 0;
}

该函数就是将struct otg_transceiver结构副给一个全局变量保存,供以后使用,以后会通过调用下面函数得到该结构

struct otg_transceiver *otg_get_transceiver(void)
{
 if (xceiv)
  get_device(xceiv->dev);
 return xceiv;
}

/***************************************************************/
    status = otg_set_transceiver( & fsl_otg_tc- > otg) ;
    if ( status) {
        printk( KERN_WARNING ": unable to register OTG transceiver./n" ) ;
        return status;
    }

    return 0;
}

 

 

 

int usb_otg_start( struct platform_device * pdev)
{
    struct fsl_otg * p_otg;

/*获得otg_transceiver结构 */
    struct otg_transceiver * otg_trans = otg_get_transceiver( ) ;
    struct otg_fsm * fsm;
    volatile unsigned long * p;
    int status;
    struct resource * res;
    u32 temp;

/*获得设备的私有数据*/
    struct fsl_usb2_platform_data * pdata = pdev- > dev. platform_data;
/* 使用container_of宏定义可以通过结构中一个变量的指针获得该结构首地址 */
    p_otg = container_of( otg_trans, struct fsl_otg, otg) ;
    fsm = & p_otg- > fsm;

    /* Initialize the state machine structure with default values */
    SET_OTG_STATE( otg_trans, OTG_STATE_UNDEFINED) ;
    fsm- > transceiver = & p_otg- > otg;

    /* We don't require predefined MEM/IRQ resource index */

/*获得设备的资源,是在设备注册时结构体里面的内容*/
    res = platform_get_resource( pdev, IORESOURCE_MEM, 0) ;
    if ( ! res)
        return - ENXIO;

    /* We don't request_mem_region here to enable resource sharing
     * with host/device */

/*通过资源中 获得的物理地址映射一个可以被驱动访问的虚拟地址指针*/
    usb_dr_regs = ioremap( res- > start, sizeof ( struct usb_dr_mmap) ) ;

/*将该指针保存到p_otg - > dr_mem_map中 */
    p_otg- > dr_mem_map = ( struct usb_dr_mmap * ) usb_dr_regs;
    pdata- > regs = ( void * ) usb_dr_regs;

    /* request irq */

/*获得设备注册时候的中断并注册,在 OTG ID发生变化时触发中断,然后调用注册的中断例程函数,函数后面分析*/
    p_otg- > irq = platform_get_irq( pdev, 0) ;
    status = request_irq( p_otg- > irq, fsl_otg_isr,
                IRQF_SHARED, driver_name, p_otg) ;
    if ( status) {
        dev_dbg( p_otg- > otg. dev, "can't get IRQ %d, error %d/n" ,
            p_otg- > irq, status) ;
        iounmap( p_otg- > dr_mem_map) ;
        kfree( p_otg) ;
        return status;
    }

    if ( pdata- > platform_init & & pdata- > platform_init( pdev) ! = 0)
        return - EINVAL;


    /* Export DR controller resources */

/**************************************************/

int otg_set_resources(struct resource *resources)
{
 otg_resources = resources;
 return 0;
}

otg_set_transceiver功能类似将设备资源保存到一个全局变量中

/**************************************************/
    otg_set_resources( pdev- > resource) ;
/*开始配置USB寄存器*/
    /* stop the controller */
    temp = readl( & p_otg- > dr_mem_map- > usbcmd) ;
    temp & = ~ USB_CMD_RUN_STOP;
    writel( temp, & p_otg- > dr_mem_map- > usbcmd) ;

    /* reset the controller */
    temp = readl( & p_otg- > dr_mem_map- > usbcmd) ;
    temp | = USB_CMD_CTRL_RESET;
    writel( temp, & p_otg- > dr_mem_map- > usbcmd) ;

    /* wait reset completed */
    while ( readl( & p_otg- > dr_mem_map- > usbcmd) & USB_CMD_CTRL_RESET) ;

    /* configure the VBUSHS as IDLE(both host and device) */
    temp = USB_MODE_STREAM_DISABLE | ( pdata- > es ? USB_MODE_ES : 0) ;
    writel( temp, & p_otg- > dr_mem_map- > usbmode) ;

    /* configure PHY interface */
    temp = readl( & p_otg- > dr_mem_map- > portsc) ;
    temp & = ~ ( PORTSC_PHY_TYPE_SEL | PORTSC_PTW) ;
    switch ( pdata- > phy_mode) {
    case FSL_USB2_PHY_ULPI:
        temp | = PORTSC_PTS_ULPI;
        break ;
    case FSL_USB2_PHY_UTMI_WIDE:
        temp | = PORTSC_PTW_16BIT;
        /* fall through */
    case FSL_USB2_PHY_UTMI:
        temp | = PORTSC_PTS_UTMI;
        /* fall through */
    default :
        break ;
    }
    writel( temp, & p_otg- > dr_mem_map- > portsc) ;

    if ( pdata- > have_sysif_regs) {
        /* configure control enable IO output, big endian register */
        p = ( volatile unsigned long * ) ( & p_otg- > dr_mem_map- > control) ;
        temp = * p;
        temp | = USB_CTRL_IOENB;
        * p = temp;
    }

    /* disable all interrupt and clear all OTGSC status */
    temp = readl( & p_otg- > dr_mem_map- > otgsc) ;
    temp & = ~ OTGSC_INTERRUPT_ENABLE_BITS_MASK;
    temp | = OTGSC_INTERRUPT_STATUS_BITS_MASK | OTGSC_CTRL_VBUS_DISCHARGE;
    writel( temp, & p_otg- > dr_mem_map- > otgsc) ;


    /*
     * The identification (id) input is FALSE when a Mini-A plug is inserted
     * in the devices Mini-AB receptacle. Otherwise, this input is TRUE.
     * Also: record initial state of ID pin
     */

    if ( le32_to_cpu( p_otg- > dr_mem_map- > otgsc) & OTGSC_STS_USB_ID) {
        p_otg- > otg. state = OTG_STATE_UNDEFINED;
        p_otg- > fsm. id = 1;
    } else {
        p_otg- > otg. state = OTG_STATE_A_IDLE;
        p_otg- > fsm. id = 0;
    }

    DBG( "initial ID pin=%d/n" , p_otg- > fsm. id) ;

    /* enable OTG ID pin interrupt */
    temp = readl( & p_otg- > dr_mem_map- > otgsc) ;
    temp | = OTGSC_INTR_USB_ID_EN;
    temp & = ~ ( OTGSC_CTRL_VBUS_DISCHARGE | OTGSC_INTR_1MS_TIMER_EN) ;
    writel( temp, & p_otg- > dr_mem_map- > otgsc) ;

    return 0;
}

 

下面分析下 中断例程函数

该函数就是判断ID的高低,也就是自己做主设备还是从设备

irqreturn_t fsl_otg_isr( int irq, void * dev_id)
{
    struct otg_fsm * fsm = & ( ( struct fsl_otg * ) dev_id) - > fsm;
    struct otg_transceiver * otg = & ( ( struct fsl_otg * ) dev_id) - > otg;
    u32 otg_int_src, otg_sc;
/* 获得ID的变化信息*/
    otg_sc = le32_to_cpu( usb_dr_regs- > otgsc) ;
    otg_int_src = otg_sc & OTGSC_INTSTS_MASK & ( otg_sc > > 8) ;

    /* Only clear otg interrupts */
    usb_dr_regs- > otgsc | = cpu_to_le32( otg_sc & OTGSC_INTSTS_MASK) ;

    /*FIXME: ID change not generate when init to 0 */
    fsm- > id = ( otg_sc & OTGSC_STS_USB_ID) ? 1 : 0;
    otg- > default_a = ( fsm- > id = = 0) ;

    /* process OTG interrupts */
    if ( otg_int_src) {
        if ( otg_int_src & OTGSC_INTSTS_USB_ID) {
            fsm- > id = ( otg_sc & OTGSC_STS_USB_ID) ? 1 : 0;
            otg- > default_a = ( fsm- > id = = 0) ;
            /* clear conn information */
            if ( fsm- > id)
                fsm- > b_conn = 0;
            else
                fsm- > a_conn = 0;

            if ( otg- > host)
                otg- > host- > is_b_host = fsm- > id;
            if ( otg- > gadget)
                otg- > gadget- > is_a_peripheral = ! fsm- > id;
            VDBG( "ID int (ID is %d)/n" , fsm- > id) ;

            if ( fsm- > id) {     /* switch to gadget *///从设备

/*schedule_delayed_work函数先停止主设备后打开从设备 */

/***************************************************/

schedule_delayed_work( & ( ( struct fsl_otg * )
                            dev_id) - > otg_event,
                            100) ;

函数就 是延迟100秒调用otg_event,就是下面函数。

static void fsl_otg_event(struct work_struct *work)
{
 struct fsl_otg *og = container_of(work, struct fsl_otg, otg_event.work);
 struct otg_fsm *fsm = &og->fsm;

 if (fsm->id) {  /* switch to gadget */
  fsl_otg_start_host(fsm, 0);
  otg_drv_vbus(fsm, 0);
  fsl_otg_start_gadget(fsm, 1);
 }
}

/***************************************************/
                schedule_delayed_work( & ( ( struct fsl_otg * )
                            dev_id) - > otg_event,
                            100) ;
            } else {     /* switch to host *///主设备
                cancel_delayed_work( &
                         ( ( struct fsl_otg * ) dev_id) - >
                         otg_event) ;
                fsl_otg_start_gadget( fsm, 0) ;//停止从设备
                otg_drv_vbus( fsm, 1) ;
                fsl_otg_start_host( fsm, 1) ;//打开主
            }

            return IRQ_HANDLED;
        }
    }

    return IRQ_NONE;
}

 

int fsl_otg_start_host( struct otg_fsm * fsm, int on)
{
    struct otg_transceiver * xceiv = fsm- > transceiver;
    struct device * dev;
    struct fsl_otg * otg_dev = container_of( xceiv, struct fsl_otg, otg) ;
    struct platform_driver * host_pdrv;
    struct platform_device * host_pdev;
    u32 retval = 0;
/*判 断是否有主设备的驱动注册进系统*/
    if ( ! xceiv- > host)
        return - ENODEV;
    dev = xceiv- > host- > controller;

/*找到主设备驱动的platform_driver结构,为下面的停止和恢复函数调用做准备 */
    host_pdrv = container_of( ( dev- > driver) , struct platform_driver, driver) ;
    host_pdev = to_platform_device( dev) ;

    /* Update a_vbus_vld state as a_vbus_vld int is disabled
     * in device mode
     */

    fsm- > a_vbus_vld =
     ( le32_to_cpu( usb_dr_regs- > otgsc) & OTGSC_STS_A_VBUS_VALID) ? 1 : 0;
    if ( on) {
        /* start fsl usb host controller */
        if ( otg_dev- > host_working)
            goto end;
        else {
            otg_reset_controller( ) ;
            VDBG( "host on....../n" ) ;
            if ( host_pdrv- > resume) {
                retval = host_pdrv- > resume( host_pdev) ;
                if ( fsm- > id) {
                    /* default-b */
                    fsl_otg_drv_vbus( 1) ;
                    /* Workaround: b_host can't driver
                     * vbus, but PP in PORTSC needs to
                     * be 1 for host to work.
                     * So we set drv_vbus bit in
                     * transceiver to 0 thru ULPI. */

# if defined( CONFIG_ISP1504_MXC)
                    write_ulpi( 0x0c, 0x20) ;
# endif
                }
            }

            otg_dev- > host_working = 1;
        }
    } else {
        /* stop fsl usb host controller */
        if ( ! otg_dev- > host_working)
            goto end;
        else {
            VDBG( "host off....../n" ) ;
            if ( host_pdrv- > suspend) {
                retval = host_pdrv- > suspend( host_pdev,
                            otg_suspend_state) ;
                if ( fsm- > id)
                    /* default-b */
                    fsl_otg_drv_vbus( 0) ;
            }
            otg_dev- > host_working = 0;
        }
    }
end:
    return retval;
}

可以看到最后设备是使用还是停止调用的函数 分别是

host_pdrv - > suspend

host_pdrv - > resume

而上面两个指针的函数赋值是在主设备驱动中完成的。

 

int fsl_otg_start_gadget( struct otg_fsm * fsm, int on)
{
    struct otg_transceiver * xceiv = fsm- > transceiver;
    struct device * dev;
    struct platform_driver * gadget_pdrv;
    struct platform_device * gadget_pdev;
/*判断是否有从设备驱动注册*/
    if ( ! xceiv- > gadget | | ! xceiv- > gadget- > dev. parent)
        return - ENODEV;

    VDBG( "gadget %s /n" , on ? "on" : "off" ) ;
    dev = xceiv- > gadget- > dev. parent;
/*找到从设备驱动的platform_driver结构首地址,为 下面调用其提供的功能函数做准备 */
    gadget_pdrv = container_of( ( dev- > driver) ,
            struct platform_driver, driver) ;
    gadget_pdev = to_platform_device( dev) ;

    if ( on)
        gadget_pdrv- > resume( gadget_pdev) ;
    else
        gadget_pdrv- > suspend( gadget_pdev, otg_suspend_state) ;

    return 0;
}

和上面主设备一样

到底是从设备停止还是恢复是调用
        gadget_pdrv - > resume ( gadget_pdev ) ;
        gadget_pdrv - > suspend ( gadget_pdev , otg_suspend_state ) ;

上面两个函数的指针就是在从设备驱动注册时链接的。

上面部分就是 OTG功能的 OTG驱动部分。 OTG功能还要有做主设备使用的主设备驱动和做从设备的从设备驱动。

从上面代码分析我们归纳出流程:

分两个大部分:

一 设备的注册  其中包括

1. 定义platform_device结构。
2. 定义platform_device下的struct resource设备资源结构
3. 定义 platform_device下的DEV设备下的平台私有数据(就是该设备私有的数据)
4. 调用platform_device_register将platform_device结构

二 OTG驱动的注册 其中包括

1.struct platform_driver fsl_otg_driver 结构的注册

2.匹配到有设备存在时调用的PORE函数,对设备进行初始化设置和功能函数的绑定

3.完成中断函数的绑定和中断例程的注册。

 

经过上面的处理后,只要OTG ID的变化就会触发中断,调用中断例程函数,决定是调用主设备还是从设备驱动。 而主设备和从设备驱动和OTG调用的链接是分别在主从设备驱动中完成的。后面我们介绍主从设备驱动中会介绍到。

 在文章的最后想起来这次调OTG遇见的问题,分享给大家希望大家有帮助。我调试OTG时,开始将OTG编译到内核中。(Y)。结果插入U盘没有反 应。后来发现原来我加入内核后,主设备驱动的先OTG设备驱动被执行,造成主设备函数和OTG功能的链接出现问题。(应该是OTG先初始化 然后从和主设备驱动链接。)后来我使用模块方式编译OTG功能。按照先载入OTG后载入从和主设备。(insmod方式),结果OTG就可以使用了。 后来通过降低主设备的优先级方式,把OTG编译进内核,然后因为主设备优先级低所以最后被调用。 也就是在主设备注册那使用
late_initcall(ehci_hcd_init); 代替//module_init(ehci_hcd_init);。这样主设备的优先级就低于设备驱动的优先级就在驱动加载完加载了。 但是总感觉这样不是很合理的方式,如果有朋友有更好的办法请指教。

  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值