KiCAD + ngspice

回路図を作成するために久しぶりにKiCADをインストールしたので、ついでにKiCADについてググってみたところ、KiCADでSPICEシミュレーションができるという記事を見かけました。
そこで、試しにやってみました。環境はLinux Mint 20です。

インストール

すでにKiCADはSynapticなどからインストールしてある状態からです。
まず、ngspice と gnucap をインストールします。(両方必要なのかはわかりません)

$ sudo apt install ngspice gnucap

次に、Linux Mint 20ではgrepとegrepがインストールされている箇所の違いから問題が出る、という記事を見かけましたので、そのワークアラウンドを探して実行しました。

$ sudo ln -s /bin/grep /usr/bin/grep
$ sudo ln -s /bin/egrep /usr/bin/egrep

回路図の作成

新規プロジェクトを作成し、回路図(.sch)をダブルクリックして開きます。

必要なシンボルを配置していきますが、シンボル選択で spice と入力するとspiceシミュレーションができるシンボルだけを絞り込み表示できます。

電圧源を選択したところ。

回路図を作成し、各部品にリファレンス(部品番号)と定数を設定します。
今回はCRローパスフィルタのシミュレーションをしてみます。

抵抗値は1kΩ、コンデンサは1uFとしました。カットオフ周波数は

となりますので、計算上は 1÷(2×3.14×1000×10^-6) = 159.24Hz となります。
電圧源は右クリックして、「プロパティを編集」を開いた後、「Spiceモデルを編集」をクリックしてパラメータを設定します。

ここでは、DC/AC解析のAC振幅に1V、過渡応答解析のパルスに以下の設定を行いました。

SPICEシミュレーションの実行

「ツール」⇒「アノテーション」、「ツール」⇒「ネットリストを生成」とした後、「ツール」⇒「シミュレータ」でシミュレータの画面を開きます。

「設定」で「AC」タブを開いて、以下の設定を行います。

「実行」ボタンをクリックした後、プローブをクリックして、回路図上のコンデンサの端子をクリックすると、周波数応答が表示されます。右上の信号欄で信号を選んで「カーソルを表示」とすると、グラフ中にカーソルを表示できます。

カーソルを移動することで、位相遅れが-45°になる周波数は159.115Hz、この周波数でのゲインは-3dBVとなり、机上計算の通りであることが確認できます。

設定で「過渡応答」タブを開いて、

として、同様に「実行」をクリックしてからプローブで抵抗の両端をクリックしすると、それぞれの位置での過渡応答波形が表示されます。

拡大したい部分を選択すると、

過渡応答波形が表示されます。

細かい使い方がまだわかりませんが、インストールで躓くことなく簡単に使えるのは嬉しいです。LTSpiceのようにWine上でエミュレーション動作させるのではなく、ネイティブ(?)に動作させられるのが良いです。

こちらの記事を参考にさせてもらいました(基本、なぞっただけです)。

CO2モニタ3号機にLCDを追加

先に作成したCO2モニタ3号機(MH-Z19C搭載)にキャラクタ液晶を追加しました。今回は凝った表示はせずに、秋月のI2C接続小型LCDモジュール(8×2行)ピッチ変換キットをつないだだけです。電源(3.3V)とGNDとSCL、SDAを配線するだけで動くのだから簡単でいいですね。電源はSeeeduino Xiaoからもらっています。

LCDの表示を行うソースコードはLCDモジュールのAQM0802Aのライブラリで良さげなのがなかったので、直接I2Cで制御しています。

/*
  CO2ガスモニタ
*/
#include <Wire.h>

const int ledPin    = LED_BUILTIN;  // ボード上のLED(PIN_LED_13)

bool errorstat = false;
void error(bool s){
  errorstat = s;
  if(s){
    digitalWrite(ledPin, LOW);  // エラー発生、LED点灯
  } else {
    digitalWrite(ledPin, HIGH); // エラーなし、LED消灯
  }
}

// LCD(AQM0802A)の制御
void LcdDat(byte d) {
 Wire.beginTransmission(0x3E);
 Wire.write(0x40);
 Wire.write(d);
 Wire.endTransmission();
 delayMicroseconds(30);
}

void LcdCmd(byte d) {
 Wire.beginTransmission(0x3E);
 Wire.write(0x00);
 Wire.write(d);
 Wire.endTransmission();
 delay(2);
}

void InitLCD(){
  // OSC,Contrast,Power/ICON/Contrast,Follower
  const byte seq1[6] = {0x38,0x39,0x14,0x70,0x56,0x6C};
  // ClearDisplay & Display ON
  const byte seq2[3] = {0x38,0x01,0x0C};
  delay(40);
  for(int i=0;i<sizeof seq1;i++) LcdCmd(seq1[i]);
  delay(200);
  for(int i=0;i<sizeof seq2;i++) LcdCmd(seq2[i]);
}

void PrintLCD(char *txt,uint8_t len){
  for(int i=0;i<len;i++){
    LcdDat(txt[i]);
  }
}

void PrintZNum(uint16_t num){
  bool s=true;      // ゼロサプレス中
  uint16_t d=1000;  // 4桁表示
  uint16_t n=num;
  uint8_t  c,p=0,b[8];
  if(n>9999) n=9999;    
  while(d>0){
    c=n/d; n=n%d;
    d=d/10;
    if(d==0) s=false;
    if(s==false || c!=0){
      b[p++]=c+0x30;
      s=false;
    } else {
      b[p++]=' ';
    }
  }
  PrintLCD((char *)b,p);
}

