AdafruitBlinkaを使ってみる

周辺機器の制御


Blinkaでは、複数のテクニックを組み合わせて、周辺機器の制御を行います。、
GPIOの制御では、RPi.GPIOかlibgpiodが使用され、I2Cの制御では、PureIO Python ライブラリが使用され、SPIの制御では、spidev Python ライブラリが使用されます。

まずはLチカを試してみます。
board.D14はGPIO14でピン番号が8です。
ピン番号8に繋いだLEDが3回点滅します。
#!/usr/bin/python
#-*- encoding: utf-8 -*-
import board
import digitalio
import time
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-g', '--gpio', type=int, help='GPIO to blink', default=14)
args = parser.parse_args()
gpio = "board.D{}".format(args.gpio)
print("gpio={}".format(gpio))

#pin = digitalio.DigitalInOut(eval("board.D14"))
pin = digitalio.DigitalInOut(eval(gpio))
pin.direction = digitalio.Direction.OUTPUT

for _ in range(3):
  pin.value = True
  time.sleep(1)
  pin.value = False
  time.sleep(1)

こ ちらにdigitalioクラスのドキュメントが公開されています。
ポートの状態としてdirection、drive_mode、value、pullの状態を設定、取得することができます。
GPIOポートは設定によりSPI、i2c、UARTなどに機能を変更することができます。
これをGPIOのALT機能(Alternative Function)と呼びます。
各ポートのALT状態はWiringPiのgpio readallコマンドで確認することができますが、
digitalioクラスを使ってALTの状態は取得することができません。

以下のコードでdigitalioクラスが提供するポート状態を確認することができます。
#!/usr/bin/python
#-*- encoding: utf-8 -*-
import board
import digitalio
import time

io14 = digitalio.DigitalInOut(board.D14)
io14.direction = digitalio.Direction.OUTPUT
print(vars(io14))

io15 = digitalio.DigitalInOut(board.D15)
io15.direction = digitalio.Direction.INPUT
print(vars(io15))

上のコードを実行すると以下を表示します。
$ python3 ./mode.py
{'_pin': 14, '_DigitalInOut__direction': digitalio.Direction.OUTPUT, '_DigitalInOut__pull': None, '_DigitalInOut__drive_mode': digitalio.DriveMode.PUSH_PULL}
{'_pin': 15, '_DigitalInOut__direction': digitalio.Direction.INPUT, '_DigitalInOut__pull': None}



このライブラリはPWM出力をサポートしています。
Raspberry Pi2はPWM0とPWM1の2チャンネルのPWM出力を持っていて、PWM0がGPIO12/18、PWM1がGPIO13/19ですが、
Raspberry Pi2(bcm283x)では/sys/class/pwmをサポートしていません。
Raspberry Pi2ではSoftware PWMをサポートしていて、全てのGPIOポートで使うことができます。
以下のコードでGPIO14に繋いだLEDの輝度が変わります。
#!/usr/bin/python
#-*- encoding: utf-8 -*-
import pwmio
import board

pwm = pwmio.PWMOut(board.D14)  # output on LED pin with default of 500Hz

while True:
    for cycle in range(0, 65535):  # Cycles through the full PWM range from 0 to 65535
        pwm.duty_cycle = cycle  # Cycles the LED pin duty cycle through the range of values
    for cycle in range(65534, 0, -1):  # Cycles through the PWM range backwards from 65534 to 0
        pwm.duty_cycle = cycle  # Cycles the LED pin duty cycle through the range of values

