日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

电池驱动介绍

發布時間:2023/12/15 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 电池驱动介绍 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

?

電池驅動介紹

一.整體框架

???????? 電池驅動代碼量很小,可是麻雀雖小,五臟俱全。與其他的很多Driver一樣,分為PDD+MDD層,雙層之間通過PDD的如下導出接口相聯系。????

Programming element

Description

BatteryDrvrGetLevels

This function returns the number of levels that the battery driver is capable of returning in the BatteryFlag and BackupBatteryFlag members of the SYSTEM_POWER_STATUS_EX2 structure.

BatteryDrvrSupportsChangeNotification

This function indicates whether the battery driver can report whether the batteries were changed.

BatteryGetLifeTimeInfo

This function retrieves the time the user changed the batteries, the amount of time they used the batteries, and the amount of time they used the batteries before replacing them.

BatteryNotifyOfTimeChange

This function adjusts times to account for the user changing the real time.

BatteryPDDDeinitialize

This function allows the battery PDD to perform hardware-specific cleanup.

BatteryPDDGetLevels

This function indicates how many battery levels are reported in the BatteryFlag and BackupBatteryFlag members of the SYSTEM_POWER_STATUS_EX2 structure filled in by BatteryPDDGetStatus.

BatteryPDDGetStatus

This function obtains the most current battery and power status available on the platform. It fills in the structures pointed to by its parameters.

BatteryPDDInitialize

This function allows the battery PDD to perform hardware-specific initialization.

BatteryPDDPowerHandler

This power callback performs hardware-specific processing for the battery driver.

BatteryPDDResume

This function performs hardware-specific battery processing in a thread context following system resume.

BatteryPDDSupportsChangeNotification

This function indicates whether the battery driver can report whether the batteries were changed.

PFN_BATTERY_PDD_IOCONTROL

This function signature is for the battery driver custom IOCTL handler. It implements the optional PDD IOCTL interface.

???????? 微軟提供了電池驅動的Sample Code,從目錄/WINCE600/PUBLIC/COMMON/OAK/DRIVERS

/BATTDRVR下可以找到。

???????? 注冊表的配置如下:

IF BSP_NOBATTERY !

?

; HIVE BOOT SECTION

?

[HKEY_LOCAL_MACHINE/System/Events]

??? "SYSTEM/BatteryAPIsReady"="Battery Interface APIs"

?

; END HIVE BOOT SECTION

?

; These registry entries load the battery driver.? The IClass value must match

; the BATTERY_DRIVER_CLASS definition in battery.h -- this is how the system

; knows which device is the battery driver.? Note that we are using

; DEVFLAGS_NAKEDENTRIES with this driver.? This tells the device manager

; to instantiate the device with the prefix named in the registry but to look

; for DLL entry points without the prefix.? For example, it will look for Init

; instead of BAT_Init.? This allows the prefix to be changed in the registry (if

; desired) without editing the driver code.

[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Battery]

?? "Prefix"="BAT"

?? "Dll"="battdrvr.dll"

?? "Flags"=dword:8????????????????????? ; DEVFLAGS_NAKEDENTRIES

?? "Order"=dword:0

?? "IClass"="{DD176277-CD34-4980-91EE-67DBEF3D8913}"

?

ENDIF BSP_NOBATTERY !

???????? PDD層基本上就是實現電池電量的采集和電池一些其它基本信息的獲取,而MDD層主要是建立了一個線程BatteryThreadProc,該線程用來和控制面板中電源管理小工具進行通信。

二.值得一說的問題

???????? 電池驅動實在是太簡單了,沒啥可介紹的。

???????? 就說說常見的問題吧。

1.電池驅動的導出接口沒有Prefix

???????? 一般的流接口驅動導出接口都會有一個前綴,即注冊表中配置的Prefix的值,為三個字節的大寫字母。

???????? 這是注冊表中”Flags”的值配置成8DEVFLAGS_NAKEDENTRIES)引起的,在這種情況下Device Manager操作流接口驅動程序的時候,就可以不需要Prefix前導符號。

???????? 詳細的”Flags”的配置如下:

Flag

Value

Description

DEVFLAGS_NONE

0x00000000

