再論TI TMP102

 雖然TMP102只是一個温度感測器, 但因為它使用I2C溝通界面, 讓我們可以學習I2C的用法, 而且架構簡單, 用起來不難, 但又有許多功能可以設定, 玩起來很有意思, 最主要的是其温度感測算是很準, 所以我一直很喜歡這顆。加上網路上也有很多人在用, 討論的文章和驅動程式都很容易找到, 所以這裡我想再談一下這顆的一些用法。

1. 驅動程式的寫法: 

其實可以不用自己寫驅動程式, 如果你使用Arduino, 那Sparkfun已經幫大家寫好了: SparkFun_TMP102_Arduino_Library 請直接拿去用就好了! 如果你使用MicroPython, 那khoulihan也寫好一個: khoulihan/micropython-tmp102他其實已經寫得很好。

但是我還是自己用MicroPython再寫了一次, 原因是khoulihan的驅動太完整了, TMP102所有的功能都實作了! 可惜的是我想用的一個開發板的Flash空間太小, 放了這個驅動後, 剩下的空間就太小了, 要寫其他的程式就很受限! 另外, 在自己改寫的過程中, 我發現自己又學到一些東西。雖然多花了幾天時間在嘗試, 還是收穫不少!

其實2017年在玩PyMata的時候, 就已經寫過TMP102的驅動, 參見python code for pymata tmp102, 但這個是架在PyMata之上的, 基本上是在PC上面執行, 稱之為驅動, 似乎有些奇怪。但程式邏輯和想法基本上相同。

操作TMP102的方法, 很簡單, 就是設定其暫存器的值就可以了, 如下圖 (取自TMP102的datasheet), TMP102主要有4個暫存器, (還有一個指位暫存器Pointer Register), 要取得温度值, 就讀取Temperature Register的內容值, 再做一些格式轉換即可。要讓TMP102進入省電的Shutdown模式, 就只要將Configuration Register內的某一個位元設成1就可以了。問題是如何指定要存取那個暫存器呢? 那就要在指位暫存器先指定就可以了, 例如, 將指位暫存器設成0x01, 就可以存取Configuration Register。

不過, 在讀或寫暫存器值時, 指位暫存器值的設定方式有點不太相同! 以下分別說明:

讀取暫存器內容值時, 先下一個I2C寫入的指令, 寫入SLA及指位暫存器的值; 然後再下I2C讀取的的指令, 這樣就可以讀到指位暫存器所指之暫存器之內容值了。

上圖(取自TMP102 datasheet), 就是讀取暫存器內容值時使用的I2C指令, 這其實是下2個指令, 一寫一讀。以邏輯分析儀來看, 如下圖, 可能清楚一些。第一個I2C指令寫入0x01的值給指位暫存器, 告訴它接下來要讀取那個暫存器(0x01表示Configuration Register), 第二個I2C指令去讀取時, 就會讀到剛才指定的暫存器的值了。即W[0x48] 0x01, 表示告訢slave address為0x48的TMP102, 後面的讀取是要讀0x01暫存器的值。因此, 後面的R[0x48] 0xE1 0xA0, 讀到的0xE1 0xA0就是暫存器0x01的內容值。



寫入暫存器時, 如下圖(取自TMP102 datasheet), 則是一個I2C寫入指令而已, 但要先寫入的是要寫入那個暫存器(即要填入指位暫存器的數值), 接著是第一個位元組, 然後是第二個位元組。因為TMP102的4個主要暫存器都是二個位元組的大小, 所以寫入指令在指位暫存器後都是帶二個位元組。

同樣用邏輯分析儀來看一下, 可能清楚一點。如下圖, W[0x48] 0x01 0xE1 0xA0, 表示向slave address為0x48的TMP102下寫入指令, 將數值寫到0x01暫存器, 第一個位元組寫入0xE1, 第二個位元組寫入0xA0。