Hardware PWM(/sys/class/pwm)をサポートしているボードの場合、boardのメンバー変数にPWMnが定義されていて、これを使ってPWM制御 を行います。
現在、以下のボードがHardware PWMをサポートしています。
$ grep -rn "pwmOuts =" *
allwinner/a20/pin.py:136:pwmOuts = (
am335x/pin.py:366:pwmOuts = (((0, 0), PWM1), ((0, 1), PWM2), ((2, 0), PWM3), ((4, 1), PWM4))
am65xx/pin.py:195:pwmOuts = (
amlogic/meson_g12_common/pin.py:118:pwmOuts = []
amlogic/meson_g12_common/pin.py:190:pwmOuts = tuple(pwmOuts)
mt8167/pin.py:54:pwmOuts = (
nova/pin.py:79:pwmOuts = (((1, 0), PWM0), ((1, 2), PWM2), ((1, 3), PWM3), ((1, 4), PWM4))
nxp_imx6ull/pin.py:58:pwmOuts = (
nxp_imx8m/pin.py:39:pwmOuts = (
nxp_lpc4330/pin.py:196:pwmOuts = (
rockchip/rk3308/pin.py:212:pwmOuts = (
rockchip/rk3568/pin.py:238:pwmOuts = (
rockchip/rk3588/pin.py:288:pwmOuts = (
rockchip/rk3568b2/pin.py:66:# pwmOuts = (
rockchip/rk3568b2/pin.py:75:pwmOuts = []
rockchip/rk3568b2/pin.py:117:pwmOuts = tuple(pwmOuts)
rockchip/rk3399/pin.py:225:pwmOuts = (
rockchip/rk3566/pin.py:185:pwmOuts = [
rockchip/rk3566/pin.py:236:pwmOuts = tuple(pwmOuts)
rockchip/rk3328/pin.py:166:pwmOuts = (((2, 0), PWM2),)
sama5/pin.py:52:pwmOuts = (
tegra/t210/pin.py:140:pwmOuts = (



こ ちらにsmbusライブラリを使って、BMP180のi2cセンサーから、温度と気圧を読み出すコードが公開されています。
そこで、これをAdafruit-Blinkaライブラリに移植してみます。
BMP180とは以下の様に接続します。
BMP180 Host Pin#
3V3 3.3V
ECC *NotUse*
CLR *NotUse*
SCL board.SCL #5
SDA board.SDA #3
5V *NotUse*
GND GND

このライブラリのために、変更した箇所は行頭が##のコードです。
このライブラリでは、センサーからの入力用のbytearray型の領域を予め確保しておく必要があります。
board.SCLとboard.SDAは、board.D2とboard.D3と同じです。
この定義が絶妙で、この定義があれば、SCLとSDAのピン位置が変わっても、そのままのコードで動きます。
#!/usr/bin/python
#-*- encoding: utf-8 -*-
##import smbus
import board
import busio
import time
from ctypes import c_short

DEVICE = 0x77 # Default device I2C address

##bus = smbus.SMBus(0) # 0 indicates /dev/i2c-0
##bus = smbus.SMBus(1) # 1 indicates /dev/i2c-1
#i2c = busio.I2C(board.SCL, board.SDA)
i2c = busio.I2C(scl=board.SCL, sda=board.SDA)

def convertToString(data):
  # Simple function to convert binary data into
  # a string
  return str((data[1] + (256 * data[0])) / 1.2)

def getShort(data, index):
  # return two bytes from data as a signed 16-bit value
  return c_short((data[index] << 8) + data[index + 1]).value

def getUshort(data, index):
  # return two bytes from data as an unsigned 16-bit value
  return (data[index] << 8) + data[index + 1]

def readBmp180Id(addr=DEVICE):
  # Chip ID Register Address
  REG_ID     = 0xD0
##  (chip_id, chip_version) = bus.read_i2c_block_data(addr, REG_ID, 2)
  result = bytearray(2)
  i2c.writeto_then_readfrom(addr, bytes([0xD0]), result)
##  return (chip_id, chip_version)
  return (result[0], result[1])

def readBmp180(addr=DEVICE):
  # Register Addresses
  REG_CALIB  = 0xAA
  REG_MEAS   = 0xF4
  REG_MSB    = 0xF6
  REG_LSB    = 0xF7
  # Control Register Address
  CRV_TEMP   = 0x2E
  CRV_PRES   = 0x34
  # Oversample setting
  OVERSAMPLE = 3    # 0 - 3

  # Read calibration data
  # Read calibration data from EEPROM
##  cal = bus.read_i2c_block_data(addr, REG_CALIB, 22)
  cal = bytearray(22)
  i2c.writeto_then_readfrom(addr, bytes([REG_CALIB]), cal)

  # Convert byte data to word values
  AC1 = getShort(cal, 0)
  AC2 = getShort(cal, 2)
  AC3 = getShort(cal, 4)
  AC4 = getUshort(cal, 6)
  AC5 = getUshort(cal, 8)
  AC6 = getUshort(cal, 10)
  B1  = getShort(cal, 12)
  B2  = getShort(cal, 14)
  MB  = getShort(cal, 16)
  MC  = getShort(cal, 18)
  MD  = getShort(cal, 20)

  # Read temperature
##  bus.write_byte_data(addr, REG_MEAS, CRV_TEMP)
  i2c.writeto(addr, bytes([REG_MEAS, CRV_TEMP]))
  time.sleep(0.005)
##  (msb, lsb) = bus.read_i2c_block_data(addr, REG_MSB, 2)
  temperature = bytearray(2)
  i2c.writeto_then_readfrom(addr, bytes([REG_MSB]), temperature)
  msb = temperature[0]
  lsb = temperature[1]
  UT = (msb << 8) + lsb

  # Read pressure
##  bus.write_byte_data(addr, REG_MEAS, CRV_PRES + (OVERSAMPLE << 6))
  i2c.writeto(addr, bytes([REG_MEAS, CRV_PRES + (OVERSAMPLE << 6)]))
  time.sleep(0.04)
##  (msb, lsb, xsb) = bus.read_i2c_block_data(addr, REG_MSB, 3)
  pressure = bytearray(3)
  i2c.writeto_then_readfrom(addr, bytes([REG_MSB]), pressure)
  msb = pressure[0]
  lsb = pressure[1]
  xsb = pressure[2]
  UP = ((msb << 16) + (lsb << 8) + xsb) >> (8 - OVERSAMPLE)

  # Refine temperature
  X1 = ((UT - AC6) * AC5) >> 15
  X2 = (MC << 11) / (X1 + MD)
  B5 = X1 + X2
  temperature = int(B5 + 8) >> 4

  # Refine pressure
  B6  = B5 - 4000
  B62 = int(B6 * B6) >> 12
  X1  = (B2 * B62) >> 11
  X2  = int(AC2 * B6) >> 11
  X3  = X1 + X2
  B3  = (((AC1 * 4 + X3) << OVERSAMPLE) + 2) >> 2

  X1 = int(AC3 * B6) >> 13
  X2 = (B1 * B62) >> 16
  X3 = ((X1 + X2) + 2) >> 2
  B4 = (AC4 * (X3 + 32768)) >> 15
  B7 = (UP - B3) * (50000 >> OVERSAMPLE)

  P = (B7 * 2) / B4

  X1 = (int(P) >> 8) * (int(P) >> 8)
  X1 = (X1 * 3038) >> 16
  X2 = int(-7357 * P) >> 16
  pressure = int(P + ((X1 + X2 + 3791) >> 4))

  return (temperature/10.0,pressure/100.0)

def main():

  (chip_id, chip_version) = readBmp180Id()
  print("Chip ID     : {0}".format(chip_id))
  print("Version     : {0}".format(chip_version))
  print

  try:
    print ("Press CTRL+C to exit")
    while True:
      (temperature,pressure)=readBmp180()
      print("Temperature : {0} C".format(temperature))
      print("Pressure    : {0} mbar".format(pressure))
      time.sleep(1)

  except KeyboardInterrupt:
    i2c.deinit()


if __name__=="__main__":
   main()

今回、BMP180ではなく、完全機能互換のBMP085を使いましたが、あっさりと動きました。
$ python3 bmp180-i2c.py
Chip ID     : 85
Version     : 2
Press CTRL+C to exit
Temperature : 19.9 C
Pressure    : 1013.43 mbar
Temperature : 19.9 C
Pressure    : 1013.4 mbar
Temperature : 19.9 C
Pressure    : 1013.44 mbar



このライブラリはSPI通信もサポートしています。
そこで、このライブラリを使ってSPI仕様のBMP280から温度を読み出してみました。

BMP280は4ピン仕様(i2c専用)と6ピン仕様(i2c/SPI兼用)のモジュールが有りますが、
SPIで使うときは6ピン仕様のモジュー ルを使う必要が有ります。

6ピン仕様のモジュールのピンマーキングは向って左から
SDO CSB SDA SCL Gnd Vcc
となっていますが、SPIで使う場合、以下の結線となります。
BMP280 Host Pin#
Vcc 3.3V
Gnd Gnd
SCL SPI SCLK 23
SDA SPI MOSI 19
CSB SPI CS 24
SDO SPI MISO 21

左からBMP085(i2c専用) BMP280(i2c専用) BMP280(i2c/SPI兼用)


こ ちらで公開されているC言語のコードを参考にさせていただきました。
BMP280のサンプルは圧倒的にi2cを使ったサンプルが多いのですが、
BitBang-SPI(ソフトウェアSPI)での使い方を紹介している貴重なページです。

C言語をPythonに移植する場合、Pythonには符号付整数の考え方が無いので、少し注意が必要です。
BMP280は0x88から0x9Fのレジスターにキャリブレーションデータを格納していますが、
一部のキャリブレーションデータは16ビット符号付整数値です。
以下のコードではint16()関数を使って符号なし整数値を16ビット符号付整数値に変換しています。

また、0xF4と0xF5のレジスターを設定しますが、SPIの通信仕様では、
書き込み時のレジスターアドレスは7ビット(0x00〜0x7F)に規定されているので、
0xF4と0xF5のレジスターを設定するときは、0x74と0x75のアドレスを指定します。
こ ちらにBMP280のデータシートが有ります。
BMP280のデータシートの6.3章にきちんと書かれていますが、気が付くまで苦労しました。

In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not
used and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read).
Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the
byte 0x77 is transferred, for read access, the byte 0xF7 is transferred.

BMP280のデータシートの4.2章にMemoryMapの初期値が公開されています。
0xF7から0xFCに測定データが格納されますが、測定が終わらないと初期値(0x80 0x00 0x00 0x80 0x00 0x00)のまま、
更新されないことが分かったので、初期値のままかどうかの判定処理を行っています。

board.SCLK、board.MOSI、board.MISOは、board.D21、board.D20、board.D19と同じで、
他のボードでも同じ定義があれば、そのままのコードで動きます。
このライブラリのSPIクラスにはCS制御の機能が有りません。
CS制御は自前で実装する必要が有ります。
#!/usr/bin/python
#-*- encoding: utf-8 -*-
##import spidev
import board
import busio
import sys
import time
import os.path

DEBUG = 0

def calibration_T(adc_T):
  global t_fine
  var1 = ((((adc_T >> 3) - (dig_T1<<1))) * (dig_T2)) >> 11
  var2 = (((((adc_T >> 4) - (dig_T1)) * ((adc_T>>4) - (dig_T1))) >> 12) * (dig_T3)) >> 14

  t_fine = var1 + var2
  t_fine = int32(t_fine)
  T = (t_fine * 5 + 128) >> 8
  return T

def calibration_P(adc_P):
  var1 = ((t_fine)>>1) - 64000
  if (DEBUG == 1):print("var1(1) = %d " % var1)
  var2 = (((var1>>2) * (var1>>2)) >> 11) * (dig_P6)
  if(DEBUG == 1):print("var2(2) = %d " % var2)
  var2 = var2 + ((var1*(dig_P5))<<1)
  if(DEBUG == 1):print("var2(3) = %d " % var2)
  var2 = (var2>>2)+((dig_P4)<<16)
  if(DEBUG == 1):print("var2(4) = %d " % var2)
  var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + (((dig_P2) * var1)>>1))>>18
  if(DEBUG == 1):print("var1(5) = %d " % var1)
  var1 = ((((32768+var1))*(dig_P1))>>15)
  if(DEBUG == 1):print("var1(6) = %d " % var1)
  if (var1 == 0):
    return 0
  P = ((((1048576)-adc_P)-(var2>>12)))*3125
  if(P<0x80000000):
    P = int((P << 1) / (var1))
  if(P>=0x80000000):
    P = (P / var1) * 2;
  var1 = ((dig_P9) * ((((P>>3) * (P>>3))>>13)))>>12
  var2 = (((P>>2)) * (dig_P8))>>13
  if(DEBUG == 1):
    print("var1 = %d" % var1),
    print("var2 = %d" % var2)
  P = (P + ((var1 + var2 + dig_P7) >> 4))
  return P

def int16(x):
  if x>0xFFFF:
    raise OverflowError
  if x>0x7FFF:
    x=int(0x10000-x)
    if x<2147483648:
      return -x
    else:
      return -2147483648
  return x

def int32(x):
  if x>0xFFFFFFFF:
    raise OverflowError
  if x>0x7FFFFFFF:
    x=int(0x100000000-x)
    if x<2147483648:
      return -x
    else:
      return -2147483648
  return x

def readData():
  while(1):
    a = []
    for x in range(0xF7, 0xFD):
      a.append(x)
    a.append(0)
##    resp = spi.xfer(a)
    resp = bytearray(len(a))
    spi.write_readinto(a, resp)
    if(DEBUG == 1):print(resp)
    # Check valid data
    if (resp[2] != 0 or resp[3] != 0):break
    time.sleep(1)

  pres_raw = (resp[1] << 12) | (resp[2] << 4) | (resp[3] >> 4)  #0xF7, msb+lsb+xlsb=19bit
  if(DEBUG == 1):print("pres_raw = %d " % pres_raw)
  temp_raw = (resp[4] << 12) | (resp[5] << 4) | (resp[6] >> 4)  #0xFA, msb+lsb+xlsb=19bit
  if(DEBUG == 1):print("temp_raw = %d " % temp_raw)

  temp_cal = calibration_T(temp_raw)
  if(DEBUG == 1):print("temp_cal = %d " % temp_cal)
  press_cal = calibration_P(pres_raw)
  if(DEBUG == 1):print("press_cal = %d " % press_cal)
  temp_act = temp_cal / 100.0
  press_act = press_cal / 100.0
  return temp_act, press_act


if __name__=="__main__":
  t_sb = 5    #stanby 1000ms
  filter = 0  #filter O = off
  spi3or4 = 0 #SPI 3wire or 4wire, 0=4wire, 1=3wire
  osrs_t = 4  #OverSampling Temperature x8
  osrs_p = 4  #OverSampling Pressure x8
  Mode = 3    #Normal mode

  temp_raw = 0
  pres_raw = 0
  t_fine = 0

  # Open spi device
  spi = busio.SPI(board.SCLK, board.MOSI, board.MISO)
  while not spi.try_lock():
    pass
  spi.configure(baudrate=1000000) # 1MHz
  spi.unlock()
  cs = digitalio.DigitalInOut(board.D24)
  cs.direction = digitalio.Direction.OUTPUT
  cs.value = False

  print('Read the contents of the ID register')
  out = [0xD0, 0x00]
##  resp = spi.xfer(out)
  resp = bytearray(len(out))
  spi.write_readinto(out, resp)
  if(DEBUG == 1):print(resp)
  ChipId = resp[1]
  print("ChipId = 0x%x " % ChipId, end="")
  if (ChipId == 0x58):
    print("BMP280")
  elif (ChipId == 0x60):
    print("BME280")
  else:
    print("Unknown")
    while True:
      pass

  # Send a command to the control register[0xF4]
  ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode
  if(DEBUG == 1):print("ctrl_meas_reg = %x" % ctrl_meas_reg)
##  spi.xfer([0x74,ctrl_meas_reg])
  spi.write([0x74,ctrl_meas_reg])

  # Send a command to the config register[0xF5]
  config_reg = (t_sb << 5) | (filter << 2) | spi3or4
  if(DEBUG == 1):print("config_reg = %x " % config_reg)
##  spi.xfer([0x75,config_reg])
  spi.write([0x75,config_reg])

  # Check control[0xF4] & config register[0xF5]
  print('Check Register')
  out = [0xF4, 0xF5, 0x00]
##  resp = spi.xfer(out)
  resp = bytearray(len(out))
  spi.write_readinto(out, resp)
  if(DEBUG == 1):
    print(resp)
    print("ctrl_meas_reg = %x" % resp[1])
    print("config_reg    = %x" % resp[2])
  if(resp[1] != ctrl_meas_reg):
    print("INVALID control register %x" % resp[1])
  if(resp[2] != config_reg):
    print("INVALID config  register %x" % resp[2])

  print('Read calibration data')
  a = []
  for x in range(0x88, 0xA0):
    a.append(x)
  a.append(0)
##  resp = spi.xfer(a)
  resp = bytearray(len(a))
  spi.write_readinto(a, resp)
  if(DEBUG == 1):print(resp)

  dig_T1 = resp[2] * 256 + resp[1]
  dig_T2 = int16(resp[4] * 256 + resp[3])
  dig_T3 = int16(resp[6] * 256 + resp[5])
  if(DEBUG == 1):
    print("dig_T1 = %d" % dig_T1),
    print("dig_T2 = %d" % dig_T2),
    print("dig_T3 = %d" % dig_T3)

  dig_P1 = resp[8] * 256 + resp[7]
  dig_P2 = int16(resp[10] * 256 + resp[9])
  dig_P3 = int16(resp[12] * 256 + resp[11])
  dig_P4 = int16(resp[14] * 256 + resp[13])
  dig_P5 = int16(resp[16] * 256 + resp[15])
  dig_P6 = int16(resp[18] * 256 + resp[17])
  dig_P7 = int16(resp[20] * 256 + resp[19])
  dig_P8 = int16(resp[22] * 256 + resp[21])
  dig_P9 = int16(resp[24] * 256 + resp[23])
  if(DEBUG == 1):
    print("dig_P1 = %d" % dig_P1),
    print("dig_P2 = %d" % dig_P2),
    print("dig_P3 = %d" % dig_P3)
    print("dig_P4 = %d" % dig_P4),
    print("dig_P5 = %d" % dig_P5),
    print("dig_P6 = %d" % dig_P6)
    print("dig_P7 = %d" % dig_P7),
    print("dig_P8 = %d" % dig_P8),
    print("dig_P9 = %d" % dig_P9)

  try:
    print ("Press CTRL+C to exit")
    while True:
      temp_act, press_act = readData()
      print("Temperature : {} C".format(temp_act))
      print("Pressure    : {} mbar".format(press_act))
      time.sleep(1)

  except KeyboardInterrupt:
    spi.deinit()

データの受信を行う場合、1バイト余計に書き込みを行う必要が有るのは、他のSPIライブラリと同じです。
例えば0xD0のレジスターアドレスからデータを読む時は、[0xD0 0x00]の2バイトをtransferで指定し、
transferからの戻り値の先頭バイトは読み捨てます。
$ python3 bmp280.py
Read the contents of the ID register
ChipId = 0x58 BMP280
Check Register
Read calibration data
Press CTRL+C to exit
Temperature : 20.08 C
Pressure    : 1016.65 mbar
Temperature : 20.1 C
Pressure    : 1016.66 mbar
Temperature : 20.11 C
Pressure    : 1016.63 mbar

続く
...