#define BAUDRATE 9600   // MH-Z19との通信速度(変更不可)

uint16_t CO2;
int8_t  DEG;

byte ReadCO2[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
byte SCalOn[9]  = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6};
byte SCalOff[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86};
byte RetVal[9];

void setup() {
  Wire.begin();
  InitLCD();
  LcdCmd(0x80 | 0x00);  // set address 0x00
  //PrintLCD("MH-Z19C ",8);
  PrintLCD("CO2 \xc9\xb3\xc4\xde",8);
  LcdCmd(0x80 | 0x45);  // set address 0x45
  PrintLCD("ppm",3);
  
  delay(1000);
  Serial.begin(115200);
  // 基板上のLED消灯(出力HIGH=消灯)
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);

  delay(2000);
  Serial1.begin(BAUDRATE, SERIAL_8N1);  // MH-Z19Cへのシリアルポート   
  Serial1.write(SCalOn,sizeof SCalOn);
  // Serial1.write(SCalOff,sizeof SCalOff);
}

void loop() {
  // CO2センサ処理
  Serial1.write(ReadCO2,sizeof ReadCO2);
  memset(RetVal,0x00,sizeof RetVal);
  Serial1.readBytes((char *)RetVal, sizeof RetVal);
  if(RetVal[0] == 0xff && RetVal[1] == 0x86){
    CO2 = RetVal[2]*256 + RetVal[3];
    DEG = RetVal[4]-40;
    error(false);
  } else {
    error(true);
  }

  LcdCmd(0x80 | 0x40);  // set address 0x40
  PrintZNum(CO2);
  // 通常表示
  if(Serial && !errorstat){
    //Serial.print(DEG);                               
    //Serial.print(",");
    Serial.println(CO2);                                
  }

  delay(3000);
}

後で3Dプリンタでケースを作るつもりです。

MH-Z19Bのキャリブレーションについて

自動校正(Auto Calibration)についてMH-Z19Cを動かしていて気づいたことが一つ。といっても、MH-Z19Cではなく、MH-Z19Bの方。(MH-Z19Cも同じ傾向なんじゃないかとは思いますが)

この3ヶ月くらい、MH-Z19Bを搭載したCO2モニタ1号機をほとんど窓を開けずにほぼ連続運用してきました(だって、寒いですもの・・・)。24時間換気のファンが動いている(といっても、そんなにガンガン換気するものではないです)ので、部屋が無人になると数時間で400ppmくらいまで表示が落ちていました。

今回、3号機を作成して初期の校正を行うために窓をずーっと開けていたところ、表示が400ppmを切り出して、380ppmを指すようになりました。大気中のCO2濃度が400ppmを少し超えるくらいなので、これは本来ありえない数字です。このまま放置しておけばこのときの値を基準に再度自動校正がかかると思いますが、この癖は頭に入れておいたほうが良さそうです。つまり、長時間の濃度が下がりきらない環境では値が低く出力されることです。それでも何もないよりもずっとましだと思いますが、値はあくまで目安で100ppm単位くらいでざっくり相対的に捉えるべきものなのだと思います。
(CCS811のeCO2のアルコールでも屁でも何でも値が跳ね上がってしまうようなものに比べればずーーーーーーっとましだと思います)

世の中でCO2濃度測定による環境の管理を売りにしていて、400ppmを下回るような数字が出ている場合には要注意かもしれません。本来はそれほど換気は良くないのに、撮影等をするために久しぶりに窓を開けて換気しているとか、そういう可能性があるかもしれません(笑)。

MH-Z19Cをとりあえず動かしてみた

以前、CO2モニタ2号機を作ったら、どうも動作が安定しないと思ったらMH-Z19Bがフェイク品だった際に、悲しみに打ちひしがれて(大げさな^^;)MH-Z19Cを購入してしまいました。今度は、製造元のWinsen社がAliExpressに直接出店していたので、そちらから購入しています。(2月19日から秋月電子でも取扱を始めています)

直接購入すると、20個は入りそうな専用の梱包箱に入って、検査成績書がついて届きました。もちろん購入したのは1個です。

おもわず感動しましたw。

3号機の作成

で、こんな感じで組んで、ようやく動かしてみました。

MH-Z19Cの端子バージョンのものは端子が2.54mmのグリッドにあっていないため、通常の2.54mmピッチのユニバーサル基板には載せられませんので、1.27mmピッチのユニバーサル基板に搭載しました。隙間が大きめですが、ここには後でI2Cのキャラクタ液晶でも搭載して(電源は外付けですが)スタンドアローンでも動作できるようにしようかと思っています。

制御用のマイコンは当初はESP32-DevKitっぽいボードで始めたのですが、途中で電源周りできちんと5Vを出すために四苦八苦しているうちにESP32-DevKitを壊してしまいました。ですので、Seeeduino Xiaoで作り直しています。

MH-Z19Cに供給する電源については、MH-Z19Cは電源が5V±0.1Vという厳しい条件を要求しますので、USBで直接給電するとこの範囲から容易に外れてしまいます。ですので、USBのBUS電圧を一旦DC-DCステップアップコンバータモジュールで8Vに昇圧してから三端子レギュレータTA48M05Fで5Vを生成しています。

MH-Z19Cからのデータの読出しはUARTで行っています。

ソフトウェアは以前作ったMH-Z19B用で動かしてみると、ライブラリでエラーが発生しました。ですので、今回は直接コマンドを送受信することにしました。

/*
  CO2ガスモニタ
*/
const int ledPin    = LED_BUILTIN;  // ボード上のLED(PIN_LED_13)

