common-lisp
電子工作
RaspberryPi

CommonLispでRaspberryPi電子工作 ~SPI 3軸加速度センサー~

はじめに

今回はSPIで3軸加速度センサモジュール LIS3DHを制御してみたいと思います。

回路図

まず最初にRaspberryPiとLIS3DHを以下のように繋ぎます。

パッケージ作成

cffiをQuicklispでロードし、パッケージ定義。
いつものです。

packages.lisp
;; cffiをQuicklispでロード
(ql:quickload "cffi")

;; cl-cffiパッケージを定義
(defpackage :cl-cffi
    (:use :common-lisp :cffi))

APIラッパー作成

今回追加する関数は以下の2つです。

・wiringPiSPISetup
チャンネルを初期化する関数。(RaspberryPiには2つのチャンネル、0と1があります。)
速度パラメータは500,000〜32,000,000の範囲の整数で、SPIクロック速度をHzで表します。

・wiringPiSPIDataRW
選択されたSPIバス上で、同時に書込み/読出しトランザクションが実行されます。
バッファ内のデータは、SPIバスから返されたデータによって上書きされます。

libwiringPi.lisp
(define-foreign-library libwiringPi
  (:unix "libwiringPi.so"))

(use-foreign-library libwiringPi)

;;;; Core function

;; Initialization of the wiringPi
(defcfun "wiringPiSetupGpio" :int)

;; Set the mode of the GPIO pin
(defcfun "pinMode" :void (pin :int) (mode :int))

;; GPIO pin output control
(defcfun "digitalWrite" :void (pin :int) (value :int))

;; Waiting process
(defcfun "delay" :void (howlong :uint))

;; Set the state when nothing is connected to the terminal
(defcfun "pullUpDnControl" :void (pin :int) (pud :int))

;; Read the status of the GPIO pin
(defcfun "digitalRead" :int (pin :int))

;;;; I2C Library

;; Initialization of the I2C systems.
(defcfun "wiringPiI2CSetup" :int (fd :int))

;; Writes 8-bit data to the instructed device register.
(defcfun "wiringPiI2CWriteReg8" :int (fd :int) (reg :int) (data :int))

;; It reads the 16-bit value from the indicated device register.
(defcfun "wiringPiI2CReadReg16" :int (fd :int) (reg :int))

;;;; SPI library

;; SPI初期化
(defcfun "wiringPiSPISetup" :int (channel :int) (speed :int))

;; 選択されたSPIバス上での同時書込み/読出しトランザクションを実行
(defcfun "wiringPiSPIDataRW" :int (channel :int) (data :pointer) (len :int))

SPI LIS3DHプログラム本体

lis3dh.lisp
;; Load packages
(load "packages.lisp" :external-format :utf-8)

(in-package :cl-cffi)

;; Load wrapper API
(load "libwiringPi.lisp" :external-format :utf-8)

(defconstant +spi-cs+ 0)                ; 対象のSPIデバイスを選択
(defconstant +spi-speed+ 100000)        ; SPIの通信速度

