スペクトルセンサーAS7341で光のスペクトル(波長分布)を測る

AMS社のスペクトルセンサーAS7341を使ったスペクトル(波長分布)測定をやってみた。背景や測定結果については[こちらのブログ](https://tomoto335.hatenablog.com/entry/as7341-spectral-color-sensor)で紹介しているので、ここではソフトウェアに関わる部分に絞って、まず本センサーの使い方一般と、次に本測定用に作ったアプリケーションについて説明する。

# AS7341とは

"11-Channel Spectral Color Sensor" である。製品ページは下記参照。

https://ams.com/ja/as7341

光の強さを波長の範囲(バンド)ごとに分けて測定することができる。データシートの下図にあるように、可視光域をカバーする8チャネル(F1～F8)と、クリアフィルター・近赤外線フィルター・フリッカー検出機能のついた3チャネルの合計11チャネルのセンサーを備えている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/98a328d7-6b8d-e05e-f65a-01f2ca636f55.png)
筆者が入手したのはみんな大好きAdafruitのブレイクアウトボードである。約$16とお手頃価格で、本記事執筆時点でMouserやDigiKeyから簡単に入手できた。

https://www.adafruit.com/product/4698

# 基本的な使い方

## 接続と動作確認

筆者はみんな大好きM5StickC Plusをコントローラーとして、Arduinoを開発環境として用いている。この場合、下記のAdafruitのチュートリアルにあるようにコントローラーとAS7341をI2Cで接続し、[Adafruit製AS7341ライブラリ](https://github.com/adafruit/Adafruit_AS7341)をインストールしてサンプルを動かすのが第一歩である。筆者は特に問題なく動かすことができた。

https://learn.adafruit.com/adafruit-as7341-10-channel-light-color-sensor-breakout/arduino

ただ一つ問題があって、上で紹介したブレイクアウトボードはあろうことか光センサーの近くに緑のLEDがついており、電源を入れるとこれが煌々と点灯してしまうのである。これではまともな計測などできない、ふざけんなと思うところだが、実は基盤の裏にある「LED」と書かれた銅線を切ればこのLEDを消すことができる。一通り動作確認したら切っておくことをお勧めする。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/131eec06-682a-cfe0-4b1f-d8a844ab4f50.png" width=320>

## データ取得方法

最も基本的なサンプルはこの read_all_channels.ino である。とにかくセンサーの生の値(16ビット)を補正なしで読み出す。

https://github.com/adafruit/Adafruit_AS7341/blob/master/examples/read_all_channels/read_all_channels.ino

このサンプルで使われている `readAllChannels` は同期呼び出し、つまり定められた時間の露出が完了するまで帰ってこない。これではボタン操作等を組み合わせると反応が悪く使いにくくなってしまうので、その場合は reading_while_looping.ino で紹介されている非同期呼び出しを用いる。

https://github.com/adafruit/Adafruit_AS7341/blob/master/examples/reading_while_looping/reading_while_looping.ino

余計なコードが入っておりあまりきれいなサンプルではないが、非同期呼び出しのポイントは下記のサイクルを繰り返すこと。
* `startReading()` で非同期露出開始
* `checkReadingProgress()` でデータが取れたかチェック
* データが取れたら`getAllChannels()` で取得

ちなみに、サンプルの中に [basic_counts.ino](https://github.com/adafruit/Adafruit_AS7341/blob/master/examples/basic_counts/basic_counts.ino) という何やらセンサーの値を補正する計算が入っているものがあるが、これはゲインや露出時間に関わらず値が一定になるように補正するもの。筆者の今回の目的は波長分布(バンド間の相対強度)を知ることなので、特に役立つものではない。

## ゲインと露出時間の設定

サンプルのsetup処理内では `setATIME`, `setASTEP`, `setGain` といった関数が呼ばれているが、これらはゲインと露出時間の設定である。

`setGain` はその名の通りゲイン(感度)設定である。0.5x から 512x まで11段階で設定できる。筆者の実験では、室内の光を測定する場合は最大の512xでちょうどよく、直射日光のような強い光は0.5xで飽和せず扱うことができた。

`setATIME` と `setASTEP` は露出時間設定で、次の式から実際の露出時間が決まる。
```math
(atime + 1) \times (astep + 1) \times 2.78μs
```
atime (8ビット)と astep (16ビット)の使い分けは、astep で基準単位を決め(例えば50msにするには17999を設定)、atime で量を決める、というのがデータシートから読み取れる意図である。まぁ数学的には可換なので必ずしもそうする必要もない。

また、$ (atime + 1) \times (astep + 1) $ はセンサー値のフルスケールにもなる。つまり、センサーからこの値または65535(uint16_tの最大値)が読みだされたら、おそらく光が強すぎてセンサーが飽和しているということだ。光を弱めるか、ゲインを下げる必要があるだろう。

この仕様から内部の仕組みが推測できる。おそらく、内部的に測定は2.78μsサイクルで行われ、その間に光子が検出されたら(この感度にゲイン設定が効く)カウンタを1増やし、それを$ (atime + 1) \times (astep + 1) $回繰り返して終了、となっているのではないだろうか。

# スペクトル(波長分布)測定アプリケーション

ここまでの基本がわかれば、こんなふうに↓可視光域のスペクトル測定アプリケーションを書くことができる。

https://github.com/tomoto/AS7341SpectrumMeter

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/76b0ca30-e350-c7fc-731a-73c81c37c96d.png)

何をしているかはREADMEやコード本体を読んで調べてみていただきたいが、ここでは調査や理解の助けになるよう、どんな考え方でどんなものを実装したのか概要を説明する。

## 波長分布の計算

可視光域の8個のセンサーの値を読み出し、キャリブレーション(後述)の係数を掛け、そのうち最大のもの(上の写真だと左から2番目)をフルスケールとして正規化する。絶対光度を求める必要などもなく、非常に簡単である。

## キャリブレーション

製品サイトにある[このドキュメント](https://ams.com/documents/20143/36005/AS7341_AN000633_1-00.pdf/fc552673-9800-8d60-372d-fc67cf075740)(その名も "Spectral Sensor Calibration Methods")によれば、センサーごとに感度が異なるので、波長分布として比較するためにはその違いを補正するキャリブレーションが必要と説明されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/94c1bacf-b450-ccca-ed6d-b0c7df579e07.png)
本来は信頼できる機器を参照しながら調整するのだが、そんな手間は掛けられないので、同ドキュメントに載っている補正係数の参考値を使わせてもらうことにする。そのまま使うと短波長側が過補正に見えたので、F1～F3は係数を参考値から80%に減らすことにした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/53a2daa4-04e2-4ae9-78fb-cec209681489.png)
これで太陽光を測定してみると、ドキュメント上のリファレンスの(≒正しい)スペクトルと概ね同様の傾向になった。雑ではあるが、これでキャリブレーションできたことにする。

## ゲインと露出時間の自動調整

ゲインと露出時間は下記の7段階で自動調整を行う。このパラメタは、フルスケールが50000になる露出時間(約140ms)を基本とし、ゲインを調整して1段階ごとに4倍の感度差を生むよう設計されている。ゲインが最大(512x)になったら、それ以上は露出時間で調整を行う。

|段階|ゲイン|露出時間(x2.78μs)|
|---|---|---|
|1|0.5x|50000|
|2|2x|50000|
|3|8x|50000|
|4|32x|50000|
|5|128x|50000|
|6|512x|50000|
|7|512x|50000 x 4|

段階の自動調整はセンサーの最大値と閾値の比較により行う。センサーの最大値が40000を越えたら(フルスケールに近づいたら)1段階下げ、10000を下回ったら1段階上げる。(室内で使う分には段階7に張り付いていることがほどんどである。明るい光源に近づけたり間接的な太陽光を扱うと4～6に下がり、直射日光を当てると段階1まで下がる。)

段階7の上には、露出時間がさらに10倍の段階が設けてある。これはBボタンで発動することができる。(暗い光源対策の実験的な機能であり、一応動けばOKくらいの適当な実装である。)

## 画面表示

上の写真に示した通り、波長分布を8本の棒グラフで表示する。バーの色は https://www.johndcook.com/wavelength_to_RGB.html で波長から求めた。

少々わかりにくいが、画面上段にセンサーから得られた(キャリブレーション前の)値の最大値を表示している。これには(1)センサーが飽和していない(50000や65535に張り付いていない)ことを確認する、(2)暗い光源の測定時に値に十分な解像度があることを確認する、の2つの目的のためである。

## リセット機能

電源ボタンクリックでリセットできる。(何かあってもリセットすれば大抵なんとかなる。)

電源ボタン長押しで電源オフできる。(M5StickCのハード電源OFFは6秒もホールドする必要があり実用的ではなく、プログラム的に処理している。)

## データ保存機能

測定データは後で分析やレポートで使いたいことが多いので、一時的にデバイス内にデータを保存し、後でUSBシリアルでPCに送信する機能を持たせてある。複雑な管理機能は付けたくないので、次のような操作系としている。

* Aボタンクリックで、デバイス内ファイルシステム(SPIFFS)にデータを保存する。
* Aボタン長押しで、デバイス内に保存されたデータをUSBシリアルに出力する。
  * PC側で保存に成功した場合: Aボタンを長押しし、デバイス内部のデータをクリアして計測に戻る。
  * 失敗した場合: Aボタンをクリックし、再試行する。
  * データを消さずに計測に戻りたければ電源ボタンでリセットすればよい。

出力データには以下が含まれる。フォーマットは実物を見てもらうかコード [Serializer.h](https://github.com/tomoto/AS7341SpectrumMeter/blob/feat/initial-commit/Meter/Serializer.h) を見てもらいたい。

* `raw`: キャリブレーション前のF1～F8の値
* `balanced`: キャリブレーション後のF1～F8の値
* `gain`: ゲイン設定を表す文字列

## データ参照機能

上記のデータ保存機能で出力されたデータを可視化するための、PC上で動くProcessingアプリケーションである。最低限の手抜き実装だが以下のことができる。

* USBシリアルからデータを受け取ってファイルに保存
* ファイルに保存されたデータの可視化

見た目こんな感じ:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/465152/706432ae-9bef-efd1-05ed-e1ba81380824.png)
詳しい使い方は[READMEのViewerの節](https://github.com/tomoto/AS7341SpectrumMeter#viewer)を参照。