No flags are defined.

DEVFLAGS_UNLOAD

0x00000001

Driver unloads after a call to the XXX_Init entry point or after the XXX_Init entry point returns. No error code is returned.

Bus Enumerator typically runs with this flag.

DEVFLAGS_LOADLIBRARY

0x00000002

Driver is loaded with LoadLibrary instead of LoadDriver.

DEVFLAGS_NOLOAD

0x00000004

Driver is not loaded.

DEVFLAGS_NAKEDENTRIES

0x00000008

Driver entry points do not have a XXX Prefix prepended.

DEVFLAGS_BOOTPHASE_1

0x00001000

Driver is loaded during boot phase one. By default, device drivers are loaded during boot phase two.

Boot phase zero is before the Device Manager loads.

Boot phase one is to find the registry.

Boot phase two is when initial device drivers load.

Boot phase three is after initial device drivers load.

DEVFLAGS_IRQ_EXCLUSIVE

0x00000100

Driver loads only when it has exclusive access to the IRQ.

DEVFLAGS_TRUSTEDCALLERONLY

0x00010000

Driver can only be opened by a trusted application.

???????? 接下來,我們從Device Manager的代碼中找到其原因。通常在應用程序或者一些Driver中嘗試去動態加載流驅動的話,可以去調用API ActivateDeviceEx(),其實該函數最終調用的就是Device Manager中的函數I_ActivateDeviceEx()

???????? 在文件/WINCE600/PRIVATE/WINCEOS/COREOS/DEVICE/DEVCORE/ devload.c中可以找到函數I_ActivateDeviceEx()的具體實現,該函數主要完成驅動程序對應的DLL的加載和初始化函數的調用,它首先會去調用函數CreateDevice()創建設備的一些結構體信息。在函數CreateDevice()中可以找到對”Flags”的一些判斷處理。

???????? 代碼如下:

// This routine allocates a device driver structure in memory and initializes it.

// As part of this process, it loads the driver's DLL and obtains pointers to

// its entry points.? This routine returns a pointer to the new driver description

// structure, or NULL if there's an error.

static fsdev_t *

CreateDevice(

??? LPCWSTR lpszPrefix,

??? DWORD dwIndex,

??? DWORD dwLegacyIndex,

??? DWORD dwId,

??? LPCWSTR lpszLib,

??? DWORD dwFlags,

??? LPCWSTR lpszBusPrefix,

??? LPCWSTR lpszBusName,

??? LPCWSTR lpszDeviceKey,

??? HANDLE hParent

??? )