了解了如何用I2C和TMP102溝通以(讀取)寫入值到其暫存器之後, 要操作TMP102就很簡單了, 因為所有的功能都是操作其暫存器的值來進行的。例如, 可以讓TMP102進入Shutdown模式, 這個時候, 其耗電量就可以降到0.5uA左右, 非常省電。

The Shutdown-mode bit saves maximum power by shutting down all device circuitry other than the serial interface, reducing current consumption to typically less than 0.5 μA. Shutdown mode enables when the SD bit is 1; the device shuts down when current conversion is completed. When SD is equal to 0, the device maintains a continuous conversion state.



由上表(取自TMP102 datasheet)可以找到SD這個位元是在0x01 Configuration Register這個暫存器的第一個位元組(Byte 1)的D0這個位置。由Datasheet的文字說明, 我們知道將SD設為1, TMP102就會進入Shutdown模式, 較為省電許多。那這個功能的驅動要如何寫就很明顯了, 因為我們無法只去改變一個位元(bit), 我們最小的寫入單位是位元組(byte), 所以必須將原本暫存器的內容讀出, 然後用位元運算去改變一個位元, 再將整個位元組寫回去。

用MicroPython來寫, 如下程式, 第一行程式將Configuration Register這個暫存器的內容取出放到config位元組陣列, 第二行程式將config[0](即暫存器的第一個位元組)的D0位元(使用MASK SHUTDOWN_BIT=0x01)設成1(_set_bit), 第三行程式再將改過之後的內容寫回去原來的暫存器。這樣就達到我們使TMP102進入省電模式的目的了。
  
    def sleep(self):
        config = bytearray(self._get_config())
        config[0] = _set_bit(config[0], SHUTDOWN_BIT)
        self._set_config(config)
  
當然, 由Shutdown模式回到正常的連續轉換模式, 也就是把SD改回0(_clear_bit), 如此而已。


    def wakeup(self):
        config = bytearray(self._get_config())
        config[0] = _clear_bit(config[0], SHUTDOWN_BIT)
        self._set_config(config) 
  
這裡讀取暫存器值和寫入暫存器值的函式, 見下列程式碼, 基本上是由khoulihan的程式修改一點點來的。其程式碼做的事情, 就是前面說明的用I2C寫入和讀取指令來做TMP102暫存器讀寫的動作而已。這裡可以學到一些小技巧, 即使MicroPython似乎是一個語言, 但其I2C部份的指令, 看來因不同開發板硬體不同, 而有2種不同語法習慣! (readfrom vs. recv; writeto vs. send) khoulihan使用了try except 語法來解決這個困擾, 這樣就可以跨不同硬體開發板。而讀取暫存器的動作, I2C送回來的2個bytes, 其資料型態為Bytes, khoulihan使用bytearray()這用法將其由Bytes轉成Byte Array型態, 以方便直接存取其中一個位元組。
 
    def _read_register(self, register):
        self._write_register(register)
        try:
            val = self.bus.readfrom(self.address, 2)
        except AttributeError:
            val = self.bus.recv(2, addr=self.address)
        return val
    
    def _write_register(self, register, value=None):
        bvals = bytearray()
        bvals.append(register)
        if value is not None:
            for val in value:
                bvals.append(val)
        try:
            self.bus.writeto(self.address, bvals)
        except AttributeError:
            self.bus.send(bvals, addr=self.address)
            
    def _get_config(self):
        return self._read_register(REGISTER_CONFIG)
    
    def _set_config(self, config):
        self._write_register(REGISTER_CONFIG, config)
我使用三用電表來量一下耗電量, 下圖是TMP102進入Shutdown模式時的耗電流, 真的如Datasheet所說的, 不到0.5uA。這時我使用的是Nordic nRF52840DK這個開發板。
當我把TMP102回復成一般模式(即連續轉換模式)時, 耗電量開始跳動, 因為預設是每秒進行4次轉換(即將取得類比温度轉換成數位值並存入0x00温度暫存器), 每次轉換時都會比較耗電。三用電表是顯示平均值, 所以只能看到在跳動的數字, 耗電量可能在60-12uA之間, 如下圖。

