M5Stack Basic/Core2用USB PDトリガモジュール

[M5Stack Advent Calendar 2023](https://qiita.com/advent-calendar/2023/m5stack) 24日目の記事です。

2023年はUDB PDがマイブームでした。秋月電子でもPD Sink ICの取り扱いが始まり自作するハードルが一気に下がりました。
そんな状況の中、USB PD対応充電器からのVBUS出力電圧を制御できるM5Stack向けのUSB PDモジュールを作成したので、設計内容をまとめてみました。

# M5Stack Basic/Core2用USB PDトリガモジュール
![PD Trigger Module](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/a3540000-dca7-d3b0-0e0e-ca9bedaf2ddf.jpeg)

WCH社のUSB PDシンクIC CH224K を使用した、M5Stack Basic/Core2のM-Busに接続する「USB PDトリガモジュール基板」です。
ALLEGRO社のホール電流センサIC ACS712 とパワーFETを搭載し、M5Stackのプログラム操作で以下の制御が行なえます。
(取り出せる電圧は充電器・モバイルバッテリーのサポートする電圧となります)

* USB PD対応充電器やモバイルバッテリーからの5V/9V/12V/15V/20Vの取り出し
* 実際の出力電圧と電流の測定
* VBUS電源出力のON/OFF

### 回路図
pdf版は [こちら](https://github.com/tomorrow56/M5_PDTrigger/blob/main/schematics/M5_PDtrigger_sch.pdf)からダウンロードできます。
 
![Schematic](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/6120d35b-eda6-1452-3c50-532236cd7867.png)

### 設計用データ
完全にオープンソースで、3Dプリント用のケースデータ・M5Stack用サンプルスケッチ・回路図・BOMは筆者の [GitHub](https://github.com/tomorrow56/M5_PDTrigger) で公開しています。

<https://github.com/tomorrow56/M5_PDTrigger>

# USB PDシンクIC CH224K

CH224Kは中国の[南京沁恒微電子(WCH)](https://www.wch.cn/products/CH224.html) 製のシングルチップでUSB PD対応デバイスが作成できるUSB PD受電プロトコルICです。[秋月電子](https://akizukidenshi.com/)で購入することができます。(通販コード　I-18023)
パッケージはESSOP-10（ピン数は10ピン）、VDD端子に1kΩの抵抗経由でVBUS電圧を接続することで動作するので、外部の電源回路(LDO)が不要でシンプルな回路構成にすることができます。

#### CH224Kのパッケージ
![CH224K](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/7b93488b-7b66-a8c7-66f9-e77228138040.jpeg)

USB PD3.0/2.0に対応し，出力電圧検知機能や過熱・過電圧保護などの機能を内蔵しています．また，E-Markerシミュレーションによる最大100WのUSB PDリクエストを送る機能もサポートしています。

### CH224Kの主な機能（製品ページより）
![CH224K_Function](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/ac8b56e6-e108-42e5-fd58-5aaa5dfdc186.png)
* VBUS入力電圧：4〜22V
* PD3.0/2.0，BC1.2等の急速充電プロトコル
* USB Type-C PDの正反挿入検知と自動切り替え
* E-MarkerシミュレーションによるVCONN自動検出と100W出力のPDリクエスト
* USB PDリクエスト電圧の動的調整機能
* 過圧保護（OVA），加熱保護（OTA）

### CH224Kの端子機能一覧
![CH２２４Kの端子機能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/ba30f8d6-92a1-52bc-e829-1dba60c38470.png)

## PDリクエスト電圧の設定方法
PDリクエスト電圧の設定方法は2種類あります。

### CFG1端子のプルダウン抵抗の切替
CFG1端子のプルダウン抵抗の値に応じてPDリクエスト電圧が設定できます。基本的には **機器への組み込みでPDリクエスト電圧を固定で使う** 前提の構成で、PD対応をしていない通常のUSB VBUS電圧の５V設定はありません。
USB充電器からの出力電圧とリクエスト電圧が一致したことは、PG(Power Good)端子を監視することで判断できます。

![PDトリガ機能のリファレンス回路1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/c41b7112-990f-4a7c-5d7d-5872d45d9af2.png)

### 3本のCFG端子のH/Lの組み合わせ
CFG1、2、3の3本の端子を外部からH/Lで制御することでPDリクエスト電圧が設定できます。**マイコン等に接続して電圧を切替えて使う** 前提の構成です。こちらもUSB充電器からの出力電圧とリクエスト電圧が一致したことは、PG(Power Good)端子を監視することで判断できます。
今回のモジュールではこちらの方法を使用しました。

![PDトリガ機能のリファレンス回路2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/953f5a24-73ab-bcee-ae36-639f136477b3.png)

# ホール電流センサIC ACS712
ACS７２１２は米国の[Allegro MicroSystems](https://www.allegromicro.com/en/products/sense/current-sensor-ics/zero-to-fifty-amp-integrated-conductor-sensor-ics/acs712) 製のホール効果ベースリニア電流センサーICです。電流検出定格は５A/20A/30Aがあり、今回は５Aのものを [LCSC](https://www.lcsc.com/product-detail/Current-Sensors_Allegro-MicroSystems-LLC-ACS712ELCTR-05B-T_C44471.html) で購入しました。

#### ACS712のパッケージ(写真はLSCSの製品ページより)
![ACS712ELCTR-05B-T](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/438a33ab-c63c-894d-ed70-d734ef5e18d6.png)

一般的に販売されている電流検出モジュールは、検出用の微小抵抗の両端電圧で電流測定するものが多いですが、大電流が流れると検出抵抗による電圧降下と発熱が問題となります。
ACS712は内部にあるパスを流れる電流を内蔵のホールセンサー回路で検出することで、AC及びDC電流検出を行います。原理的に直流抵抗を小さくできるために、電圧固化や発熱が非常に少ない状態で電流測定をすることができます。

### ACS712の主な機能（データシートより）
![ACS712function](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/2ff7b985-585d-1edb-8b0b-a8b6eabcb8fd.png)

データシートの特徴部分を日本語訳すると以下になります。
* 低ノイズのアナログ信号パス
* 新しいFILTERピンを介してデバイスの帯域幅が設定されます
    * ステップ入力電流に対する出力立ち上がり時間：5µs
    * 帯域幅：80kHz
* TA = 25°Cにおける合計出力エラーが1.5%
* 小型フットプリント、低プロファイルのSOIC-8パッケージ
* 内部導体抵抗:1.2mΩ
* ピン1-4からピン5-8への最小絶縁電圧が2.1kVRMS
* 5.0V、シングルサプライ動作
* 66〜185mV/Aの出力感度
* ACまたはDCの電流に比例した出力電圧
    * 精度は工場で調整済み
    * 非常に安定した出力オフセット電圧
    * ほぼゼロの磁気的ヒステリシス
    * 電源電圧からの比例出力

### ACS712の動作の概要

ACS712は、ダイの表面付近に銅の伝導路を備えた低オフセットのリニアホールセンサー回路で構成されています。この伝導路を電流が流れると磁場が生じ、ホールICによって検出され比例電圧に変換されます。
電流検出経路であるピン1・ピン2→ピン3・ピン4を流れる電流が増えると、デバイスの出力電圧が上昇します。デバイスは最大5倍の過電流状態に耐えることができます。
伝導路の端子は、センサー回路の端子(ピン5〜8)とは電気的に絶縁(最小絶縁電圧:2.1kVRMS)されているので、他に追加する部品なしで電気絶縁が必要な回路に使用できます。

#### Block Diagram
![ACS712BlockDiagram](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/58ae4d9b-be8b-4dc0-b583-11bbf1025f9b.png)

#### 内部構造
![ACS712InternalCurrentPath](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/95f57b1d-bfa1-2891-ff12-1c8224a6033f.png)

### ACS712の端子機能一覧
![ACS712pinDescription](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/257a19c9-97e8-16fa-f4df-4431a672e59d.png)

# 実際の動作

PD出力は汎用性を考え、コネクタではなくターミナル端子にして、リード線が直接つなげるようにしました。
![出力用ターミナル端子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/f83432ff-f5bc-b537-a0c5-95235a3d8aaf.png)

M5Stack用のサンプルプログラムが動作している様子は以下です。A・Cボタンで電圧の切り替え、Bボタンで出力のON-OFFができます。PDの通信が正常にできたらPG(Power Good)のインジケータが緑になります。

![実際の動作](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/c4bfcdbb-c697-f72e-c4d6-56d9e6c22c5f.png)

# ソースコード
最新版はGitHubで公開しています。
[M5_PDTrigger](https://github.com/tomorrow56/M5_PDTrigger)

* ESP32内蔵ADCの非線形特性の補正機能
* 電流値: ふらつきを抑えるために20ポイントの移動平均値を表示
* 出力をOFFにしている期間に電流値の0キャリブレーションを実施
* パラメータ調整機能: CALIBRATE機能で電圧値を表示

```M5_PDtrigger_test.ino
/***************************************************
 * M5Stack PD Trigger (CH224K & ACS712xLCTR-05B)
 * 2023/08/26
 * Copyright(c) @tomorrow56 all rights reserved
****************************************************/

#include <M5Stack.h>
#include "M5StackUpdater.h"

//fot calibration parameter measurement
//#define CALIBRATE

// M5Stack Pin config
// no PSRAM model only 
#define VBUS_I          35
#define VI_I            36
#define CFG1_O          16
#define CFG2_O          17
#define CFG3_O          13
#define VBUSEN_O         2
#define PG_I            12

// M5Stack Core2 Pin config
/*
#define VBUS_I          35
#define VI_I            36
#define CFG1_O          13
#define CFG2_O          14
#define CFG3_O          19
#define VBUSEN_O        32
#define PG_I            27
*/

/***********
CFG Pin Settig
 +----+----+----+-----+
 |CFG1|CFG2|CFG3| OUT |
 +----+----+----+-----+
 |  1 |  - |  - |   5V|
 |  0 |  0 |  0 |   9V|
 |  0 |  0 |  1 |  12V|
 |  0 |  1 |  1 |  15V|
 |  0 |  1 |  0 |  20V|
 +----+----+----+-----+
***********/

/**********
* Calibration parameter
* Changed to match actual measurements with your equipment
**********/
float vScale = 7.83;  // actual VBUS/VBUS_I
float vi_0A = 2.44;  // VI_I(V) @ 0A output
float vi_2A = 2.85;  // VI_I(V) @ 2A output
float vi_0cal = vi_0A;
float   vbus_i_temp[20];  // for moving average

uint8_t PDO = 0;
bool OE = false;

uint32_t updateTime = 0;       // time for next update
uint8_t interval = 100;  // Update interval

#define CURRENT_100MA  (0x01 << 0)
#define CURRENT_200MA  (0x01 << 1)
#define CURRENT_400MA  (0x01 << 2)
#define CURRENT_800MA  (0x01 << 3)
#define CURRENT_1600MA  (0x01 << 4)

void setup(void) {
  // M5Stack::begin(LCDEnable, SDEnable, SerialEnable, I2CEnable);
  M5.begin(true, false, true);
  M5.Power.begin();
  M5.Power.setVinMaxCurrent(CURRENT_100MA);

  if(digitalRead(BUTTON_A_PIN) == 0) {
    Serial.println("Will Load menu binary");
    updateFromFS(SD);
    ESP.restart();
  }

  // Pin initialize
  pinMode(VBUS_I, INPUT);
  pinMode(VI_I, INPUT);
  pinMode(PG_I, INPUT);

  pinMode(CFG1_O, OUTPUT);
  pinMode(CFG2_O, OUTPUT);
  pinMode(CFG3_O, OUTPUT);
  pinMode(VBUSEN_O, OUTPUT);

  // Set PD5V
  digitalWrite(CFG1_O, HIGH); 
  digitalWrite(CFG2_O, LOW); 
  digitalWrite(CFG3_O, LOW);
  PDO = 0;
  // Output disable
  digitalWrite(VBUSEN_O, LOW); 
  OE = false;

  // M5.Lcd.setRotation(1);
  M5.Lcd.fillScreen(TFT_BLACK);
  drawTitle("M5 PD Trigger");
  drawBtnMenu("DOWN", "ON", "UP");
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);

  for(int i = 0; i < 20; i++){
    vbus_i_temp[i] = readVoltage(analogRead(VI_I));
    delay(20);
  }

  vi_0cal = averageVI();

  updateTime = millis(); // Next update time
}

void loop() {
  float vbus_v;
  float vbus_i;
  char buf1[5];
  char buf2[5];

  M5.update();

  if (updateTime <= millis()) {
    updateTime = millis() + interval; // Update interval

    vbus_v = readVoltage(analogRead(VBUS_I)) * vScale;  // read data from CH1
    // 20 moving averages
    for(int i = 19; i > 0; i--){
      vbus_i_temp[i] = vbus_i_temp[i - 1];
    }
    vbus_i_temp[0]= readVoltage(analogRead(VI_I));    // read data from CH2
    vbus_i = (averageVI() - vi_0cal) / ((vi_2A - vi_0A) / 2);

    #ifndef CALIBRATE
      dtostrf(vbus_v,4,1,buf1);
      drawText("Vout = " + (String)buf1 + " V    ", 1, 0);
      Serial.printf("Vout = %s V\n", buf1);

      dtostrf(vbus_i,5,2,buf2);
      drawText("Iout = " + (String)buf2 + " A    ", 1, 1);
      Serial.printf("Iout = %s A\n", buf2);
    #else
      drawText("Vout(raw) = " + (String)readVoltage(analogRead(VBUS_I)) + " V    ", 1, 0);
      Serial.println("Vout(raw) = " + (String)readVoltage(analogRead(VBUS_I)) + " V");

      drawText("Iout(raw) = " + (String)readVoltage(analogRead(VI_I)) + " V    ", 1, 1);
      Serial.println("Iout(raw) = " + (String)readVoltage(analogRead(VI_I)) + " V");
    #endif


    switch(PDO){
      case 0:
      digitalWrite(CFG1_O, HIGH); 
      digitalWrite(CFG2_O, LOW); 
      digitalWrite(CFG3_O, LOW);
      drawSubTitle("PDO 5V    ");
      break;
      case 1:
      digitalWrite(CFG1_O, LOW); 
      digitalWrite(CFG2_O, LOW); 
      digitalWrite(CFG3_O, LOW);
      drawSubTitle("PDO 9V    ");
      break;
      case 2:
      digitalWrite(CFG1_O, LOW); 
      digitalWrite(CFG2_O, LOW); 
      digitalWrite(CFG3_O, HIGH);
      drawSubTitle("PDO 12V    ");
      break;
      case 3:
      digitalWrite(CFG1_O, LOW); 
      digitalWrite(CFG2_O, HIGH); 
      digitalWrite(CFG3_O, HIGH);
      drawSubTitle("PDO 15V    ");
      break;
      case 4:
      digitalWrite(CFG1_O, LOW); 
      digitalWrite(CFG2_O, HIGH); 
      digitalWrite(CFG3_O, LOW);
      drawSubTitle("PDO 20V    ");
      break;
      default:
      digitalWrite(CFG1_O, HIGH); 
      digitalWrite(CFG2_O, LOW); 
      digitalWrite(CFG3_O, LOW);
      drawSubTitle("PDO 5V    ");
      break;
    }

    if(OE == true){
      digitalWrite(VBUSEN_O, HIGH);
      Serial.println("Output Enabled");
    }else{
      digitalWrite(VBUSEN_O, LOW); 
      Serial.println("Output Disabled");
      vi_0cal = averageVI();
    }
    
    drawText("PG", 0, 3);
    if(vbus_v >= 3){
      if(digitalRead(PG_I) == LOW){
        M5.Lcd.fillRect(40, 150, 30, 30, TFT_GREEN);
        Serial.println("PG OK");
      }else{
        M5.Lcd.fillRect(40, 150, 30, 30, TFT_RED);
        Serial.println("PG NG");
      }
    }else{
      M5.Lcd.fillRect(40, 150, 30, 30, TFT_BLACK);
      M5.Lcd.drawRect(40, 150, 30, 30, TFT_WHITE);
    }

  }

  if(M5.BtnA.wasPressed()){
    if(PDO > 0){
      PDO--;
    }
  }

  if(M5.BtnB.wasPressed()){
    if(OE == false){
      OE = true;
      drawBtnMenu("DOWN", "OFF", "UP");
      M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK);
    }else{
      OE = false;
      drawBtnMenu("DOWN", "ON", "UP");
      M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
    }
  }

  if(M5.BtnC.wasPressed()){
    if(PDO < 4){
      PDO++;
    }
  }
}

void drawTitle(String Title){
  M5.Lcd.setTextSize(1);
  M5.Lcd.fillRect(0, 0, 320, 30, TFT_BLUE);
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLUE);
  M5.Lcd.drawCentreString(Title, 160, 2, 4);
}

void drawSubTitle(String SubTitle){
  M5.Lcd.setTextSize(1);
  M5.Lcd.drawString(SubTitle, 10, 33, 4);
}

void drawText(String Text, int xPos, int yPos){
  M5.Lcd.setTextSize(1);
  M5.Lcd.drawString(Text, xPos * 30, yPos * 30 + 60 + 3, 4);
}

void drawBtnMenu(String A, String B, String C){
  M5.Lcd.setTextSize(1);
  M5.Lcd.fillRect(0, 210, 320, 30, TFT_BLUE);
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLUE);
  M5.Lcd.drawCentreString(A, 65, 214, 4);
  M5.Lcd.drawCentreString(B, 160, 214, 4);
  M5.Lcd.drawCentreString(C, 255, 214, 4);
}

float readVoltage(uint16_t Vread){
  float Vdc;
  // Convert the read data into voltage
  if(Vread < 5){
    Vdc = 0;
  }else if(Vread <= 1084){
    Vdc = 0.11 + (0.89 / 1084) * Vread;
  }else if(Vread <= 2303){
    Vdc = 1.0 + (1.0 / (2303 - 1084)) * (Vread - 1084);
  }else if(Vread <= 3179){
    Vdc = 2.0 + (0.7 / (3179 - 2303)) * (Vread - 2303);
  }else if(Vread <= 3659){
    Vdc = 2.7 + (0.3 / (3659 - 3179)) * (Vread - 3179);
  }else if(Vread <= 4071){
    Vdc = 3.0 + (0.2 / (4071 - 3659)) * (Vread - 3659);
  }else{
    Vdc = 3.2;
  }
  return Vdc;
}

float averageVI(){
  float vi_sum = 0;
  for(int i = 0; i < 20; i++){
    vi_sum = vi_sum + vbus_i_temp[i];
  }
  return vi_sum / 20;
}
```


# おわりに
BOOTHの方でも完成品を販売していますので、USB PDに興味がある方はぜひ遊んでみてください。
[M5Stack Basic/Core2用USB PDトリガモジュール](https://thousandiy.booth.pm/items/5141442)

### では、皆さん良いM5Stack生活を！ USB PDはいいぞ！