{

??? fsdev_t *lpdev;

??? DWORD dwSize;

??? DWORD dwStatus = ERROR_SUCCESS;

??? WCHAR szDeviceName[MAXDEVICENAME];

??? WCHAR szLegacyName[MAXDEVICENAME];

?

??? DEBUGCHK(lpszPrefix != NULL);

??? DEBUGCHK(lpszLib != NULL);

??? DEBUGCHK(wcslen(lpszPrefix) <= 3);

??? DEBUGCHK(dwLegacyIndex == dwIndex || (dwLegacyIndex == 0 && dwIndex == 10));

??? DEBUGCHK(lpszBusName != NULL);

?

??? // figure out how much memory to allocate

??? dwSize = sizeof(*lpdev);

?

??? // is the device named?

??? if(lpszPrefix[0] == 0) {

??????? // unnamed device

??????? szDeviceName[0] = 0;

??? } else {

??????? // named device, allocate room for its names

??????? StringCchPrintf(szDeviceName,MAXDEVICENAME,TEXT("%s%u"), lpszPrefix, dwIndex);

??????? if(dwLegacyIndex <= 9) {

??????????? // allocate room for name and null

??????????? StringCchPrintf(szLegacyName, MAXDEVICENAME, L"%s%u:", lpszPrefix, dwLegacyIndex);

????? ??????dwSize += (wcslen(szLegacyName) + 1) * sizeof(WCHAR);

??????? }

?

??????? // allocate room for name and null???????

??????? dwSize += (wcslen(szDeviceName) + 1) * sizeof(WCHAR);

??? }

?

??? // If the bus driver didn't allocate a name the device may still support the

??? // bus name interface -- use its device name (if present) just in case.

??? if(lpszBusName[0] == 0 && szDeviceName[0] != 0) {

??????? lpszBusName = szDeviceName;

??? }

???

??? // allocate room for the bus name

??? if(lpszBusName[0] != 0) {

??????? dwSize += (wcslen(lpszBusName) + 1) * sizeof(WCHAR);

??? }

?

??? // make room to store the device key as well

??? if(lpszDeviceKey != NULL) {

??????? dwSize += (wcslen(lpszDeviceKey) + 1) * sizeof(WCHAR);

??? }

???

??? // allocate the structure

??? if (!(lpdev = LocalAlloc(0, dwSize))) {

??????? DEBUGMSG(ZONE_WARNING, (_T("DEVICE!CreateDevice: couldn't allocate device structure/r/n")));

??????? dwStatus = ERROR_OUTOFMEMORY;

??? } else {

??????? LPCWSTR pEffType = NULL;

??????? LPWSTR psz = (LPWSTR) (((LPBYTE) lpdev) + sizeof(*lpdev));

??????? memset(lpdev, 0, dwSize);

??????? lpdev->dwId = dwId;

??????? lpdev->wFlags = 0;???????????

??????? lpdev->dwFlags = dwFlags;

??????? if(PSLGetCallerTrust() == OEM_CERTIFY_TRUST) {

??????????? lpdev->wFlags |= DF_TRUSTED_LOADER;

??????????? lpdev->hParent = hParent;

??????? }

??????? if (lpszPrefix[0] != 0) {

??????????? if(dwLegacyIndex <= 9) {

??????????????? lpdev->pszLegacyName = psz;

??????????????? wcscpy(lpdev->pszLegacyName, szLegacyName);

??????? ????????psz += wcslen(lpdev->pszLegacyName) + 1;

??????????? }

??????????? lpdev->pszDeviceName = psz;

??????????? wcscpy(lpdev->pszDeviceName, szDeviceName);

??????????? psz += wcslen(lpdev->pszDeviceName) + 1;

??????? }

??????? if(lpszBusName[0] != 0) {

??????????? lpdev->pszBusName = psz;

??????????? wcscpy(lpdev->pszBusName, lpszBusName);

??????????? psz += wcslen(lpszBusName) + 1;

??????? }

??????? if(lpszDeviceKey != NULL) {

??????????? lpdev->pszDeviceKey= psz;

??????????? wcscpy(lpdev->pszDeviceKey, lpszDeviceKey);

??????????? psz += wcslen(lpszDeviceKey) + 1;

??????? }

??????? if((dwFlags & DEVFLAGS_NAKEDENTRIES) == 0) {

??????????? if(lpszPrefix[0] != 0) {

??????????????? DEBUGCHK(lpszBusPrefix[0] == 0 || wcsicmp(lpszBusPrefix, lpszPrefix) == 0);

??????????????? pEffType = lpszPrefix;????? // use standard prefix decoration

??????????? } else if(lpszBusPrefix[0] != 0 && lpdev->pszBusName != NULL) {

??????????????? pEffType = lpszBusPrefix;?? // no standard prefix, use bus prefix decoration

????????? ??} else {

??????????????? if(lpdev->pszDeviceName != NULL) {

??????????????????? // device is expected to have a device or bus name, but we don't know

??????????????????? // how to look for its entry points

??????????????????? DEBUGMSG(ZONE_ACTIVE || ZONE_ERROR,

??????????????????????? (_T("DEVICE!CreateDevice: no entry point information for '%s' can't load '%s'/r/n"),

??????????????????????? lpszLib, lpdev->pszDeviceName));

??????????????????? dwStatus = ERROR_INVALID_FUNCTION;

??????????????? }

?????? ?????}

??????? }

??????? // 這里會去判斷Flags的值,決定是否使用Prefix

??????? if ((dwFlags & DEVFLAGS_LOAD_AS_USERPROC)) {

??????????? lpdev->hLib = NULL;

??????????? lpdev->dwData? = Reflector_Create(lpszDeviceKey, pEffType, lpszLib, dwFlags );

??????????? if (lpdev->dwData != 0 ) {

??????????????? lpdev->fnInit = NULL;

??????????????? lpdev->fnInitEx = (pInitExFn)Reflector_InitEx;

??????????????? lpdev->fnPreDeinit = (pDeinitFn)Reflector_PreDeinit;

??????????????? lpdev->fnDeinit = (pDeinitFn)Reflector_Deinit;

??????????????? lpdev->fnOpen = (pOpenFn)Reflector_Open;

??????????????? lpdev->fnPreClose = (pCloseFn)Reflector_PreClose;

??????????????? lpdev->fnClose = (pCloseFn)Reflector_Close;

??????????????? lpdev->fnRead = (pReadFn)Reflector_Read;

??????????????? lpdev->fnWrite = (pWriteFn)Reflector_Write;

??????????????? lpdev->fnSeek = (pSeekFn)Reflector_SeekFn;

??????????????? lpdev->fnControl = (pControlFn)Reflector_Control;

??????????????? lpdev->fnPowerup = (pPowerupFn)Reflector_Powerup;

??????????????? lpdev->fnPowerdn = (pPowerupFn)Reflector_Powerdn;

??????????? }

??????????? else {

??????????????? DEBUGMSG(ZONE_WARNING, (_T("DEVICE!CreateDevice: couldn't load(%s) to user mode!!/r/n"),lpszLib));

??????????????? dwStatus = ERROR_FILE_NOT_FOUND;

??????????? }

??????? }

??????? else {

??????????? DEBUGMSG(ZONE_ACTIVE, (_T("DEVICE!CreateDevice: loading driver DLL '%s'/r/n"), lpszLib));

??????????? // 這里去判斷的如何去加載Stream Driver,其實這里最終會影響到驅動DLL需要的內存的Page-inpage-out,這些不是這里要說的重點,暫且不說

??????????? lpdev->hLib =

??????????????? (dwFlags & DEVFLAGS_LOADLIBRARY) ? LoadLibrary(lpszLib) : LoadDriver(lpszLib);

??????????? if (!lpdev->hLib) {

??????????????? DEBUGMSG(ZONE_WARNING, (_T("DEVICE!CreateDevice: couldn't load '%s' -- error %d/r/n"),

??????????????????? lpszLib, GetLastError()));

???????? ???????dwStatus = ERROR_FILE_NOT_FOUND;

??????????? } else {

?? ?????????????// 最終如果配置為0x08,程序將會走到這里

??????????????? lpdev->fnInitEx = NULL;

??????????????? lpdev->fnInit = (pInitFn)GetDMProcAddr(pEffType,L"Init",lpdev->hLib);

??????????????? lpdev->fnPreDeinit = (pDeinitFn)GetDMProcAddr(pEffType,L"PreDeinit",lpdev->hLib);

??????????????? lpdev->fnDeinit = (pDeinitFn)GetDMProcAddr(pEffType,L"Deinit",lpdev->hLib);

??????????????? lpdev->fnOpen = (pOpenFn)GetDMProcAddr(pEffType,L"Open",lpdev->hLib);

??????????????? lpdev->fnPreClose = (pCloseFn)GetDMProcAddr(pEffType,L"PreClose",lpdev->hLib);

??????????????? lpdev->fnClose = (pCloseFn)GetDMProcAddr(pEffType,L"Close",lpdev->hLib);

??????????????? lpdev->fnRead = (pReadFn)GetDMProcAddr(pEffType,L"Read",lpdev->hLib);

??????????????? lpdev->fnWrite = (pWriteFn)GetDMProcAddr(pEffType,L"Write",lpdev->hLib);

??????????????? lpdev->fnSeek = (pSeekFn)GetDMProcAddr(pEffType,L"Seek",lpdev->hLib);

??????????????? lpdev->fnControl = (pControlFn)GetDMProcAddr(pEffType,L"IOControl",lpdev->hLib);

??????????????? lpdev->fnPowerup = (pPowerupFn)GetDMProcAddr(pEffType,L"PowerUp",lpdev->hLib);

??????????????? lpdev->fnPowerdn = (pPowerdnFn)GetDMProcAddr(pEffType,L"PowerDown",lpdev->hLib);

?

??????????????? // Make sure that the driver has an init and deinit routine.? If it is named,

??????????? ????// it must have open and close, plus at least one of the I/O routines (read, write

??????????????? // ioctl, and/or seek).? If a named driver has a pre-close routine, it must also

??????????????? // have a pre-deinit routine.

??????????????? if (!(lpdev->fnInit && lpdev->fnDeinit) ||

??????????????????? lpdev->pszDeviceName != NULL && (!lpdev->fnOpen ||

???????????????????????????????? !lpdev->fnClose ||

???????????????????????????????? (!lpdev->fnRead && !lpdev->fnWrite &&

?????????????????????????? ???????!lpdev->fnSeek && !lpdev->fnControl) ||

???????????????????????????????? (lpdev->fnPreClose && !lpdev->fnPreDeinit))) {

??????????????????? DEBUGMSG(ZONE_WARNING, (_T("DEVICE!CreateDevice: illegal entry point combination in driver DLL '%s'/r/n"),

??????????????????????? lpszLib));

??????????????????? dwStatus = ERROR_INVALID_FUNCTION;

??????????????? }

?

??????????????? if (!lpdev->fnOpen) lpdev->fnOpen = (pOpenFn) DevFileNotSupportedBool;

??????????????? if (!lpdev->fnClose) lpdev->fnClose = (pCloseFn) DevFileNotSupportedBool;

??????????????? if (!lpdev->fnControl) lpdev->fnControl = (pControlFn) DevFileNotSupportedBool;

??????????????? if (!lpdev->fnRead) lpdev->fnRead = (pReadFn) DevFileNotSupportedDword;

??????????????? if (!lpdev->fnWrite) lpdev->fnWrite = (pWriteFn) DevFileNotSupportedDword;

??????????????? if (!lpdev->fnSeek) lpdev->fnSeek = (pSeekFn) DevFileNotSupportedDword;

??????????? }

??????? }

??? }

?

??? // did everything go ok?

??? if(dwStatus != ERROR_SUCCESS) {

??????? if(lpdev != NULL) {

??????????? DeleteDevice(lpdev);

??????????? lpdev = NULL;

??????? }

??????? SetLastError(dwStatus);

??? }

?

??? DEBUGMSG(ZONE_ACTIVE || (dwStatus != ERROR_SUCCESS && ZONE_WARNING),

??????? (_T("CreateDevice: creation of type '%s', index %d, lib '%s' returning 0x%08x, error code %d/r/n"),

??????? lpszPrefix[0] != 0 ? lpszPrefix : _T("<unnamed>"), dwIndex, lpszLib, lpdev, dwStatus));

?

??? return lpdev;

}