所以確實, 這樣可以用來控制TMP102的行為。有用的是這個驅動是可以跨硬體的, 如下圖Thonny這個工具, (1)只要把我自己改的驅動tmp102.py複製到開發板上的flash, (2)寫一個簡單的MicroPython程式, 如此圖中的tmp102_temp.py, 修改一下因不同板子而不同的 I2C接腳編號, 就可以叫TMP102工作了。如上圖, 我還有一個Raspberry Pi Pico開發板, 我也把驅動copy過去, 然後改了一下tmp102_temp.py, 之後就可以在RP Pico上讀到TMP102測到的温度了。


因為我只需要one-shot並保留原本連續轉換模式的功能, 這樣才能省電且驅動程式較小, 因此沒有全部TMP102的功能, 如果有同樣需求的朋友, 我仍將程式碼放在這裡。程式碼最後面註解部份為此驅動的用法, 如程式註解對應二個用法, 一個為預設用法, 一個為省電用法。
REGISTER_TEMP = 0
REGISTER_CONFIG = 1

SHUTDOWN_BIT = 0x01
ONE_SHOT_BIT = 0x80

def _set_bit(b, mask):
    return b | mask

def _clear_bit(b, mask):
    return b & ~mask

class Tmp102(object):
    
    def __init__(self, bus, address):
        self.bus = bus
        self.address = address
        
    def get_TempC(self, data):
        lo = data[1]
        hi = data[0]
        temp = ((hi * 256) + lo) >> 4
        return temp * 0.0625
    
    def _read_register(self, register):
        self._write_register(register)
        try:
            val = self.bus.readfrom(self.address, 2)
        except AttributeError:
            val = self.bus.recv(2, addr=self.address)
        return val
    
    def _write_register(self, register, value=None):
        bvals = bytearray()
        bvals.append(register)
        if value is not None:
            for val in value:
                bvals.append(val)
        try:
            self.bus.writeto(self.address, bvals)
        except AttributeError:
            self.bus.send(bvals, addr=self.address)
            
    def _get_config(self):
        return self._read_register(REGISTER_CONFIG)
    
    def _set_config(self, config):
        self._write_register(REGISTER_CONFIG, config)
        
    def temperature(self):
        return bytearray(self._read_register(REGISTER_TEMP))
            
    def wakeup(self):
        config = bytearray(self._get_config())
        config[0] = _clear_bit(config[0], SHUTDOWN_BIT)
        self._set_config(config)    
    def sleep(self):
        config = bytearray(self._get_config())
        config[0] = _set_bit(config[0], SHUTDOWN_BIT)
        self._set_config(config)
    
    def set_one_shot(self):
        config = bytearray(self._get_config())
        config[0] = _set_bit(config[0], ONE_SHOT_BIT)
        self._set_config(config)
        
    def get_one_shot(self):
        while True:
            config = bytearray(self._get_config())
            data1 = (config[0] >> 7) & 0x01
            if data1 != 0:
                break
        return bytearray(self._read_register(REGISTER_TEMP))
    
'''
from xxx import Tmp102
from machine import I2C, Pin
i2c = I2C(0, scl=Pin(29), sda=Pin(31))
sensor = Tmp102(i2c, 0x48)

# periodic read
sensor.wakeup()
sensor.get_TempC(sensor.temperature())

# one-shot read
sensor.sleep()
sensor.set_one_shot()
sensor.get_TempC(sensor.get_one_shot())
'''

留言

這個網誌中的熱門文章

D-BUS學習筆記

關於藍牙裝置找尋(inquiry, scan)兩三事

Cisco Switch學習筆記: EtherChannel