#define BAUDRATE 9600   // MH-Z19との通信速度(変更不可)

uint16_t CO2;
int8_t  DEG;

byte ReadCO2[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
byte SCalOn[9]  = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6};
byte SCalOff[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86};
byte RetVal[9];

bool errorstat = false;

void error(bool s){
  errorstat = s;
  if(s){
    digitalWrite(ledPin, LOW);  // エラー発生、LED点灯
  } else {
    digitalWrite(ledPin, HIGH); // エラーなし、LED消灯
  }
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  // 基板上のLED消灯(出力HIGH=消灯)
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);

  delay(2000);
  Serial1.begin(BAUDRATE, SERIAL_8N1);  // MH-Z19Cへのシリアルポート   
  Serial1.write(SCalOn,sizeof SCalOn);
  // Serial1.write(SCalOff,sizeof SCalOff);
}

void loop() {
  // CO2センサ処理
  Serial1.write(ReadCO2,sizeof ReadCO2);
  memset(RetVal,0x00,sizeof RetVal);
  Serial1.readBytes((char *)RetVal, sizeof RetVal);
  if(RetVal[0] == 0xff && RetVal[1] == 0x86){
    CO2 = RetVal[2]*256 + RetVal[3];
    DEG = RetVal[4]-40;
    error(false);
  } else {
    error(true);
  }

  if(Serial && !errorstat){
    //Serial.print(DEG);                               
    //Serial.print(",");
    Serial.println(CO2);                                
  }
  delay(3000);
}

これで、MH-Z19Cからデータを読み出してCO2濃度を出力します。Arduino IDEのシリアルプロッタ機能を使えば、時系列での変化もグラフで見ることができます。

動作させるとかなり大きめの値が出ます。MH-Z19Bのときも同じでしたが、キャリブレーションが必要なようです。風通しのいいところで24時間動かせば多分収束すると思うのですが、今回は1時間ほど窓を開けておいて(寒い!)、センサのHd端子をGNDに落として初期のキャリブレーションを行いました。たぶん、400ppmよりはかなり悪い条件だとは思いますが、オートキャリブレーションをONにしているので、そのうち落ち着くだろう、という発想です。

実際にシリアルプロッタ機能で表示させてみたものです。途中、大きく跳ね上がっているのは、MH-Z19Cに息を吹きかけた際のものです。呼気の中のCO2に反応して大きく跳ね上がっているのがわかります。

大きな地震が起きると電源周波数が大きく変動する?

1月に #新春商用電源監視月間 というTwitterのタグにそそのかされて作ってみた、電源周波数の監視ハードウェアですが、この後インターネットからブラウザで見えるようにしていました。(本当は、epoch.jsでリアルタイムデータをカッコよく見せたかったのですが、力不足で強引にリロードを繰り返すクソ実装ですw)

しかし、これだけではどんなときに大きく変動するのか見ているのは難しい。ということで、2号機では変動大きめの際にブザーが鳴るようにしていました。

それから1ヶ月ほど経った2月13日の夜のこと、突如高いブザー音が鳴り続けます。通常、±0.2Hzを超えるような変動が長時間続くことはありませんので、鳴っても数秒くらいです。もっと低い±0.1Hzくらいの振れ幅だと1分くらい続くことはあるのですが、この時は高い音がずーっと鳴り続けていました。
ブラウザで周波数変動を見てみると、49.8Hzどころか、もっと大きく周波数が落ちていきます。

何事かと考えているうちに、ゆらゆらと揺れ始めて遠方で規模の大きな地震が発生したことがわかりました。しかし、このタイミングで周波数が戻り始めます。

