同事因為想要在 D1 mini(ESP8266) 上讀取多個通道的ADC 值, 但是 ESP8266 本身只有一個 ADC 通道, 所以接了一塊採用 ADS1115 晶片的擴充板, 讓 ESP8266 可以讀取 4 個通道的 ADC。這個板子因為是使用 I2C 通訊, 而且可以透過特定腳位電位變化變更 4 種 I2C 位址, 所以最多可以同時接上 4 片擴充板讀取 16 個通道的 ADC 值。
這個板子因為非常常見, 所以也可以找到 MicroPython 程式庫, 這一切看似美好, 可是同事實測的時候發現, 依序讀取 0、1、2、3 通道的 ADC 值時, 卻是讀到通道 3、0、1、2 的 ADC 值, 他的程式如下:
from machine import I2C, Pin, Timer
import time
import ads1x15 # 匯入程式庫
addr1 = 72 # 設定 I2C 位址
gain = 1 # 設定偵測範圍為 0~4.096V
rate=6 # 設定取樣率為 475Hz
i2c = I2C(scl=Pin(5), sda=Pin(4),freq=400000) # 建立 I2C 物件
ads1 = ads1x15.ADS1115(i2c, addr1, gain) # 建立擴充板物件
def get1vol(cha): # 讀取指定通道的 ADC 值
ads1.set_conv(rate=rate,channel1=cha) # 設定通道等參數
raw1 = ads1.read_rev() # 讀取 ADC 值
vol1 = ads1.raw_to_v(raw1) # 轉換為電壓
return vol1
while True:
time.sleep(0.1) # 每 01 秒讀取 1 次
vol1=get1vol(0) # 讀取通道 0 ADC 值
vol2=get1vol(1) # 讀取通道 1 ADC 值
vol3=get1vol(2) # 讀取通道 2 ADC 值
vol4=get1vol(3) # 讀取通道 3 ADC 值
print("%2.2f, %2.2f, %2.2f, %2.2f" % (vol1, vol2, vol3, vol4))
仔細看了一下文件, 發現他所使用的 set_conv() 和 read_rev() 這一對方法的用法是比較奇特的, set_conv() 會設定下一次要讀取的通道編號以及採樣率, read_rev() 則會先取出上一次叫用 read_rev() 時讀取的 ADC 值當作傳回值, 然後依據目前設定的通道編號以及採樣率啟動讀取 ADC 值的程序就返回。也就是說, read_rev() 並不是傳回這一次的 ADC 值。這樣的作法主要是為了節省時間, 在叫用 read_rev() 時可以立即返回, 不用等取樣以及轉換的時間。不過這樣做就會導致如果是要輪流讀取不同通道的 ADC 值時, 會有同事遇到的怪現象, 舉來說來, 同事使用上述程式依序讀取 0、1、2、3 通道, 就會變成這樣:
ads1.rset_conv(rate=rate,channel1=0)
raw1 = ads1.read_rev() # 傳回前一次讀 channel ? 的數值, 然後依據目前設定讀 channel 0 的值
ads1.rset_conv(rate=rate,channel1=1)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 0 的數值, 然後依據目前設定讀 channel 1 的值
ads1.rset_conv(rate=rate,channel1=2)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 1 的數值, 然後依據目前設定讀 channel 2 的值
ads1.rset_conv(rate=rate,channel1=3)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 2 的數值, 然後依據目前設定讀 channel 3 的值
ads1.rset_conv(rate=rate,channel1=0)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 3 的數值, 然後依據目前設定讀 channel 0 的值
ads1.rset_conv(rate=rate,channel1=1)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 0 的數值, 然後依據目前設定讀 channel 1 的值
ads1.rset_conv(rate=rate,channel1=2)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 1 的數值, 然後依據目前設定讀 channel 2 的值
ads1.rset_conv(rate=rate,channel1=3)
raw1 = ads1.read_rev() # 傳回前一次讀 channel 2 的數值, 然後依據目前設定讀 channel 3 的值
...
如果覺得這樣很怪, 可以改用同一程式庫的 read() 方法, 將程式中的 get1vol() 函式改寫成這樣:
def get1vol(cha):
raw1 = ads1.read(rate, cha) # 讀取指定通道的 ADC 值
vol1 = ads1.raw_to_v(raw1) # 轉換為電壓
return vol1
read() 方法會啟動讀取 ADC 值的程序, 等待取樣完成並且轉換成數位值後才會返回。至於要用哪一種方式, 就看自己的需求囉。
Top comments (4)
我觉得“文件”中的说明很清楚了,您反而要先量测在两个get1vol()的呼叫中,是否遵循rate=6, ie. 475Hz 的样本率 (最好用逻辑分析仪量一下),“文件”中也有采用Timer以及ADC中断方式来读取的例子。
你講的跟我講的不是同一件事, 主要是 read_rev() 本來就是把前一次 ADC 結果返回, 然後啟動下一輪的 ADC 程序, 所以他返回的不是目前的 ADC 值。
是的,在文件当中已经写的很清楚了(会这样实现也是这种Sigma-Delta ADC需要时间来量测),是您同事程式码逻辑不符合这个程式库的设计理念。另外,我主要指出在您同事的程式码中,没有配合量测的“延时”,一下子读了4次,ADC的速度一定跟不上,这跟您在REPL手敲的结果是不一样的。
他原本的想法就是以為 read_rev() 會測量完取值再返回, 所以才會連續讀四次, 如果是使用 read(), 就沒有等待量測時間這個問題了。