(defconstant +out-x-l+ #X28)            ; OUT_X Low
(defconstant +out-x-h+ #X29)            ; OUT_X High
(defconstant +out-y-l+ #X2A)            ; OUT_Y Low
(defconstant +out-y-h+ #X2B)            ; OUT_Y High
(defconstant +out-z-l+ #X2C)            ; OUT_Z Low
(defconstant +out-z-h+ #X2D)            ; OUT_Z High

(defconstant +who-am-i+  #X0F)          ; WHO_AM_I
(defconstant +ctrl-reg1+ #X20)          ; CTRL_REG1
(defconstant +ctrl-reg4+ #X23)          ; CTRL_REG4

(defconstant +read+  #X80)              ; Read
(defconstant +write+ #X3F)              ; Write

(defconstant +pin+    8)                ; CS
(defconstant +output+ 1)
(defconstant +high+   1)
(defconstant +low+    0)

;; SPIデータの読み書き
(defun spi-data-rw (channel data &optional (len (length data)))
  (let ((mp (foreign-alloc :unsigned-char :count len :initial-contents data))) ; 要素数lenのunsigned char型配列mpを確保して、dataを代入
    (digitalWrite +pin+ +low+)                                                 ; CSをLowにセット
    (wiringPiSPIDataRW channel mp len)                                         ; SPI Read/Write 実行
    (digitalWrite +pin+ +high+)                                                ; CSをHighにセット
    (let ((rval (loop for i from 0 below len                                   ; 0~(len-1)までiをインクリメントしながらループ
                   collect (mem-aref mp :unsigned-char i))))                   ; mpから値を取得し、rvalへ格納
      (foreign-free mp)                                                        ; mpを開放
      rval)))

;; 加速度センサーで計測したデータを取得する処理
(defun spi-read (read-addr)
  (let (outdat out)
    (setq outdat (list (logior read-addr +read+) #X00)) ; 書込みデータ作成
    (setq out (spi-data-rw +spi-cs+ outdat))            ; SPIデータの読書き処理実行
    (nth 1 out)))                                       ; リストの2つ目の値がデバイスからのデータなのでそれを返す

;; CTRL_REGに値を設定する処理
(defun spi-write (write-addr data)
  (spi-data-rw +spi-cs+ (list (logand write-addr +write+) data)))

;; 取得したデータから加速度を算出する処理
(defun conv-two-byte (high low)
  (let (dat)
    (setq dat (logior (ash high 8) low))
    (if (>= high #X80)
        (setq dat (- dat 65536)))
    (setq dat (ash dat -4))
    dat))

;; メイン関数
(defun main ()
  (let (lb hb x y z)
    ;; wiringPi SPI 初期化
    (wiringPiSPISetup +spi-cs+ +spi-speed+)

    ;; GPIOを初期化
    (wiringPiSetupGpio)

    ;; GPIO11を出力モード(1)に設定
    (pinMode +pin+ +output+)

    ;; 最初はCSをHighにしておく
    (digitalWrite +pin+ +high+)

    ;; LIS3DHのレジスタWHO_AM_Iを読み、0x33なら正しく通信できている
    (if (equal (spi-read +who-am-i+) #X33)
        (format t "I AM LIS3DH~%") ; 正しく通信できている場合は、"I AM LIS3DH"と表示
        (return-from main nil))    ; 正しく通信できていない場合は、処理終了

    ;; CTRL_REG1に0x77をを書込み、HR/Normal/Low-power mode (400 Hz)とします。
    (spi-write +ctrl-reg1+ #X77)

    ;; ここから実際にデータを取得する処理
    (loop
       ;; X軸のデータを取得
       (setq lb (spi-read +out-x-l+))
       (setq hb (spi-read +out-x-h+))
       (setq x  (conv-two-byte hb lb))

       ;; Y軸のデータを取得
       (setq lb (spi-read +out-y-l+))
       (setq hb (spi-read +out-y-h+))
       (setq y  (conv-two-byte hb lb))

       ;; Z軸のデータを取得
       (setq lb (spi-read +out-z-l+))
       (setq hb (spi-read +out-z-h+))
       (setq z  (conv-two-byte hb lb))

       (format t "x=~6d y=~6d z=~6d~%" x y z)

       (delay 500))))

;; 実行!
(main)

実行

以下のコマンドで実行します。

sbcl --load lis3dh.lisp

以下実行中の様子

XYZ軸のデータが取得できました!

最後に

SPIはI2Cに比べてずっと難しかったです・・・。
まさかここまで苦戦するとは・・・。

SPIデータの読書き処理がLispではリストで処理し、wiringPiSPIDataRW関数ではunsigned char型配列を使うため、その変換処理を作るのが一番大変でした。

何はともあれ、何とか動かせて良かったです。
これでI2C、SPIともに使えるようになりました!

追記

conv-two-byte関数について

2バイトデータを結合する以下の処理は、

lis3dh.lisp
(setq dat (logior (ash high 8) low))

dpb関数を使って以下のように書き換えることができそうです。

lis3dh.lisp
(setq dat (dpb high (byte 15 8) low))

また、上位12ビットの結果を取得している以下の処理は、

lis3dh.lisp
(setq dat (ash dat -4))

ldb関数を使って以下のように書き換えることができそうです。

lis3dh.lisp
(setq dat (ldb (byte 15 4) dat))

実際に試してみたところ、dpb関数とldb関数を使った方ではマイナスの結果が出てきませんでした。

正の数だけを扱う場合であれば、ldb関数で上位12ビットを取り出すだけでよいのですが、負の数を扱う場合はash関数で算術右シフトしているため、空いたビット位置に1が入るため、最終的な結果が異なってしまいます。

例)

(ash (- #X8010 65536) -4) ;=>-2047
(ldb (byte 15 4) (- #X8010 65536)) ;=>30721

なので、2バイトデータを結合するdpb関数は使えますが、ldb関数はここで使うのは難しそうです。