//

// This routine is security check for dwInfo passed in by either RegistryDevice or ActiveDevice

DWORD CheckLauchDeviceParam(DWORD dwInfo)

{

??? if (CeGetCallerTrust() != OEM_CERTIFY_TRUST) { // Untrusted caller will do following.

??????? LPCTSTR lpActivePath = (LPCTSTR) dwInfo; // We assume it is Registry Path.

??????? if (lpActivePath) {

??????????? HKEY hActiveKey;

??????????? if (RegOpenKeyEx( HKEY_LOCAL_MACHINE, lpActivePath, 0, 0, &hActiveKey) == ERROR_SUCCESS ) { // It is registry.

??????????????? // We need check Registry is in secure location.

??????????????? CE_REGISTRY_INFO regInfo;

??????????????? DWORD dwRet = ERROR_INVALID_PARAMETER;

??????????????? memset(&regInfo,0,sizeof(regInfo));

??????????????? regInfo.cbSize = sizeof(CE_REGISTRY_INFO);

??????????????? if (CeFsIoControl(NULL, FSCTL_GET_REGISTRY_INFO, &hActiveKey, sizeof(HKEY), &regInfo, sizeof(CE_REGISTRY_INFO), NULL, NULL)) { // Succeed

??????????????????? if (regInfo.dwFlags & CE_REG_INFO_FLAG_TRUST_PROTECTED) {

??????????????????????? dwRet = ERROR_SUCCESS;

??????????????????? }

??????????????? }

??????????????? RegCloseKey( hActiveKey );

??????????????? return dwRet;

??????????? }

??????? }

??? }

??? return ERROR_SUCCESS;?

}

