wince串口之MDD分析(作者:wogoyixikexie@gliet)

                                                            wince串口之MDD分析

(作者:wogoyixikexie@gliet

 

         以前曾经看过一阵子串口,只了解一点皮毛,现在在5.0 2440BSP上增加串口遇到阻碍,现在不得不仔细研究一下。MDD是驱动的最上层,应用程序等都是和它打交道的,所以从它开始看才合理。

首先看Com_Init这个函数吧

  1. // ****************************************************************
  2. //
  3. //  @doc EXTERNAL
  4. //  @func       HANDLE | COM_INIT | Serial device initialization.
  5. //
  6. //@parm ULONG  | Identifier | Port identifier.  The device loader
  7. //passes in the registry key that contains information(参数是注册表的键值)
  8. //              about the active device.
  9. //
  10. //@remark       This routine is called at device load time in order
  11. //                  to perform any initialization.   Typically the init
  12. //              routine does as little as possible, postponing memory
  13. //              allocation and device power-on to Open time.
  14. //
  15. //   @rdesc     Returns a pointer to the serial head which is passed into 
  16. //   the COM_OPEN and COM_DEINIT entry points as a device handle.
  17. //
  18. HANDLE
  19. COM_Init(
  20.         ULONG   Identifier
  21.         )
  22. {
  23.     PVOID           pHWHead     = NULL;
  24.     PHW_INDEP_INFO  pSerialHead = NULL;
  25.     ULONG           HWBufferSize;
  26.     DWORD           DevIndex;
  27.     HKEY            hKey;
  28.     ULONG           kreserved = 0, kvaluetype;
  29.     ULONG           datasize = sizeof(ULONG);
  30.     /*
  31.      *  INTERNAL: this routine initializes the hardware abstraction interface
  32.      *  via HWInit(). It allocates a data structure representing this
  33.      *  instantiation of the device. It also creates an event and initializes
  34.      *  a critical section for receiving as well as registering the logical
  35.      *  interrupt dwIntID with NK via InterruptInitialize. This call
  36.      *  requires that the hardware dependent portion export apis that return
  37.      *  the physical address of the receive buffer and the size of that buffer.
  38.      *  Finally, it creates a buffer to act as an intermediate
  39.      *  buffer when receiving.
  40.      */
  41.     DEBUGMSG (ZONE_INIT | ZONE_FUNCTION, (TEXT("+COM_Init/r/n")));
  42.     // Allocate our control structure.
  43.     pSerialHead  =  (PHW_INDEP_INFO)LocalAlloc(LPTR, sizeof(HW_INDEP_INFO));
  44.     // Check that LocalAlloc did stuff ok too.
  45.     if ( !pSerialHead ) {
  46.         DEBUGMSG(ZONE_INIT | ZONE_ERROR,
  47.                  (TEXT("Error allocating memory for pSerialHead, COM_Init failed/n/r")));
  48.         return(NULL);
  49.     }
  50.     // Initially, open list is empty.
  51.     InitializeListHead( &pSerialHead->OpenList );
  52.     InitializeCriticalSection(&(pSerialHead->OpenCS));
  53.     pSerialHead->pAccessOwner = NULL;
  54.     pSerialHead->fEventMask = 0;
  55.     // Init CommTimeouts.
  56.     pSerialHead->CommTimeouts.ReadIntervalTimeout = READ_TIMEOUT;
  57.     pSerialHead->CommTimeouts.ReadTotalTimeoutMultiplier =
  58.     READ_TIMEOUT_MULTIPLIER;
  59.     pSerialHead->CommTimeouts.ReadTotalTimeoutConstant =
  60.     READ_TIMEOUT_CONSTANT;
  61.     pSerialHead->CommTimeouts.WriteTotalTimeoutMultiplier=  0;
  62.     pSerialHead->CommTimeouts.WriteTotalTimeoutConstant =   0;
  63.     /* Create tx and rx events and stash in global struct field. Check return.
  64.      */
  65.     pSerialHead->hSerialEvent = CreateEvent(0,FALSE,FALSE,NULL);
  66.     pSerialHead->hKillDispatchThread = CreateEvent(0, FALSE, FALSE, NULL);
  67.     pSerialHead->hTransmitEvent = CreateEvent(0, FALSE, FALSE, NULL);
  68.     pSerialHead->hReadEvent = CreateEvent(0, FALSE, FALSE, NULL);
  69.     if ( !pSerialHead->hSerialEvent || !pSerialHead->hKillDispatchThread ||
  70.          !pSerialHead->hTransmitEvent || !pSerialHead->hReadEvent ) {
  71.         DEBUGMSG(ZONE_ERROR | ZONE_INIT,
  72.                  (TEXT("Error creating event, COM_Init failed/n/r")));
  73.         LocalFree(pSerialHead);
  74.         return(NULL);
  75.     }
  76.     /* Initialize the critical sections that will guard the parts of
  77.      * the receive and transmit buffers.
  78.      */
  79.     InitializeCriticalSection(&(pSerialHead->ReceiveCritSec1));
  80.     InitializeCriticalSection(&(pSerialHead->TransmitCritSec1));
  81.     /* Want to use the Identifier to do RegOpenKey and RegQueryValue (?)
  82.      * to get the index to be passed to GetHWObj.
  83.      * The HWObj will also have a flag denoting whether to start the
  84.      * listening thread or provide the callback.
  85.      */
  86. //下面是一系列读注册表函数
  87.     DEBUGMSG (ZONE_INIT,(TEXT("Try to open %s/r/n"), (LPCTSTR)Identifier));
  88.     hKey = OpenDeviceKey((LPCTSTR)Identifier);
  89.     if ( !hKey ) {
  90.         DEBUGMSG (ZONE_INIT | ZONE_ERROR,
  91.                   (TEXT("Failed to open devkeypath, COM_Init failed/r/n")));
  92.         LocalFree(pSerialHead);
  93.         return(NULL);
  94.     }
  95.     datasize = sizeof(DWORD);
  96. //注册表中的DeviceArrayIndex被读出
  97.     if ( RegQueryValueEx(hKey, L"DeviceArrayIndex", NULL, &kvaluetype,
  98.                          (LPBYTE)&DevIndex, &datasize) ) {
  99.         DEBUGMSG (ZONE_INIT | ZONE_ERROR,
  100.                   (TEXT("Failed to get DeviceArrayIndex value, COM_Init failed/r/n")));
  101.         RegCloseKey (hKey);
  102.         LocalFree(pSerialHead);
  103.         return(NULL);
  104.     }
  105.     datasize = sizeof(DWORD);
  106.     if ( RegQueryValueEx(hKey, L"Priority256", NULL, &kvaluetype,
  107.                          (LPBYTE)&pSerialHead->Priority256, &datasize) ) {
  108.         pSerialHead->Priority256 = DEFAULT_CE_THREAD_PRIORITY;
  109.         DEBUGMSG (ZONE_INIT | ZONE_WARN,
  110.                   (TEXT("Failed to get Priority256 value, defaulting to %d/r/n"), pSerialHead->Priority256));
  111.     }
  112.     RegCloseKey (hKey);
  113.     DEBUGMSG (ZONE_INIT,
  114.               (TEXT("DevIndex %X/r/n"), DevIndex));
  115.     // Initialize hardware dependent data.
  116.     pSerialHead->pHWObj = GetSerialObject( DevIndex );
  117.     if ( !pSerialHead->pHWObj ) {
  118.         DEBUGMSG(ZONE_ERROR | ZONE_INIT,
  119.                  (TEXT("Error in GetSerialObject, COM_Init failed/n/r")));
  120.         LocalFree(pSerialHead);
  121.         return(NULL);
  122.     }
  123.     DEBUGMSG (ZONE_INIT, (TEXT("About to call HWInit(%s,0x%X)/r/n"),
  124.                           Identifier, pSerialHead));
  125. //其实下面执行的是SerInit函数(由一个表完成了初始化的)
  126.     pHWHead = pSerialHead->pHWObj->pFuncTbl->HWInit(Identifier, pSerialHead, pSerialHead->pHWObj);
  127.     pSerialHead->pHWHead = pHWHead;
  128.     /* Check that HWInit did stuff ok.  From here on out, call Deinit function
  129.      * when things fail.
  130.      */
  131.     if ( !pHWHead ) {
  132.         DEBUGMSG (ZONE_INIT | ZONE_ERROR,
  133.                   (TEXT("Hardware doesn't init correctly, COM_Init failed/r/n")));
  134.         COM_Deinit(pSerialHead);
  135.         return(NULL);
  136.     }
  137.     DEBUGMSG (ZONE_INIT,
  138.               (TEXT("Back from hardware init/r/n")));
  139.     // Allocate at least twice the hardware buffer size so we have headroom
  140.     HWBufferSize        = 2 * pSerialHead->pHWObj->pFuncTbl->HWGetRxBufferSize(pHWHead);
  141.     // Init rx buffer and buffer length here.
  142.     pSerialHead->RxBufferInfo.Length =
  143.     HWBufferSize > RX_BUFFER_SIZE ? HWBufferSize:RX_BUFFER_SIZE;
  144.     pSerialHead->RxBufferInfo.RxCharBuffer =
  145.     LocalAlloc(LPTR, pSerialHead->RxBufferInfo.Length);
  146.     if ( !pSerialHead->RxBufferInfo.RxCharBuffer ) {
  147.         DEBUGMSG(ZONE_INIT|ZONE_ERROR,
  148.                  (TEXT("Error allocating receive buffer, COM_Init failed/n/r")));
  149.         COM_Deinit(pSerialHead);
  150.         return(NULL);
  151.     }
  152.     DEBUGMSG (ZONE_INIT, (TEXT("RxHead init'ed/r/n")));
  153.     RxResetFifo(pSerialHead);
  154.     InitializeCriticalSection(&(pSerialHead->RxBufferInfo.CS));
  155.     InitializeCriticalSection(&(pSerialHead->TxBufferInfo.CS));
  156.     DEBUGMSG (ZONE_INIT, (TEXT("RxBuffer init'ed with start at %x/r/n"),
  157.                           pSerialHead->RxBufferInfo.RxCharBuffer));
  158.     if ( pSerialHead->pHWObj->BindFlags & THREAD_AT_INIT ) {
  159.         // Hook the interrupt and start the associated thread.
  160.         if ( ! StartDispatchThread( pSerialHead ) ) {
  161.             // Failed on InterruptInitialize or CreateThread.  Bail.
  162.             COM_Deinit(pSerialHead);
  163.             return(NULL);        
  164.         }
  165.     }
  166.     // OK, now that everything is ready on our end, give the PDD
  167.     // one last chance to init interrupts, etc.
  168.     (void) pSerialHead->pHWObj->pFuncTbl->HWPostInit( pHWHead );
  169.     DEBUGMSG (ZONE_INIT | ZONE_FUNCTION, (TEXT("-COM_Init/r/n")));
  170.     return(pSerialHead);//返回结构体句柄,给设备管理器
  171. }

com_init这个函数参数是注册表的键值,并返回pSerialHead指向的结构体的指针。该函数最重要的是

pSerialHead指向的结构体,现在来看看

  1. // @struct  HW_INDEP_INFO | Hardware Independent Serial Driver Head Information.
  2. typedef struct __HW_INDEP_INFO {
  3.     CRITICAL_SECTION    TransmitCritSec1;       // @field Protects tx action
  4.     CRITICAL_SECTION    ReceiveCritSec1;        // @field Protects rx action
  5.     PHWOBJ          pHWObj;         // @field Represents PDD object.
  6.     PVOID           pHWHead;        // @field Device context for PDD.
  7.     
  8.     HANDLE          hSerialEvent;   // @field Serial event, both rx and tx
  9.     HANDLE          hReadEvent;     // @field Serial event, both rx and tx
  10.     HANDLE          hKillDispatchThread;    // @field Synchonize thread end
  11.     HANDLE          hTransmitEvent; // @field transmit event, both rx and tx
  12.     HANDLE          pDispatchThread;// @field ReceiveThread 
  13.     ULONG           Priority256;    // @field CeThreadPriority of Dispatch Thread.
  14.     ULONG           DroppedBytesMDD;// @field Record of bytes dropped by MDD.
  15.     ULONG           DroppedBytesPDD;// @field Record of bytes dropped by PDD.
  16.     ULONG           RxBytes;        // @field Record of total bytes received.
  17.     ULONG           TxBytes;        // @field Record of total bytes transmitted.
  18.     ULONG           TxBytesPending; // @field Record of total bytes awaiting transmit.
  19.     ULONG           TxBytesSent;    // @field Record of bytes sent in one transmission
  20.     DCB             DCB;            // @field DCB (see Win32 Documentation.
  21.     COMMTIMEOUTS    CommTimeouts;   // @field Time control field. 
  22.     DWORD           OpenCnt;        // @field Protects use of this port 
  23.     DWORD           KillRxThread:1; // @field Flag to terminate SerialDispatch thread.
  24.     DWORD           XFlow:1;        // @field True if Xon/Xoff flow ctrl.
  25.     DWORD           StopXmit:1;     // @field Stop transmission flag.
  26.     DWORD           SentXoff:1;     // @field True if XOFF sent.
  27.     DWORD           DtrFlow:1;      // @field True if currently DTRFlowed   
  28.     DWORD           RtsFlow:1;      // @field True if currently RTSFlowed
  29.     DWORD           fAbortRead:1;   // @field Used for PURGE
  30.     DWORD           fAbortTransmit:1;// @field Used for PURGE
  31.     DWORD           Reserved:24;    // @field remaining bits.
  32.     ULONG           fEventMask;     // @field Sum of event mask for all opens
  33.     RX_BUFFER_INFO  RxBufferInfo;   // @field rx buffer info.
  34.     TX_BUFFER_INFO  TxBufferInfo;   // @field tx buffer info.
  35.     LIST_ENTRY      OpenList;       // @field Head of linked list of OPEN_INFOs    
  36.     CRITICAL_SECTION OpenCS;        // @field Protects Open Linked List + ref counts
  37.     PHW_OPEN_INFO   pAccessOwner;   // @field Points to whichever open has acess permissions
  38. } HW_INDEP_INFO, *PHW_INDEP_INFO;

 

现在来逐一分析这个结构体的成员。

————————————————

PHWOBJ          pHWObj;         // @field Represents PDD object. 原型在Serhw.h

  1. typedef struct __HWOBJ {
  2.     ULONG   BindFlags;//Flags controlling MDD behaviour.  Se above.
  3.    DWORD dwIntID;//Interrupt Identifier used if THREAD_AT_INIT or THREAD_AT_OPEN
  4.     PHW_VTBL    pFuncTbl;
  5.     } HWOBJ, *PHWOBJ;

在com_init函数中有pSerialHead->pHWObj = GetSerialObject( DevIndex );GetSerialObject函数参数其实是注册表中的

DeviceArrayIndex的值,这相当于串口的编号。现在来看看GetSerialObject的原型

 

  1. // GetSerialObj : The purpose of this function is to allow multiple PDDs to be
  2. // linked with a single MDD creating a multiport driver.  In such a driver, the
  3. // MDD must be able to determine the correct vtbl and associated parameters for
  4. // each PDD.  Immediately prior to calling HWInit, the MDD calls GetSerialObject
  5. // to get the correct function pointers and parameters.
  6. //
  7. extern "C" PHWOBJ
  8. GetSerialObject(
  9.                DWORD DeviceArrayIndex
  10.                )
  11. {
  12.     PHWOBJ pSerObj;
  13.     // Unlike many other serial samples, we do not have a statically allocated
  14.     // array of HWObjs.  Instead, we allocate a new HWObj for each instance
  15.     // of the driver.  The MDD will always call GetSerialObj/HWInit/HWDeinit in
  16.     // that order, so we can do the alloc here and do any subsequent free in
  17.     // HWDeInit.
  18.     // Allocate space for the HWOBJ.
  19.     pSerObj=(PHWOBJ)LocalAlloc( LPTR ,sizeof(HWOBJ) );
  20.     if ( !pSerObj )
  21.         return (NULL);
  22.     // Fill in the HWObj structure that we just allocated.
  23. //BindFlags 表示在哪里启动串口驱动的IST线程
  24.     pSerObj->BindFlags = THREAD_IN_PDD;     // PDD create thread when device is first attached.
  25.     pSerObj>dwIntID = DeviceArrayIndex;   // Only it is useful when set set THREAD_AT_MDD. We use this to transfer DeviceArrayIndex
  26.     pSerObj->pFuncTbl = (HW_VTBL *) &IoVTbl; // Return pointer to appropriate functions
  27. //IoVTbl这个数组很重要,在中间层起承上启下的作用,
  28.     // Now return this structure to the MDD.
  29.     return (pSerObj);
  30. }

GetSerialObject函数获得了串口的编号,以及负责和中间层联系。在4.2下增加串口是要修改这个函数的,现在

放在微软的源代码下是否不妥当?难道5.0另有安排?

 

pSerObj->pFuncTbl = (HW_VTBL *) &IoVTbl; // Return pointer to appropriate functions 这个很关键

  1. 不知道怎么回事,为什么要实现那么多的层,这个IoVTbl初始化HW_VTBL 指向的结构体,何必多次一举,为什么还要改变个名字,用原来的名字不是更直观吗??不会是我C语言还没有达到水准吧?
  2. const
  3. HW_VTBL IoVTbl = {
  4.     SerInit,
  5.     SerPostInit,
  6.     SerDeinit,
  7.     SerOpen,
  8.     SerClose,
  9.     SerGetInterruptType,
  10.     SerRxIntr,
  11.     SerTxIntrEx,
  12.     SerModemIntr,
  13.     SerLineIntr,
  14.     SerGetRxBufferSize,
  15.     SerPowerOff,
  16.     SerPowerOn,
  17.     SerClearDTR,
  18.     SerSetDTR,
  19.     SerClearRTS,
  20.     SerSetRTS,
  21.     SerEnableIR,
  22.     SerDisableIR,
  23.     SerClearBreak,
  24.     SerSetBreak,
  25.     SerXmitComChar,
  26.     SerGetStatus,
  27.     SerReset,
  28.     SerGetModemStatus,
  29.     SerGetCommProperties,
  30.     SerPurgeComm,
  31.     SerSetDCB,
  32.     SerSetCommTimeouts,
  33.     SerIoctl
  34.     };

 

  1. 上面的函数表其实是实现了下面结构体内的函数(结构体赋值,相当巧妙),这个函数表只是实现,但是调用的却是下面的函数,刚开始还真反映不过来。为了分层,太TMD了,
  2. typedef struct __HW_VTBL    {
  3.     PVOID   (*HWInit)(ULONG Identifier, PVOID pMDDContext);
  4.     ULONG   (*HWDeinit)(PVOID pHead);
  5.     BOOL    (*HWOpen)(PVOID pHead);
  6.     ULONG   (*HWClose)(PVOID pHead);
  7.     ULONG   (*HWGetBytes)(PVOID pHead, PUCHAR pTarget, PULONG pBytes);
  8.     PVOID   (*HWGetRxStart)(PVOID pHead);
  9.     INTERRUPT_TYPE (*HWGetIntrType)(PVOID pHead);
  10.     VOID    (*HWOtherIntrHandler)(PVOID pHead);
  11.     VOID    (*HWLineIntrHandler)(PVOID pHead);
  12.     ULONG   (*HWGetRxBufferSize)(PVOID pHead);
  13.     VOID    (*HWTxIntrHandler)(PVOID pHead);
  14.     ULONG   (*HWPutBytes)(PVOID pHead, PUCHAR pSrc, ULONG NumBytes, PULONG pBytesSent);
  15.     BOOL    (*HWPowerOff)(PVOID pHead);
  16.     BOOL    (*HWPowerOn)(PVOID pHead);
  17.     VOID    (*HWClearDTR)(PVOID pHead);
  18.     VOID    (*HWSetDTR)(PVOID pHead);
  19.     VOID    (*HWClearRTS)(PVOID pHead);
  20.     VOID    (*HWSetRTS)(PVOID pHead);
  21.     BOOL    (*HWEnableIR)(PVOID pHead, ULONG BaudRate);
  22.     BOOL    (*HWDisableIR)(PVOID pHead);
  23.     VOID    (*HWClearBreak)(PVOID pHead);
  24.     VOID    (*HWSetBreak)(PVOID pHead);
  25.     BOOL    (*HWXmitComChar)(PVOID pHead, UCHAR ComChar);
  26.     ULONG   (*HWGetStatus)(PVOID pHead, LPCOMSTAT lpStat);
  27.     VOID    (*HWReset)(PVOID pHead);
  28.     VOID    (*HWGetModemStatus)(PVOID pHead, PULONG pModemStatus);
  29.     VOID    (*HWGetCommProperties)(PVOID pHead, LPCOMMPROP pCommProp);
  30.     VOID    (*HWPurgeComm)(PVOID pHead, DWORD fdwAction);
  31.     BOOL    (*HWSetDCB)(PVOID pHead, LPDCB pDCB);
  32.     BOOL    (*HWSetCommTimeouts)(PVOID pHead, LPCOMMTIMEOUTS lpCommTO);
  33.     BOOL    (*HWIoctl)(PVOID pHead, DWORD dwCode,PBYTE pBufIn,DWORD dwLenIn,
  34.     PBYTE pBufOut,DWORD dwLenOut,PDWORD pdwActualOut);
  35.     } HW_VTBL, *PHW_VTBL;

现在来看看在com_init中调用的HWInit——从后面的分析可以知道,其实调用的是SerInit函数

//--------------------------------------------------------------------------------------------------------
//这个函数在MDD和PDD之间按照微软的命名SerXXX,应该叫做服务层
// Converting C Style to C++ Serial Object.
/*
 @doc OEM
 @func PVOID | SerInit | Initializes device identified by argument.
 *  This routine sets information controlled by the user
 *  such as Line control and baud rate. It can also initialize events and
 *  interrupts, thereby indirectly managing initializing hardware buffers.
 *  Exported only to driver, called only once per process.
 *
 @rdesc The return value is a PVOID to be passed back into the HW
 dependent layer when HW functions are called.
 */
PVOID
SerInit(
       ULONG   Identifier, // @parm Device identifier.
       PVOID   pMddHead,   // @parm First argument to mdd callbacks.
       PHWOBJ  pHWObj      // @parm Pointer to our own HW OBJ for this device
       )
{
    DEBUGMSG (ZONE_CLOSE,(TEXT("+SerInit, 0x%X/r/n"), Identifier));
    CSerialPDD * pSerialPDD = NULL;
    if (pHWObj) {
        DWORD dwIndex= pHWObj->dwIntID;
        pHWObj->dwIntID = 0;
        pSerialPDD = CreateSerialObject((LPTSTR)Identifier,pMddHead, pHWObj,dwIndex);
    }
    if (pSerialPDD==NULL) {
        ASSERT(FALSE);
        LocalFree(pHWObj);
    }
    DEBUGMSG (ZONE_CLOSE,(TEXT("-SerInit, 0x%X/r/n"), pSerialPDD));
    return pSerialPDD;
}

————————————————————————————————————————

//要增加串口的话,就要修改这个函数——增加几个case即可,哈哈,不用修改GetSerialObject了

//这个函数在BSP下的

CSerialPDD * CreateSerialObject(LPTSTR lpActivePath, PVOID pMdd,PHWOBJ pHwObj, DWORD DeviceArrayIndex)
{
    CSerialPDD * pSerialPDD = NULL;
    switch (DeviceArrayIndex) {
      case 0:
        pSerialPDD = new CPdd2440Serial1(lpActivePath,pMdd, pHwObj);
        break;
      case 1:
        pSerialPDD = new CPdd2440Serial2(lpActivePath,pMdd, pHwObj);
        break;
    }
    if (pSerialPDD && !pSerialPDD->Init()) {
        delete pSerialPDD;
        pSerialPDD = NULL;
    }   
    return pSerialPDD;
}


 

    其实其他的串口接口函数com_Read等等,起实现方法都和com_init一致,都是调用上面结构体的函数实现了,

使用了从上到下的设计方法。在这里就不再分析了。现在开始看PDD层。

————————————————————————————————————————————————

转载请标明:作者wogoyixikexie@gliet.桂林电子科技大学一系科协,原文地址:http://blog.csdn.net/gooogleman——如有错误,希望能够留言指出;如果你有更加好的方法,也请在博客后面留言,我会感激你的批评和分享。
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值