【转】android电池(四):电池 电量计(MAX17040)驱动分析篇
關(guān)鍵詞:android電池電量計(jì)MAX17040任務(wù)初始化宏power_supply
平臺(tái)信息:
內(nèi)核:linux2.6/linux3.0
系統(tǒng):android/android4.0
平臺(tái):samsungexynos4210、exynos4412、exynos5250
作者:xubin341719(歡迎轉(zhuǎn)載,請(qǐng)注明作者)
歡迎指正錯(cuò)誤,共同學(xué)習(xí)、共同進(jìn)步!!
完整驅(qū)動(dòng)代碼&規(guī)格書下載:MAX17040_PL2301
android 電池(一):鋰電池基本原理篇
android 電池(二):android關(guān)機(jī)充電流程、充電畫面顯示
android 電池(三):android電池系統(tǒng)
android電池(四):電池 電量計(jì)(MAX17040)驅(qū)動(dòng)分析篇
android電池(五):電池 充電IC(PM2301)驅(qū)動(dòng)分析篇
電池電量計(jì),庫(kù)侖計(jì),用max17040這顆電量IC去計(jì)量電池電量,這種方法比較合理。想起比較遙遠(yuǎn)的年代,做samsungs5pc110/sp5v210的時(shí)候,計(jì)量電量用一個(gè)AD口加兩個(gè)分壓電阻就做了,低電量的時(shí)候系統(tǒng)一直判斷不準(zhǔn)確,“低電關(guān)機(jī)”提示一會(huì)有,一會(huì)沒(méi)有,客戶那個(gè)郁悶呀,“到底是有電還是沒(méi)電?”。
如下圖,通過(guò)兩個(gè)分壓電阻,和一個(gè)AD腳去偵測(cè)VCC(電池)電壓。
一、MAX17040的工作原理
電量計(jì)MAX17040,他通過(guò)芯片去測(cè)量電池電量,芯片本身集成的電路比較復(fù)雜,同時(shí)可以通過(guò)軟件上的一些算法去實(shí)現(xiàn)一些處理,是測(cè)量出的電量更加準(zhǔn)確。還有一個(gè)好處,就是他之接輸出數(shù)字量,通過(guò)IIC直接讀取,我們?cè)陔娐吩O(shè)計(jì)、程序處理上更加的統(tǒng)一化。
如下圖所示,MAX17040和電池盒主控的關(guān)系,一個(gè)AD腳接到電池VBAT+,檢測(cè)到的電量信息,通過(guò)IIC傳到主控。
下面是電路圖,電路接口比較簡(jiǎn)單,VBAT+,接到max17040的CELL,IIC接到主控的IIC2接口,這個(gè)我們?cè)诔绦蛑幸渲谩?催@個(gè)器件比較簡(jiǎn)單吧。
看下max17040的內(nèi)部結(jié)構(gòu),其實(shí)這也是一個(gè)AD轉(zhuǎn)換的過(guò)程,單獨(dú)一顆芯片去實(shí)現(xiàn),這樣看起來(lái)比較專業(yè)些。CELL接口,其實(shí)就是一個(gè)ADC轉(zhuǎn)換的引腳,我們可以看到芯片內(nèi)部有自己的時(shí)鐘(timebase),IIC控制器之類的,通過(guò)CELL采集到的模擬量,轉(zhuǎn)換成數(shù)字量,傳輸給主控。
通過(guò)上面的介紹Max17040的硬件、原理我們基本上都了解了,比較簡(jiǎn)單,下面我們就重點(diǎn)去分析下驅(qū)動(dòng)程序。
二、MAX17040總體流程
電量計(jì)的工作流程比較簡(jiǎn)單,max17040通過(guò)CELLADC轉(zhuǎn)換引腳,把電池的相關(guān)信息,實(shí)時(shí)讀取,存入max17040相應(yīng)的寄存器,驅(qū)動(dòng)申請(qǐng)一個(gè)定時(shí)器,記時(shí)結(jié)束,通過(guò)IIC去讀取電池狀態(tài)信息,和老的電池信息對(duì)比,如果用變化上報(bào),然后重新計(jì)時(shí);這樣循環(huán)操作,流程如下所示:
三、MAX17040這個(gè)電量計(jì)驅(qū)動(dòng),我們主要用到以下知識(shí)點(diǎn)
1、IIC的注冊(cè)(這個(gè)在TP、CAMERA中都有分析);
2、linux中定時(shí)器的使用;
3、任務(wù)初始化宏;
4、linux定時(shí)器調(diào)度隊(duì)列;
5、max17040測(cè)到電量后如何上傳到系統(tǒng)(這個(gè)電池系統(tǒng)中有簡(jiǎn)要的分析);
6、AC、USB充電狀態(tài)的上報(bào),這個(gè)和電池電量是一種方法。
7、電池曲線的測(cè)量與加入;
1、IIC的注冊(cè)
IIC這個(gè)總線,在工作中用的比較多,TP、CAMERA、電量計(jì)、充電IC、音頻芯片、電源管理芯片、基本所有的傳感器,所以這大家要仔細(xì)看下,后面有時(shí)間的話單獨(dú)列一片介紹下IIC,從單片機(jī)時(shí)代都用的比較多,看來(lái)?xiàng)l總線的生命力很強(qiáng),像C語(yǔ)言一樣,很難被同類的東西替代到,至少現(xiàn)在應(yīng)該是這樣的。
看下他結(jié)構(gòu)體的初始化與驅(qū)動(dòng)的申請(qǐng),這個(gè)比較統(tǒng)一,這里就不想想解釋了。
(1)、IIC驅(qū)動(dòng)的注冊(cè):
static const struct i2c_device_id max17040_id[] = {
    { "max17040", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, max17040_id);
static struct i2c_driver max17040_i2c_driver = {
    .driver    = {
        .name    = "max17040",
    },
    .probe        = max17040_probe,
    .remove        = __devexit_p(max17040_remove),
    .suspend    = max17040_suspend,
    .resume        = max17040_resume,
    .id_table    = max17040_id,
};
static int __init max17040_init(void)
{
    printk("MAX17040 max17040_init !!
");
    wake_lock_init(&vbus_wake_lock, WAKE_LOCK_SUSPEND, "vbus_present");
    return i2c_add_driver(&max17040_i2c_driver);
}
module_init(max17040_init);
(2)在arch/arm/mach-exynos/mach-smdk4x12.c中,IC平臺(tái)驅(qū)動(dòng)的注冊(cè):
static struct i2c_board_info i2c_devs2[] __initdata = {
#if defined(CONFIG_BATTERY_MAX17040)
    {
        I2C_BOARD_INFO("max17040", 0x36),//IIC地址;
        .platform_data = &max17040_platform_data,
    },
#endif
……………………
};
下圖就是我們IIC驅(qū)動(dòng)注冊(cè)生成的文件;
/sys/bus/i2c/drivers/max17040
2、linux中定時(shí)器的使用
定時(shí)器,就是定一個(gè)時(shí)間,比如:申請(qǐng)一個(gè)10秒定時(shí)器,linux系統(tǒng)開始計(jì)時(shí),到10秒,請(qǐng)示器清零重新計(jì)時(shí)并發(fā)出信號(hào)告知系統(tǒng)計(jì)時(shí)完成,系統(tǒng)接到這個(gè)信號(hào),做相應(yīng)的處理;
#include <linux/delay.h> #define MAX17040_DELAY msecs_to_jiffies(5000)
3、任務(wù)初始化宏
INIT_WORK(work,func); INTI_DELAYED_WORK(work,func); INIT_DELAYED_WORK_DEFERRABLE(work,func);
任務(wù)結(jié)構(gòu)體的初始化完成后,接下來(lái)要將任務(wù)安排進(jìn)工作隊(duì)列。可采用多種方法來(lái)完成這一操作。首先,利用queue_work簡(jiǎn)單地將任務(wù)安排進(jìn)工作隊(duì)列(這將任務(wù)綁定到當(dāng)前的CPU)。或者,可以通過(guò)queue_work_on來(lái)指定處理程序在哪個(gè)CPU上運(yùn)行。兩個(gè)附加的函數(shù)為延遲任務(wù)提供相同的功能(其結(jié)構(gòu)體裝入結(jié)構(gòu)體work_struct之中,并有一個(gè)計(jì)時(shí)器用于任務(wù)延遲)。
INIT_DELAYED_WORK_DEFERRABLE(&chip->work, max17040_work); 把調(diào)度函數(shù) max17040_work加入chip->work隊(duì)列;
4、linux定時(shí)器調(diào)度隊(duì)列
    INIT_DELAYED_WORK_DEFERRABLE(&chip->work, max17040_work);
    schedule_delayed_work(&chip->work, MAX17040_DELAY);
    通過(guò)定時(shí)器調(diào)度隊(duì)列;
5、max17040測(cè)到電量后如何上傳到系統(tǒng)(這個(gè)電池系統(tǒng)中有簡(jiǎn)要的分析);
4中的定時(shí)器記時(shí)完成,就可以調(diào)度隊(duì)列,chip->work執(zhí)行:max17040_work函數(shù),把改讀取的信息上傳,我們看下max17040_work函數(shù)的實(shí)現(xiàn):
static void max17040_work(struct work_struct *work)
{
    struct max17040_chip *chip;
    int old_usb_online, old_online, old_vcell, old_soc;
    chip = container_of(work, struct max17040_chip, work.work);
#ifdef MAX17040_SUPPORT_CURVE
    /* The module need to be update per hour (60*60)/3 = 1200 */
    if (g_TimeCount >= 1200) {
        handle_model(0);
        g_TimeCount = 0;
    }
    g_TimeCount++;
#endif
    old_online = chip->online;//(1)、保存老的電池信息,如電量、AC、USB是否插入;
    old_usb_online = chip->usb_online;
    old_vcell = chip->vcell;
    old_soc = chip->soc;
    max17040_get_online(chip->client);//(2)、讀取電池新的狀態(tài)信息
    max17040_get_vcell(chip->client);
    max17040_get_soc(chip->client);
    max17040_get_status(chip->client);
    if ((old_vcell != chip->vcell) || (old_soc != chip->soc)) {//(3)、如果電池信息有變化,就上報(bào)系統(tǒng);
        /* printk(KERN_DEBUG "power_supply_changed for battery
"); */
        power_supply_changed(&chip->battery);
    }
#if !defined(CONFIG_CHARGER_PM2301)//(4)、如果用PM2301充電IC,USB充電功能不用;
    if (old_usb_online != chip->usb_online) {
        /* printk(KERN_DEBUG "power_supply_changed for usb
"); */
        power_supply_changed(&chip->usb);
    }
#endif
    if (old_online != chip->online) {//(5)、如果有DC插入,則更新充電狀態(tài);
        /* printk(KERN_DEBUG "power_supply_changed for AC
"); */
        power_supply_changed(&chip->ac);
    }
    schedule_delayed_work(&chip->work, MAX17040_DELAY);
}
(1)、保存老的電池信息,如電量、AC、USB是否插入
    old_online = chip->online;
    old_usb_online = chip->usb_online;
    old_vcell = chip->vcell;
    old_soc = chip->soc;
(2)、讀取電池新的狀態(tài)信息
    max17040_get_online(chip->client);//讀取是否有AC插入;
    max17040_get_vcell(chip->client);//讀取電池電壓;(這個(gè)地方原來(lái)寫錯(cuò),多謝細(xì)心網(wǎng)友,更正!!)
    max17040_get_soc(chip->client);//讀取電池電量;
    max17040_get_status(chip->client);//讀取狀態(tài);
(3)、如果電池信息有變化,就上報(bào)系統(tǒng)
    if ((old_vcell != chip->vcell) || (old_soc != chip->soc)) {
        /* printk(KERN_DEBUG "power_supply_changed for battery
"); */
        power_supply_changed(&chip->battery);
    }
power_supply_changed這個(gè)函數(shù)比較重要,我們后面分析;
(4)、如果用PM2301充電IC,USB充電功能不用
這個(gè)是由于我們的系統(tǒng)耗電比較大,用USB充電時(shí),電流過(guò)小,所以出現(xiàn)越充越少的現(xiàn)象,所以這個(gè)功能給去掉了。
(5)、如果有DC插入,則跟新充電狀態(tài)
power_supply_changed(&chip->ac);
6、AC、USB充電狀態(tài)怎么更新到應(yīng)用
如上面所說(shuō),通過(guò)power_supply_changed上報(bào);
7、電池曲線的測(cè)量與加入
電池曲線,就是電池的沖放電信息,就是用專業(yè)的設(shè)備,對(duì)電池連續(xù)充放電幾天,測(cè)出一個(gè)比較平均的值。然后轉(zhuǎn)換成針對(duì)電量IC(如我們用的max17040)的數(shù)字量,填入一個(gè)數(shù)組中,如下圖所示:
下面數(shù)據(jù)時(shí)針對(duì)電池曲線的數(shù)字量,和相關(guān)參數(shù)。如上圖所示,為160小時(shí)的電池信息,包括:不同顏色分別代表不同的曲線:如temperature,referenceSOC,fuelgaugeSOC,Vcell,EmptyVoltage
數(shù)據(jù)表格如下:
Device=MAX17040 Title = 1055_2_113012 EmptyAdjustment = 0 FullAdjustment= 100 RCOMP0=161 TempCoUp =0 TempCoDown = -2 OCVTest = 56224 SOCCheckA = 113 SOCCheckB = 115 bits= 18 0xC2 0xE8 0x0D 0x37 0x51 0x5B 0x5E 0x62 0x6A 0x88 0xA6 0xCB 0xF1 0x3C 0x99 0x1A 0x60 0x0D 0x80 0x0D 0xA0 0x01 0xC0 0x0C 0xF0 0x0F 0x30 0x0F 0x90 0x06 0x10 0x06 0xAC 0x20 0xAE 0x80 0xB0 0xD0 0xB3 0x70 0xB5 0x10 0xB5 0xB0 0xB5 0xE0 0xB6 0x20 0xB6 0xA0 0xB8 0x80 0xBA 0x60 0xBC 0xB0 0xBF 0x10 0xC3 0xC0 0xC9 0x90 0xD1 0xA0 0x02 0x90 0x0E 0x00 0x0C 0x10 0x0E 0x20 0x2C 0x60 0x4C 0xB0 0x39 0x80 0x39 0x80 0x0C 0xD0 0x0C 0xD0 0x0A 0x10 0x09 0xC0 0x08 0xF0 0x07 0xF0 0x05 0x60 0x05 0x60 0xC0 0x09 0xE0 0x00 0x00 0x01 0x30 0x02 0x52 0x06 0x54 0x0B 0x53 0x080x63 0x08 0x29 0xE0 0xC1 0xE2 0xC6 0xCB 0x98 0x98 0xCD 0xCD 0xA1 0x9C 0x8F 0x7F 0x56 0x56
加入驅(qū)動(dòng)中的值:
/driver/power/max17040_common.c中
unsigned char model_data[65] = {
    0x40,    /* 1st field is start reg address, others are model parameters */
    0xAC, 0x20,0xAE, 0x80, 0xB0, 0xD0, 0xB3, 0x70,
    0xB5, 0x10, 0xB5, 0xB0,    0xB5, 0xE0,0xB6, 0x20,
    0xB6, 0xA0, 0xB8, 0x80, 0xBA, 0x60, 0xBC, 0xB0,
    0xBF, 0x10, 0xC3, 0xC0, 0xC9, 0x90, 0xD1, 0xA0,
    0x02, 0x90, 0x0E, 0x00, 0x0C, 0x10,0x0E, 0x20,
    0x2C, 0x60,0x4C, 0xB0, 0x39, 0x80, 0x39, 0x80,
    0x0C, 0xD0,0x0C, 0xD0,    0x0A, 0x10,0x09, 0xC0,
    0x08, 0xF0, 0x07, 0xF0, 0x05, 0x60, 0x05, 0x60,
};
unsigned char INI_OCVTest_High_Byte = 0xDB; //56224
unsigned char INI_OCVTest_Low_Byte = 0xA0;
unsigned char INI_SOCCheckA = 0x71;// 113
unsigned char INI_SOCCheckB = 0x73;//115
unsigned char INI_RCOMP = 0xa1;//161
unsigned char INI_bits = 18;
unsigned char original_OCV_1;
unsigned char original_OCV_2;
#elseunsigned char INI_RCOMP = 0x64;
unsigned char INI_bits = 19;
unsigned char original_OCV_1;
<STRONG>unsigned char original_OCV_2;</STRONG>
四、驅(qū)動(dòng)分析
1、Probe函數(shù)分析
上面我們簡(jiǎn)單了解驅(qū)動(dòng)中用到的主要知識(shí)點(diǎn),后面我們把這些點(diǎn)串起來(lái),驅(qū)動(dòng)還是從probe說(shuō)起;
static int __devinit max17040_probe(struct i2c_client *client,
                                    const struct i2c_device_id *id)
{
    struct i2c_adapter *adapter = to_i2c_adapter(client->dev.parent);
    struct max17040_chip *chip;
    int ret;
    printk("MAX17040 probe !!
");
    if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE))
        return -EIO;
    chip = kzalloc(sizeof(*chip), GFP_KERNEL);
    if (!chip)
        return -ENOMEM;
    g_chip = chip;
    g_i2c_client = client;//(1)、IIC 驅(qū)動(dòng)部分client 申請(qǐng);
    chip->client = client;
    chip->pdata = client->dev.platform_data;
    i2c_set_clientdata(client, chip);
    chip->battery.name        = "battery";//(2)、chip name;
    chip->battery.type        = POWER_SUPPLY_TYPE_BATTERY;
    chip->battery.get_property    = max17040_get_property;//(3)、獲取電池信息;
    chip->battery.properties    = max17040_battery_props;//(4)、電池各種信息;
    chip->battery.num_properties    = ARRAY_SIZE(max17040_battery_props);
    chip->battery.external_power_changed = NULL;
    ret = power_supply_register(&client->dev, &chip->battery);//(5)、battery加入power_supply
    if (ret)
        goto err_battery_failed;
    chip->ac.name        = "ac"
    chip->ac.type        = POWER_SUPPLY_TYPE_MAINS;
    chip->ac.get_property    = adapter_get_property;
    chip->ac.properties    = adapter_get_props;
    chip->ac.num_properties    = ARRAY_SIZE(adapter_get_props);
    chip->ac.external_power_changed = NULL;
    ret = power_supply_register(&client->dev, &chip->ac);//(6)、和battery相似,把a(bǔ)c加入power_supply
    if (ret)
        goto err_ac_failed;
#if !defined(CONFIG_CHARGER_PM2301)
    chip->usb.name        = "usb";
    chip->usb.type        = POWER_SUPPLY_TYPE_USB;
    chip->usb.get_property    = usb_get_property;
    chip->usb.properties    = usb_get_props;
    chip->usb.num_properties    = ARRAY_SIZE(usb_get_props);
    chip->usb.external_power_changed = NULL;
    ret = power_supply_register(&client->dev, &chip->usb);//(7)、和battery相似,把usb加入power_supply
    if (ret)
        goto err_usb_failed;
    if (chip->pdata->hw_init && !(chip->pdata->hw_init())) {
        dev_err(&client->dev, "hardware initial failed.
");
        goto err_hw_init_failed;
    }
#endif
#ifdef MAX17040_SUPPORT_CURVE
     g_TimeCount = 0;
     handle_model(0);
#endif
    max17040_get_version(client);
    battery_initial = 1;
    INIT_DELAYED_WORK_DEFERRABLE(&chip->work, max17040_work);//(8)、任務(wù)宏初始化,max17040加入chip->work隊(duì)列;
    schedule_delayed_work(&chip->work, MAX17040_DELAY);//(9)、通過(guò)定時(shí)器調(diào)度隊(duì)列;
    printk("MAX17040 probe success!!
");
    return 0;
err_hw_init_failed:
    power_supply_unregister(&chip->usb);
err_usb_failed:
    power_supply_unregister(&chip->ac);
err_ac_failed:
    power_supply_unregister(&chip->battery);
err_battery_failed:
    dev_err(&client->dev, "failed: power supply register
");
    i2c_set_clientdata(client, NULL);
    kfree(chip);
    return ret;
}
(1)、IIC驅(qū)動(dòng)部分client申請(qǐng);
(2)、chipname;
(3)、獲取電池信息;
通過(guò)傳遞下來(lái)的參數(shù),來(lái)讀取結(jié)構(gòu)體中相應(yīng)的狀態(tài),這個(gè)函數(shù)實(shí)現(xiàn)比較簡(jiǎn)單。
static int max17040_get_property(struct power_supply *psy,
                                 enum power_supply_property psp,
                                 union power_supply_propval *val)
{
    struct max17040_chip *chip = container_of(psy,
                                 struct max17040_chip, battery);
    switch (psp) {
    case POWER_SUPPLY_PROP_STATUS:
        val->intval = chip->status;
        break;
    case POWER_SUPPLY_PROP_ONLINE:
        val->intval = chip->online;
        break;
    case POWER_SUPPLY_PROP_VOLTAGE_NOW:
    case POWER_SUPPLY_PROP_PRESENT:
        val->intval = chip->vcell;
        if (psp  == POWER_SUPPLY_PROP_PRESENT)
            val->intval = 1; /* You must never run Odrioid1 without Battery. */
        break;
    case POWER_SUPPLY_PROP_CAPACITY:
        val->intval = chip->soc;
        break;
    case POWER_SUPPLY_PROP_TECHNOLOGY:
        val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
        break;
    case POWER_SUPPLY_PROP_HEALTH:
        if (chip->vcell  < 2850)
            val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
        else
            val->intval = POWER_SUPPLY_HEALTH_GOOD;
        break;
    case POWER_SUPPLY_PROP_TEMP:
        val->intval = 365;
        break;
    default:
        return -EINVAL;
    }
    return 0;
}
(4)電池各種信息
static enum power_supply_property max17040_battery_props[] = {
            POWER_SUPPLY_PROP_PRESENT,
            POWER_SUPPLY_PROP_STATUS,
            /*POWER_SUPPLY_PROP_ONLINE,*/
            POWER_SUPPLY_PROP_VOLTAGE_NOW,
            POWER_SUPPLY_PROP_CAPACITY,
            POWER_SUPPLY_PROP_TECHNOLOGY,
            POWER_SUPPLY_PROP_HEALTH,
            POWER_SUPPLY_PROP_TEMP,
        };
(5)、battery加入power_supply;
(6)、和battery相似,把a(bǔ)c加入power_supply;
(7)、和battery相似,把usb加入power_supply;
(8)、max17040加入chip->work隊(duì)列;
前面已經(jīng)分析;
(9)、通過(guò)定時(shí)器調(diào)度隊(duì)列;
前面已經(jīng)分析;
2、power_supply_changed簡(jiǎn)要分析
如:把電池電量信息上報(bào):我們?cè)趍ax17040_work隊(duì)列調(diào)度函數(shù)中,如果有電池信息、狀態(tài)變化,則上用power_supply_changed上報(bào)。
power_supply_changed(&chip->battery);
Kernel/drivers/power/power_supply_core.c中:
void power_supply_changed(struct power_supply *psy)
{
    unsigned long flags;
    dev_dbg(psy->dev, "%s
", __func__);
    spin_lock_irqsave(&psy->changed_lock, flags);
    psy->changed = true;
    wake_lock(&psy->work_wake_lock);
    spin_unlock_irqrestore(&psy->changed_lock, flags);
    schedule_work(&psy->changed_work);//調(diào)度psy->changed_work
}
Psy->changed_work的執(zhí)行函數(shù):
static void power_supply_changed_work(struct work_struct *work)
{
    unsigned long flags;
    struct power_supply *psy = container_of(work, struct power_supply,
                        changed_work);
    dev_dbg(psy->dev, "%s
", __func__);
    spin_lock_irqsave(&psy->changed_lock, flags);
    if (psy->changed) {
        psy->changed = false;
        spin_unlock_irqrestore(&psy->changed_lock, flags);
        class_for_each_device(power_supply_class, NULL, psy,
                      __power_supply_changed_work);
        power_supply_update_leds(psy);
        kobject_uevent(&psy->dev->kobj, KOBJ_CHANGE);//uevent狀態(tài)
        spin_lock_irqsave(&psy->changed_lock, flags);
    }
    if (!psy->changed)
        wake_unlock(&psy->work_wake_lock);
    spin_unlock_irqrestore(&psy->changed_lock, flags);
}
                            總結(jié)
以上是生活随笔為你收集整理的【转】android电池(四):电池 电量计(MAX17040)驱动分析篇的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
                            
                        - 上一篇: GPU Gems1 - 22 颜色控制(
 - 下一篇: GPU Gems1 - 25 用纹理贴图