???????? 另外,注冊表項”Flags”的值是函數I_ActivateDeviceEx()調用RegReadActivationValues()來獲取的。篇幅有限,這里不再對函數RegReadActivationValues()進行解釋。

???????? 其實注冊表項”Flags”的值很有用處,能夠控制加載過程中的很多行為。

2.電池電量更新的問題

???????? CE5.0中,默認情況下系統會每0.5s去獲取一次主電池和輔助電池的電量,其默認值得定義在文件battdrvr.c中,如下:

#define DEF_BATTERYPOLLTIMEOUT????????? 500???????? // in milliseconds

?

???????? 6.0中,可能微軟也覺得0.5s獲取一次電池電量有點變態,而且也沒有必要,所以改為5s去獲取一次電量,同樣通過宏定義進行定義。

// 5 Seconds for average delay of 2.5 seconds

#define DEF_BATTERYPOLLTIMEOUT????????? (5*1000)???????? // in milliseconds

???????? 當然,你可以通過注冊表項"PollInterval"去配置這個值,如下:

[HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Battery]

"Prefix"="BAT"

"Dll"="battdrvr.dll"

"Flags"=dword:8????????????????????? ; DEVFLAGS_NAKEDENTRIES

"Order"=dword:45

"IClass"="{DD176277-CD34-4980-91EE-67DBEF3D8913}"