なにがなんだかわからない状態でしたが、とりあえずTweetしてみました(^^;

このツイートをみて、地震発生よりも先に電源周波数が変動した、地震直前の前兆現象ではないか、と考えた方がたくさんいらっしゃったようです。正確には、『地震の揺れが伝わってくるよりも先に電源周波数の変動を観測した』というのが正しいです。この時点ではなにがなんだかわかりませんでしたが。

揺れが収まったところで、何が起きたのか気になりました。そこで、強振モニタとの比較をしてみます。

すると、電源周波数が落ち始めるのは地震発生直後ということがわかりました。最初はこの周波数変動は地震によって需要家の電力消費が減ったからかと思ったのですが、その場合には発電機の負荷が減って回転数が上昇するので、周波数が上がる方向のはずです。しかし、周波数は下がる方向なので、負荷が増える方向です。そしてもう一つわからなかったのが、48.5Hzまで下がった後、周波数が急回復していることです。

その後、宮城県と福島県の火力発電所が停止していること、東日本の広い地域で停電していることがわかりました。

また、北海道胆振東部地震による北海道全域の大規模ブラックアウトを受けて、電源周波数が大きく変動した場合には負荷を切り離すUFR(Under Frequency Relay)というものがあることがわかりました。48.5HzはそのUFRが動作する周波数だったとのこと。

これらの情報を総合して、以下のような推測を立てました。

ということで、残念ながら(というか当然のことながら)地震発生前に起きた現象ではなく、地震による電力網の変動が地震波よりも先に伝わってくる、ということだったものと思われます。

それにしても、こんな現象が見えるとは・・・(^^;;;

Raspberry Pi PicoでHello World

Raspberry Pi PicoでUSBシリアルを使ったHello Worldプログラムを試してみます。

まずはLinux Mint 20にシリアルターミナルプログラムをインストールします。今回はgtktermを使います。

~$ sudo apt install gtkterm

シリアルターミナルを使えるようにユーザーをdialoutグループに追加します。

~$ sudo addgroup xxx dialout
ユーザー `xxx' をグループ `dialout' に追加しています...
ユーザ xxx をグループ dialout に追加
完了。
~$ 

有効にするには一旦ログアウトしてログインし直しますが、ここではそのまま進めています。
引き続き、helloworldプログラムをビルドします。

~/pico/pico-examples/build$ cd hello_world/
~/pico/pico-examples/build/hello_world$ make -j4
[  0%] Built target bs2_default
(中略)
[100%] Linking CXX executable hello_usb.elf
[100%] Built target hello_usb
~/pico/pico-examples/build/hello_world$ 

このディレクトリの下のusbディレクトリに生成された、hello_usb.uf2 を RPI-RP2ディレクトリにドラッグアンドドロップします。
その後、USBシリアルのデバイスファイルを確認します。

~$ dmesg | tail
[20457.063661] usb 3-4.1: new full-speed USB device number 8 using xhci_hcd
[20457.187659] usb 3-4.1: New USB device found, idVendor=2e8a, idProduct=000a, bcdDevice= 1.00
[20457.187661] usb 3-4.1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[20457.187663] usb 3-4.1: Product: Pico
[20457.187664] usb 3-4.1: Manufacturer: Raspberry Pi
[20457.187665] usb 3-4.1: SerialNumber: 000000000000
[20457.239178] FAT-fs (sdb1): unable to read boot sector to mark fs as dirty
[20457.293371] cdc_acm 3-4.1:1.0: ttyACM0: USB ACM device
[20457.295851] usbcore: registered new interface driver cdc_acm
[20457.295851] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
~$ 

ttyACM0としてデバイスファイルが生成されているようです。

gtktermを開いて、「Configration」⇒「Port」でttyACM0を設定します。(※スクリーンキャプチャではユーザーをdialoutグループに追加した後一旦ログアウトしていないので、gtktermをスーパーユーザーで起動しています)

OKをクリックすると、hello_worldのメッセージが1秒毎に表示されます。

Raspberry Pi PicoでLチカ

やっとRaspberry Pi Picoを手に入れました。

とりあえずC/C++でLチカしてみました。環境はLinux Mint 20 64bit、Ryzen3 3100 + 16GBです。
・・・といっても、瞬時に終わってしまいました。基本的には

クリックするとリンク先が開きます。その下の方にこの項目があります。

「getting started」に沿って行うだけです。

「2.1. Get the SDK and examples」に従い、SDKとexampleをダウンロードします。

~/pico$ git clone -b master https://github.com/raspberrypi/pico-sdk.git
Cloning into 'pico-sdk'...
remote: Enumerating objects: 248, done.
remote: Counting objects: 100% (248/248), done.
remote: Compressing objects: 100% (145/145), done.
remote: Total 1292 (delta 51), reused 160 (delta 36), pack-reused 1044
Receiving objects: 100% (1292/1292), 1.07 MiB | 1.23 MiB/s, done.
Resolving deltas: 100% (362/362), done.
~/pico$ cd pico-sdk
~/pico/pico-sdk$ git submodule update --init
Submodule 'tinyusb' (https://github.com/raspberrypi/tinyusb.git) registered for path 'lib/tinyusb'
Cloning into '/home/xxx/pico/pico-sdk/lib/tinyusb'...
Submodule path 'lib/tinyusb': checked out 'e0aa405d19e35dbf58cf502b8106455c1a3c2a5c'
~/pico/pico-sdk$ cd ..
~/pico$  git clone -b master https://github.com/raspberrypi/pico-examples.git
Cloning into 'pico-examples'...
remote: Enumerating objects: 450, done.
remote: Total 450 (delta 0), reused 0 (delta 0), pack-reused 450
Receiving objects: 100% (450/450), 2.20 MiB | 1.79 MiB/s, done.
Resolving deltas: 100% (108/108), done.
~/pico$ 

「2.2. Install the Toolchain」に従って、ツールチェーンをインストールします。これもダウンロードサイズが大きいもののすぐに終わります。

~/pico$ sudo apt update
~/pico$ sudo apt upgrade
~/pico$ sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential

以降はChapter.3に従ってLチカをビルドします。
まず、ビルドディレクトリを用意して環境変数を設定、ビルド用の環境を構築します。これもすぐに終わります。

~/pico$ cd pico-examples/
~/pico/pico-examples$ mkdir build
~/pico/pico-examples$ cd build
~/pico/pico-examples/build$ export PICO_SDK_PATH=../../pico-sdk
~/pico/pico-examples/build$ cmake ..
Using PICO_SDK_PATH from environment ('../../pico-sdk')
PICO_SDK_PATH is /home/xxx/pico/pico-sdk
Defaulting PICO_PLATFORM to rp2040 since not specified.
Defaulting PICO platform compiler to pico_arm_gcc since not specified.
-- Defaulting build type to 'Release' since not specified.
PICO compiler is pico_arm_gcc
PICO_GCC_TRIPLE defaulted to arm-none-eabi
-- The C compiler identification is GNU 9.2.1
-- The CXX compiler identification is GNU 9.2.1
-- The ASM compiler identification is GNU
-- Found assembler: /usr/bin/arm-none-eabi-gcc
Defaulting PICO target board to pico since not specified.
Using board configuration from /home/xxx/pico/pico-sdk/src/boards/include/boards/pico.h
-- Found Python3: /usr/bin/python3.8 (found version "3.8.5") found components: Interpreter 
TinyUSB available at /home/xxx/pico/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040; adding USB support.
-- Could NOT find Doxygen (missing: DOXYGEN_EXECUTABLE) 
ELF2UF2 will need to be built
PIOASM will need to be built
-- Configuring done
-- Generating done
-- Build files have been written to: /home/xxx/pico/pico-examples/build
~/pico/pico-examples/build$ 

Lチカをコンパイルします。これもすぐに終わります。

~/pico/pico-examples/build$ cd blink
~/pico/pico-examples/build/blink$ make -j4
Scanning dependencies of target ELF2UF2Build
Scanning dependencies of target bs2_default
[  0%] Creating directories for 'ELF2UF2Build'
(中略)
[100%] Building C object blink/CMakeFiles/blink.dir/home/xxx/pico/pico-sdk/src/rp2_common/pico_stdio_uart/stdio_uart.c.obj
[100%] Linking CXX executable blink.elf
[100%] Built target blink
~/pico/pico-examples/build/blink$ 

このディレクトリに生成された blink.uf2 がRaspberry Pi Picoに書き込むファイルです。

これをRaspberry Pi Picoを接続すると開くRPI-RP2ディレクトリにドラッグアンドドロップをすると、Lチカが始まります。2回目以降は、基板上のBOOTSELスイッチを押しながら接続すると、RPI-RP2ディレクトリが開きます。

わかってしまえば、10分もあれば終わってしまう作業です。環境がしっかりしているというのがRaspberry Pi系のありがたいところです。

ESP32でBluetoothキーボード

最近流行っている?ワンキーの特定目的キーボードとして、ZoomやTeamsのマイクON/マイクOFFができるものを作ろうとWebを彷徨っていたら、たまたまこちらのページを見つけました。

WordPressでこういう形で他のサイトを引用できることを初めて知りました・・・・

今回は実質的にこちらの追実験として、ESP32 BLE Keyboard libraryを使ったキーボードが作れるかのテストしてみます。

ハードウェアの用意

ハードウェアは(ちょっと怪しい)ESP32 DevKit互換っぽいESP32が載ったボードを使います。写真ではユニバーサル基板に載っていますが、意味はありません。

ちゃんと技適マークは入ってるモジュールです。CMIITは中国の電波法、ICはIndustry Canada、FCCは米国連邦通信委員会のそれぞれの認証IDですね。KCマーク(韓国)は一番面積食ってますね。CEマーキングは番号なし。ふと思ったんだけど、日本の電波法の技適マークって検索性悪そうです。こんなマークは知らないという海外の人はどうやって調べたらいいのだろう。

ソースコードの用意

ESP32 DevKitのビルドができるArduino環境で以下のソースコードを新規に作成します。

#include <BleKeyboard.h>

BleKeyboard bleKeyboard("ESP32 KEYBOARD"); //デバイスの名前

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleKeyboard.begin();

  pinMode(0,INPUT_PULLUP);    //GPIO0(BOOT)をプルアップ付き入力設定
}

void loop() {
  if(bleKeyboard.isConnected()) {    //接続されているとき
    if(digitalRead(0) == LOW){       //BOOTスイッチが押されている時
      bleKeyboard.print("keyboard test!");    //文字を送信
      bleKeyboard.write(KEY_RETURN); //Enterを送信
      delay(200);
    }
  }
}

参考にさせていただいた記事ではGPIO32とGPIO33にタクトスイッチとプルダウン抵抗を付けていますが、今回は動作テストなので入力にはDevKit上のBOOTのボタンを使用するよう改めています。

ライブラリはGitHubのESP32 BLE Keyboard libraryのページで、右上の「Code」のところから「Download ZIP」でライブラリをZIP形式でダウンロードしてきます。ダウンロードしたら「スケッチ」⇒「ライブラリのインクルード」でダウンロードしたライブラリを取り込みます。

動作テスト

これでビルドして書き込んでみると、Bluetooth内蔵のPCからキーボードとして見えるようになりますので、ペアリングしてから適当にテキストエディタを開いてDevKit上のBOOTのボタンを押すと、文字列が入力されて改行されます。

特殊キーの入力テスト

ソースコードを以下のように修正します。

#include <BleKeyboard.h>

BleKeyboard bleKeyboard("ESP32 KEYBOARD"); //デバイスの名前

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");
  bleKeyboard.begin();

  pinMode(0,INPUT_PULLUP);    //GPIO0(BOOT)をプルアップ付き入力設定
}

void loop() {
  static int sn,sp=HIGH;
  if(bleKeyboard.isConnected()) {    //接続されているとき
    sn = digitalRead(0);
    if(sn == LOW && sp==HIGH){       //BOOTスイッチが押されている時
      bleKeyboard.write(KEY_MEDIA_MUTE); //メディアキーを送信
    }
    sp=sn;
    delay(20);
  }
}

このコードでは、スイッチ押下時のチャタリング防止処理を追加しています。一旦スイッチを離さないと次のキーが入力できないようにしています。
これで実行すると、キーを押すたびにスピーカーがミュート/ミュート解除されます。

マイクのミュート/ミュート解除は一筋縄ではいきそうにない

この特殊キーのシンボルはライブラリのBleKeyboard.h 内で、

typedef uint8_t MediaKeyReport[2];

const MediaKeyReport KEY_MEDIA_NEXT_TRACK = {1, 0};
const MediaKeyReport KEY_MEDIA_PREVIOUS_TRACK = {2, 0};
const MediaKeyReport KEY_MEDIA_STOP = {4, 0};
const MediaKeyReport KEY_MEDIA_PLAY_PAUSE = {8, 0};
const MediaKeyReport KEY_MEDIA_MUTE = {16, 0};
const MediaKeyReport KEY_MEDIA_VOLUME_UP = {32, 0};
const MediaKeyReport KEY_MEDIA_VOLUME_DOWN = {64, 0};
const MediaKeyReport KEY_MEDIA_WWW_HOME = {128, 0};
const MediaKeyReport KEY_MEDIA_LOCAL_MACHINE_BROWSER = {0, 1}; // Opens "My Computer" on Windows
const MediaKeyReport KEY_MEDIA_CALCULATOR = {0, 2};
const MediaKeyReport KEY_MEDIA_WWW_BOOKMARKS = {0, 4};
const MediaKeyReport KEY_MEDIA_WWW_SEARCH = {0, 8};
const MediaKeyReport KEY_MEDIA_WWW_STOP = {0, 16};
const MediaKeyReport KEY_MEDIA_WWW_BACK = {0, 32};
const MediaKeyReport KEY_MEDIA_CONSUMER_CONTROL_CONFIGURATION = {0, 64}; // Media Selection
const MediaKeyReport KEY_MEDIA_EMAIL_READER = {0, 128};

として定義されています。要は16個の特殊キーの同時押しまで対応するようにビット割付されているようです。

そして、これらのキーは、BleKeyboard.c内で、

static const uint8_t _hidReportDescriptor[] = {
  USAGE_PAGE(1),      0x01,          // USAGE_PAGE (Generic Desktop Ctrls)
  USAGE(1),           0x06,          // USAGE (Keyboard)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  // ------------------------------------------------- Keyboard
  REPORT_ID(1),       KEYBOARD_ID,   //   REPORT_ID (1)
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  USAGE_MINIMUM(1),   0xE0,          //   USAGE_MINIMUM (0xE0)
  USAGE_MAXIMUM(1),   0xE7,          //   USAGE_MAXIMUM (0xE7)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   Logical Maximum (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  REPORT_COUNT(1),    0x08,          //   REPORT_COUNT (8)
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 1 byte (Reserved)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE (8)
  HIDINPUT(1),        0x01,          //   INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  REPORT_COUNT(1),    0x05,          //   REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  USAGE_PAGE(1),      0x08,          //   USAGE_PAGE (LEDs)
  USAGE_MINIMUM(1),   0x01,          //   USAGE_MINIMUM (0x01) ; Num Lock
  USAGE_MAXIMUM(1),   0x05,          //   USAGE_MAXIMUM (0x05) ; Kana
  HIDOUTPUT(1),       0x02,          //   OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 3 bits (Padding)
  REPORT_SIZE(1),     0x03,          //   REPORT_SIZE (3)
  HIDOUTPUT(1),       0x01,          //   OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  REPORT_COUNT(1),    0x06,          //   REPORT_COUNT (6) ; 6 bytes (Keys)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE(8)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM(0)
  LOGICAL_MAXIMUM(1), 0x65,          //   LOGICAL_MAXIMUM(0x65) ; 101 keys
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  USAGE_MINIMUM(1),   0x00,          //   USAGE_MINIMUM (0)
  USAGE_MAXIMUM(1),   0x65,          //   USAGE_MAXIMUM (0x65)
  HIDINPUT(1),        0x00,          //   INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  END_COLLECTION(0),                 // END_COLLECTION
  // ------------------------------------------------- Media Keys
  USAGE_PAGE(1),      0x0C,          // USAGE_PAGE (Consumer)
  USAGE(1),           0x01,          // USAGE (Consumer Control)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  REPORT_ID(1),       MEDIA_KEYS_ID, //   REPORT_ID (3)
  USAGE_PAGE(1),      0x0C,          //   USAGE_PAGE (Consumer)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  REPORT_COUNT(1),    0x10,          //   REPORT_COUNT (16)
  USAGE(1),           0xB5,          //   USAGE (Scan Next Track)     ; bit 0: 1
  USAGE(1),           0xB6,          //   USAGE (Scan Previous Track) ; bit 1: 2
  USAGE(1),           0xB7,          //   USAGE (Stop)                ; bit 2: 4
  USAGE(1),           0xCD,          //   USAGE (Play/Pause)          ; bit 3: 8
  USAGE(1),           0xE2,          //   USAGE (Mute)                ; bit 4: 16
  USAGE(1),           0xE9,          //   USAGE (Volume Increment)    ; bit 5: 32
  USAGE(1),           0xEA,          //   USAGE (Volume Decrement)    ; bit 6: 64
  USAGE(2),           0x23, 0x02,    //   Usage (WWW Home)            ; bit 7: 128
  USAGE(2),           0x94, 0x01,    //   Usage (My Computer) ; bit 0: 1
  USAGE(2),           0x92, 0x01,    //   Usage (Calculator)  ; bit 1: 2
  USAGE(2),           0x2A, 0x02,    //   Usage (WWW fav)     ; bit 2: 4
  USAGE(2),           0x21, 0x02,    //   Usage (WWW search)  ; bit 3: 8
  USAGE(2),           0x26, 0x02,    //   Usage (WWW stop)    ; bit 4: 16
  USAGE(2),           0x24, 0x02,    //   Usage (WWW back)    ; bit 5: 32
  USAGE(2),           0x83, 0x01,    //   Usage (Media sel)   ; bit 6: 64
  USAGE(2),           0x8A, 0x01,    //   Usage (Mail)        ; bit 7: 128
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  END_COLLECTION(0)                  // END_COLLECTION
};

でHIDのレポートディスクリプタとして定義されている中のハイライト部分に対応しているようで、この部分で16個のビットに対してキーコードを割り当てています。キーコードはUSBのHIDコードのようで、USBのHIDコードは

https://www.usb.org/document-library/hid-usage-tables-121

にあるPDFファイル(この1.21は2020/11/17のタイムスタンプがついている)に記載されているもののようです。試しに、このBleKeyboard.cppに書いてあるキーコードを書き換えると、動作が変わります。ただし、(Linuxの場合)一旦ペアリングを解除しないと、新しいディスクリプタを読み込んでくれませんでした。

そして、ZoomやTeamsといったオンラインミーティングの際にマイクのミュート/ミュート解除をしたくて、P.120にあるConsumer Pageの

を試してみるためにD5を設定してみたが、これは効きませんでした。(P.157にゲームの録画と配信の際に切り替えるようなことを書いてあります)

ところで、LenovoのPCではFn+F4でマイクをミュートすることができます。つまり、なんらかのキーコードがありそうなので探しまくりました。そして、Linuxのソースコードの中の /usr/include/linux/input-event-codes.h の中にも、

#define KEY_MICMUTE 248 /* Mute / unmute the microphone */

といういかにもマイクをミュート/ミュート解除するキーイベントを定義する行があり、いかにもマイクをミュート/ミュート解除するキーコードがありそうにも見えます。しかし、これに読み替えている箇所が見つかりません。さらには、キーの組み合わせでこのイベントを発生されているレノボ用のドライバっぽいソースコードも(インターネット上では)見つかるので、このキーコードは(少なくとも標準には)ないのかもしれないと思っています。

となると、スケッチ例にあるようにホットキーの組み合わせ(Zoomの場合は Alt + A、Teamsの場合は Ctrl + Shift + M)を送信するしかなさそうです。ただ、両方使っているので、どうしたもんでしょうかね。

CO2モニタ2号機の作成(4)

正月休みに作ったCO2モニタですが、早くも壊れてしまいました。

電源を入れっぱなし(というか切れないのですが)で放置していたところ、Seeeduino Xiao内部のXC9306が壊れてしまったようです。5Vを供給しても3.3V側に電圧が出てこなくなりました。
ですので、電源を供給してもLEDが点灯しません。

残念ですが、この構成は無理があるようなので、解体することにしました。
(Nokia液晶も実はバックライトが4個中2個点灯しない・・・)

他にSensirionのSCD30とかもあるのですが、どうしましょうかね。SCD30も同じ構成でSeeeduino Xiaoで動かそうかと思って違う液晶モジュール用にコードを書いている途中だったのですが、バッテリ切れで壊れてしまうのではこの構成は取れません。

できれば自分でチップ集めてボード作りたいところで、いまならRaspberry Pi Picoに搭載されるRP2040でボードを作りたいところですが、このチップは単体で売られるかなぁ。売られることを期待したいのですが・・・。

ATTINY2313で商用電源周波数監視

ATmega328で商用電源周波数の監視ができるようになりましたが、グラフを見てないと変動に気づきません。そこで、スタンドアローンで動くものを余っているATTINY2313で作ってみました。

基本的な構成はATmega328の時と同じです。100均のニッケル水素充電器のマイナス端子から内部のトランスの2次側電圧を引き出して、半波整流してLP2950L-3.3でATTINY2313の3.3V電源を生成します。一方でフォトカプラで商用電源の周期に同期した信号を生成して、ATTINY2313のTIMER1のICP信号に入力します。ATTINY2313の動作クロックは8MHzのクリスタルとして、適当に作っても最悪100ppm程度の精度は得られるようにします。スタンドアローンで動作するように、OC0B信号を圧電ブザーに接続して、TIMER0でブザーを鳴らせるようにします。

完成したものはこんな感じになります。

基板上のLEDはUARTのTXに接続してあるので、そこにトランジスタをつけたフォトダイオードをかざしてやると、非接触でシリアル通信がができます。

これをArduinoのシリアルプロッタに食わせると以下のような感じです。周波数は実際の周波数を1000倍したものです。

シリアルライブラリなどのArduinoのライブラリは使えないのでその部分は簡単なものを作っていますが、ソフトウェアはATmega328の時と大きくは変わりません。(下記の中で&が化けています・・・)

#include <avr/io.h>   // /usr/lib/avr/include/
#include <util/delay.h>
#include <util/atomic.h>
#include <string.h>

// FUSEは外部クリスタル8MHz、システムクロックを分周なしに設定(E:FF, H:DF, L:EF)

/////////////////////////////////////////////////
// 端子定義
#define BUZ 5	 // PORT D5 に圧電ブザー接続
#define ICPPIN 6 // PORT D6 = ICP
#define UARTTX 1 // PORT D1 = TX

/////////////////////////////////////////////////
// タイマ0関連
void Buzzer(uint8_t period){
    if(period>0){
	    // 高速PWM設定によるブザー駆動
	    TCCR0A = 0x23;     // COM0B[1-0]=10,WGM0[1-0]=11  高速PWM動作
	    TCCR0B = 0x0C;     // WGM02=1,CS[2-0]=4  分周=256分周=8MHz/256=31.25kHz
	    OCR0A = period;    // FREQ 31.25kHz / 31 ≒ 1008Hz / 63 ≒ 496Hz
	    OCR0B = period>>1; // DUTYを設定
	} else {
	    TCCR0A = 0x00;  // 出力停止
	}
}

/////////////////////////////////////////////////
// タイマ1関連
// 周期測定ののための変数
volatile uint16_t NumV;
volatile uint32_t SumV;
volatile uint16_t CapV;
volatile uint16_t PreCap,NowCap;

// 1秒間の最大最小平均を格納するための変数
uint16_t tNumV;
uint32_t tAveV;
uint32_t tSumV;

// 割り込みハンドラ
ISR(TIMER1_CAPT_vect){
  TIFR |= (1<<ICF1); 
  NowCap = ICR1;
  CapV = NowCap - PreCap;       // 差分を求めて周期を算出
  PreCap = NowCap;
  SumV += CapV;
  NumV++;
}

// タイマ1設定
void TIMER1_Init(void){
    // TCC1でICP信号(PD6端子)の周期を計測する
    TCCR1A = 0; //initialize Timer1
    TCCR1B = 0;
    TCNT1 = 0;

    TCCR1B =  0x02;   // 8MHzを8分周
    TIMSK  |= (1 << ICIE1); // キャプチャ割り込み許可
}

uint32_t Period(void){
    // 割り込みで更新している変数をコピー&初期化する
    cli(); //割り込み停止
    tSumV = SumV; tNumV = NumV;
    SumV = 0; NumV = 0;
    sei();   //割り込み再開
    tAveV = tSumV / tNumV;
    return(tAveV);
}

/////////////////////////////////////////////////
// UART関連
#define FOSC 8000000
#define BAUD 38400
#define MYUBRR FOSC/16/BAUD-1

uint8_t txbuf[16];
uint8_t txp=0,txn=0;

ISR(USART_UDRE_vect){
  if(txp<txn){
    while ( !(UCSRA & (1<<UDRE)) );
    UDR = txbuf[txp];
    txp++;
  } else {
    UCSRB &= ~_BV(UDRIE);
  }
}

void USART_Init(unsigned int baud){
    UBRRH = (unsigned char)(baud>>8);
    UBRRL = (unsigned char)baud;
    UCSRC = (1<<USBS)|(3<<UCSZ0);
    txp = 0;
    UCSRB = (1<<RXEN)|(1<<TXEN)|(1<<UDRIE);
}

void USART_txt(uint8_t *txt,uint8_t len){
    while(txp<txn);
    cli();
    memcpy(txbuf,txt,len);
    txp=0;
    txn=len;
    UCSRB |= _BV(UDRIE);
    sei();
}

void USART_num(uint16_t num){
    uint16_t d=10000;
    uint16_t n=num;
    uint8_t  c,p=0,b[8];
    while(d>0){
        c=n/d; n=n%d;
        d=d/10;
        b[p++]=c+0x30;
    }
    b[p++]='\r'; b[p++]='\n';
    USART_txt(b,p);
}

void USART_znum(uint16_t num){
    uint8_t s=1;    // ゼロサプレス中
    uint16_t d=10000;
    uint16_t n=num;
    uint8_t  c,p=0,b[8];
    while(d>0){
        c=n/d; n=n%d;
        d=d/10;
        if(d==0) s=0;
        if(s==0 || c!=0){
            b[p++]=c+0x30;
            s=0;
        }
    }
    b[p++]='\r'; b[p++]='\n';
    USART_txt(b,p);
}

/////////////////////////////////////////////////
// メイン
int main(void)
{
    uint32_t f,p,d;

    PORTB  = 0xFF; // 入力設定時のプルアップ有効
    PORTD  = 0xFF; // 入力設定時のプルアップ有効
    DDRD   = _BV(BUZ);     // PORT D5を出力に
    DDRD  &= ~_BV(ICPPIN); // ICPピンを入力に
    PORTD &= ~_BV(ICPPIN); // ICPピンのプルアップ無効化
    PORTD  = _BV(UARTTX);  // PORT D1を出力に

    TIMER1_Init();

    USART_Init(MYUBRR);

    for(;;){
        p = Period();
        f = 1000000000UL/p;  // 周波数の1000倍を求める
        if(f > 50000){
            d = f - 50000;
        } else {
            d = 50000 - f;
        }
        if(d > 200){      // 0.20Hz以上ずれた場合
            Buzzer(31);  // 高速PWM設定によるブザー駆動 31=1kHz
        }else if(d > 150){ // 0.15Hz以上ずれた場合
            Buzzer( 78);  // 高速PWM設定によるブザー駆動 78=400Hz
        }else if(d > 100){ // 0.10Hz以上ずれた場合
            Buzzer(250);  // 高速PWM設定によるブザー駆動 250=125Hz
        }else {
            Buzzer(0);
        }
        USART_znum(f);
        _delay_ms(1000);
    }
}

【2021年2月21日追記】
回路図のリクエストをいただきましたので、現物から書き起こしました。
(注:ATtiny2313への書き込み回路は描いてありません。別途ICクリップで挟んで書き込んでいます。)
追記2: 下記回路図ではRESETのプルアップがないですが、プルアップしておいたほうがいいと思います。何かの拍子にリセットがかかるようです。(データシートの読み込み不足だと思います)

左側の入力には、100均ニッケル水素充電器の2つある電池マイナス端子をそれぞれつなぎます。
入力をショットキーダイオードで半端整流して3端子レギュレータで3.3V電源を作ります。入力はフォトカプラに通して入力をデジタル信号化して、ATTINYのICP端子へ接続、インプットキャプチャ機能でエッジからエッジまでの時間を1us単位で測定します。整流時にショットキーダイオードを使ったのは、電圧降下を小さくしたいためです。
大したことはしていないので、インプットキャプチャ機能のあるマイコンであれば、(ソフトウェアは当然作り直しになりますが)大抵のものが使えるものと思います。