"PollInterval"=dword:1388

???????? 其實,就算是用戶去通過控制面板去查看電池電量,也沒有必要把這個時間設置的很短。再者,一般的手持設備上都會去調用API? GetSystemPowerStatusEx()去獲取電池電量百分比。

???????? 毫無疑問,將更新電池電量的時間配置的越大越好,可是有些時候,我們可能希望在用戶插入AC進行充電的時候,控制面板中的電池電量馬上有反應,而不是在等待5s鐘之后,這樣顯得產品更加人性化一點。這種情況下,可以通過使AC的插入產生一個中斷,然后把這個中斷和電池驅動MDD層中的線程BatteryThreadProc Event聯系起來,這樣就可以滿足插入AC,控制面板中的電池電量馬上就有反應的需求。

3.電池電量顯示不準確

???????? 最簡單的電池電量的獲取是通過當前AD值和滿電量AD值相比得到的。舉個例子,假設滿電量的時候AD值為100,當前的值為50,則當前的電量就是50/100*100% = 50%,并通過PDD層函數BatteryPDDGetStatus()返回給上層調用者。

???????? 實際使用中發現,電池電量為100%的時候很耐用,可是從電池電量80%左右的時候開始,電池電量很快就被消耗完畢。

???????? 為什么?

???????? 根本原因是,電池電量和電池電壓并不成正比,而AD轉換的結果恰巧反映的就是電池電壓。

???????? 了解了電池的充放電原理后發現,電池電量和電池電壓的關系是一條拋物線。接下來就有兩種改進的方法獲取電池電量:

???????? 第一種方法:得到電池電量和電池電壓的準確關系。

???????? 事先去測量電池電量和電池電壓的關系,得到這條拋物線的方程,從而得到準確的電池電量和電池電壓的關系。通常不是計算這條拋物線,而是將這條拋物線簡單的分隔成幾個段,近似的認為每一段是一條斜率恒定的直線,然后通過測量結果確定這幾條直線的斜率以及合時的分隔點。

???????? 做過衡器或者其它一些儀器儀表的朋友,對這種方法肯定不陌生,呵呵。

???????? 這種做法的最大缺點是,每更換一種電池都需要重新的去測時充放電曲線,比較麻煩。優點是不用增加硬件成本。

???????? 第二種方法:利用電源管理芯片。

???????? 這種方法沒用過,只是聽有人在論壇上討論過。利用一顆電源管理芯片去獲取電池電量,其原理是通過AD轉換值經過一套復雜的算法得到電量,搞不清楚,據說很好用。但是Cost呵呵

????????

?

總結

以上是生活随笔為你收集整理的电池驱动介绍的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。