ESP32S3 CartBase

Stepper-Motor2個 VL53L1X ICM42670P を使ったCartのベース
BLE-UART無線リモコン
ESP32のPWM信号発生機能でStepper-Motor駆動

2026.7.9 Coskx Lab  

1 はじめに

Xiao ESP32S3で,小型ステッパモータ2個で動くカートのベースを作ります。次の内容が含まれます。
・小型ステッパモータ2個を回転角に変換できる任意の指令値でPWM信号を生成して回転させる
・6軸センサICM42670Pで姿勢取得(I2C通信,アドレスに注意)
・TOF距離センサVL53L1Xで距離測定(I2C通信)
・BLE-UARTを使ってスマホなどのターミナルアプリと文字列の送受信
これらの機能が使える状態のカートベースなので,意味のある動作は何もできません。
しかし,このカートベースを利用すれば,所望の動作ができるカートなどを製作することができます。
例えば,スマホなどのターミナルアプリを使ったリモコン動作や,センサの値に基づいた自律動作をするカートが考えられます。

これらの機能を使ったプログラムをゼロから作成するのはかなり大変になります。 そこでPWM信号生成,6軸センサ,TOF距離センサ,BLE-UARTについてクラスが用意し,プログラミングの負担を軽減しています。
それぞれクラスのインスタンスの初期化を,setup()内で行い,loop()中でインスタンスを使って作業します。

このカートベースでは,スマホなどのターミナルからBLE-UARTで文字列が送られてきたら,あらかじめ決めておいたコマンド文字列との一致を調べ, 一致したものがあったら,そのコマンドに対応した作業を行うようになっています。
また,6軸センサICM42670Pの較正(キャリブレーション)も'cal'コマンドで行います。

それぞれの機能に関する詳細な説明は次を参照してください。

 BLE通信
BLE通信のページへ ≫

 ステッピングモータ制御(ESP32のPWM信号発生機能利用)
ステッピングモータ制御のページへ ≫

 TOFレーザ距離センサ
TOFレーザ距離センサのページへ ≫

 6軸センサICM42670P
6軸センサICM42670Pのページへ ≫


2 使用環境

3 想定する配線

3.1 信号配線
次のような信号配線を想定しています。
2つのステッパモータドライバの先にはそれぞれステッパモータが接続される想定です。



I2Cデバイスを2つ使用しています。I2C通信は動作に失敗することがあります。I2C通信のケーブルはできるだけ短く(それぞれ20cm程度以下)し,プルアップ抵抗を取り付けます。また,プルアップ抵抗の取り付け位置には注意が必要です。
下図でA,B,Cのどこにつけても回路的には同じように見えますが,ノイズや信号の反射の影響を避けるためできるだけ通信元のマイコン近くになるようにAの位置に取り付けます。
また,BとCのように2か所にプルアップ抵抗を付けないようにします。



3.2 電圧監視の工夫
リチウムポリマー電池を使用する場合,電池を過放電させると電池寿命を縮め,場合によっては充電できないなどのトラブルが発生します。
そのため,電池電圧をモニターし,設定値より低い電圧になったら,知らせる仕組みを持てるようにしておくと電池の過放電を防ぐことができます。
後述のプログラムでは,この工夫を有効にする関数が含まれています。



3.3 ICM42670の初期化失敗に対する対処
ICM42670は高速通信に対応しているため,ノイズ,反射波に対して敏感です。
一方,TOF距離センサVL53L1Xの信号入出力線にはレベルシフタ(FXMA2102)のアクセラレータ機能がついており,この二つは組み合わせとしてはあまりよくないことが後からわかりました。
そのため,ICM42670は通信できず,プログラムのsetup()内での初期化に失敗することがあります。
そのような場合は,以下の対策が考えられます。
・ICM42670基板上の0.1μFのセラミックコンデンサを1または10μFの積層セラミックコンデンサに変更する。
・プルアップ抵抗を4.7kΩから2.2kΩに替える。
・並列接続ですがGND線,3.3V線はデバイス間を数珠つなぎにせず,それぞれESP32S3のGND,3.3Vにつなぎます。(信号レベルの安定化対策)
・デバイスの3.3VとGND間に0.1μFのセラミックコンデンサと10μFの電解コンデンサを並列に接続する。(高周波電源ノイズ対策)
・SDA線とSCL線をシールド線にする。フラットケーブルを使いSDA線とSCL線の間にGND線を通す。(信号線の浮遊容量対策)
・SDA線とSCL線の途中に10Ωほどのダンピング抵抗を入れる。(信号線の浮遊容量対策)

初期化に失敗する場合 奥の手
ICM42670の3.3V端子に指を触れたままでプログラムのsetup()でのICM42670の初期化を試みてください。
うまく起動出来るなら,ICM42670の3.3V端子に20cmほどの被覆線をはんだ付けして,終端側は被覆を剥かずに放置して下さい。理由はよくわかりませんが,これでうまく行くかもしれません。


4 カート制御ベースプログラム

setup()では,BLE,姿勢取得,ステッパモータ,距離センサの各インスタンスの初期化が行われます。
loop()には,BLE通信が途切れたとき,ICM42670キャリブレーションが要求されたときなどに, それぞれやるべきことが先頭部分に書かれています。
loop()中の基本動作はmicrosDifferenceとoperatingPeriodを比較しながら,基本動作周期(5msec)ごとに実行され, 姿勢取得,距離センサからのデータ取得が行われ, 姿勢表示,あるいは距離表示要求があった時には1秒ごとにそれに対応しています。
ICM42670は1msecごとにデータが更新されているのですが,VL53L1Xはデータ更新に20から30msecを要します。
そのため,5msecごとに距離データを使用しても,最悪6周期前の値を使うことになるかもしれません。
ステッパモータは見世物として動作するように,1秒間隔でモータ指令値が更新されていますが, モータ指令値は5msecごとにステッパモータドライバに与えています。
なお,ステッパモータは,次のモータ指令値が送信されるまで,前のモータ指令値で動作し続けます。

BLE-UART受信とコマンド文字列割り当て
StringReceptionReport()は,BLE-UARTを通じて文字列が送られてきたときに呼び出され, 送られてきた文字列に対応した要求を行います。(この要求は,enumクラスの変数に保存されています。)
コマンド文字列は次のものを割り当てています。

文字列解釈
calICM42670キャリブレーション要求
att姿勢表示要求
dst距離表示要求
motステッパモータデモ要求
oal姿勢表示距離表示ステッパモータデモ同時要求
stpすべての要求取り消し

BLE初期設定
BLE初期設定ではPINコード(パスワード)は使っていません。デバイス名は他との衝突を避けるため,変更して使ってください。

ICM42670のキャリブレーション
ICM42670のキャリブレーション時には角速度センサのオフセット誤差を測定しています。
そのため,ICM42670を静止させた状態で行ってください。

姿勢表示距離表示ステッパモータデモ同時要求
モータ駆動は電磁ノイズを発生します。一方I2Cの通信はノイズの影響を受けやすいです。
そのため,モータを駆動しながら,姿勢表示・距離表示が正しく行われるかどうかのチェックをしています。

LEDの点滅
(1) 起動時に2つのセンサの初期化が終わると,BLEのアドバタイジング(BLE接続要求)が始まりますが,同時にマイコンに組み込まれているLEDを3Hzで点滅させて知らせます。
3Hz点滅が始まらなかったら2つのセンサの初期化に失敗したことになります。
(2)電源にリチウムポリマー電池を使用している場合は,指定電圧より電圧が下がったときに6Hz点滅で知らせます。

追加したクラス
このプログラムで利用している追加したクラスは次の3つです。
(1)AttitudeDetectorICM42670Coarse
6軸センサICM42670に関するクラスを元にした,ラッパークラスです。
キャリブレーション機能を追加し,角速度オフセットエラー訂正したデータ取得・オイラー角取得ができる。
(2)Handy_BLE_Uart
BLE通信クラス群をラップしたラッパークラスBLE通信クラスです。
BLE通信の面倒な設定を舞台裏へ任せ,文字列のBLE-UART送受信を行う。
(3)StepperMotorESP32
Ledc関数群(PWM信号発生)をラップした,ステッパモータ操作のためのクラスです。
ステッパモータへの指令値を-1.0から+1.0の範囲で与えることでできる。
(絶対値が大きいほど回転角が大きくなる,-符号は逆転)
76.29394531Hzごとの離散的な周波数で,76.29394531Hzから78125までのPWM信号を発生します。(1024段階)
このプログラムでは左右のモータに対応した2つのインスタンスを使用しています。
(4)BuiltinLED
マイコンXiaoESP32S3に組み込まれている黄色のLEDの点滅動作を行う。

使用ファイル
ArduinoIDEを作業では,3つのクラス定義のための6個のファイルを含む,次の7つのファイルを使います。

9つのファイルは次からダウンロードできます。
9つすべてのファイルを同一作業フォルダに入れて作業してください。

    9つのファイルのダウンロード

外部ライブラリー
ArduinoIDEで作業する前に,次の2つの外部ライブラリをIDEに組み込んでください。
(IDEの「ツール」>「ライブラリを管理...」で検索してダウンロード)
  ・ICM42670P by TDK/Invensense
  ・Madgwick by Arduino

  CartBaseプログラム本体(上記zipファイルに含まれている)

//TWIPRoboBaseSteper1ICM42670BLE.ino
//library: ICM42670P by TDK/Invensense
//library: Madgwick by Arduino
//library: vl53l1x-arduino by Pololu

//Assigning Command Strings
// 'cal': execute calibration.
// 'att': measure attitude.
// 'dst": measure distance.
// 'mot': test stepper motor.
// 'stp': stop everything.
// 'oal': execute overall operation.

#include <AttitudeDetectorICM42670Coarse.h>
#include <Handy_BLE_Uart.h>
#include <StepperMotorESP32.h>
#include <VL53L1X.h>
#include <BuiltinLED.h>

#define BLE_DEVICENAME "CartBase"

const uint32_t operatingFrequency = 200;  //[Hz] Base operating frequency 200[Hz]
const uint32_t operatingPeriod = 1000000 / operatingFrequency; //[us] Base operating period 5000[us]=5[ms]
const uint32_t MinPulseperiod = 20;  //[us] Minimum period of the step pulse for stepper motors

AttitudeDetectorICM42670Coarse Attitude_Detector(Wire, true);//default true (ICM42670's address 0x69)
Handy_BLE_Uart bleuart;
StepperMotorESP32 stepperR, stepperL; //Right and Left stepper motor
VL53L1X vl53l1x;
BuiltinLED builtinled;          //class BuiltinLED のインスタンス

//Assigning pins
const uint8_t Pin_R_DIR  = D0; // Pin number used for the right motor's "dir" signal
const uint8_t Pin_R_STEP = D1; // Pin number used for the right motor's "step" signal
const uint8_t Pin_L_DIR  = D2; // Pin number used for the left motor's "dir" signal
const uint8_t Pin_L_STEP = D3; // Pin number used for the left motor's "step" signal
const uint8_t Pin_Enable = D6; // Pin number used for the motor driver's "Enable" (negative logic)
const uint8_t POWER_MONITORING_PIN = D10; //LiPo Battery Voltage Monitoring

enum class ERequest {
    non,
    cal,
    att,
    dst,
    mot,
    oal
};

unsigned long microsPrevious;
ERequest request = ERequest::non;
ERequest previousrequest = ERequest::non;

void setup() {
  int ret;
  Serial.begin(115200);
  delay(1000);
  //Serial.println("operatingPeriod " + String(operatingPeriod));
  //Wire.begin();
  //Wire.setClock(10000);  //I2C clockの設定 [Hz] defaultは400000 I2C不調なら10000 or 10000

  //*** initialize Attitude_Detector ***
  Attitude_Detector.start(operatingFrequency, -65, 6, -89);

  //*** initialize TOF Distance Sensor VL53L1X ***
  Serial.println("vl53l1x initialization started");
  vl53l1x.setTimeout(500); //[msec]
  if (!vl53l1x.init()) {
    Serial.println("Failed to detect and initialize vl53l1x!");
    while (1);
  }
  //set long distance range 
  vl53l1x.setDistanceMode(VL53L1X::Short);
  //set timing budget: 20000usec
  vl53l1x.setMeasurementTimingBudget(20000);
  //set measurement interval in Continuous Measurement Mode: 20msec
  vl53l1x.startContinuous(20);
  Serial.println("vl53l1x initialization completed");

  //*** initialize BLE ***
  builtinled.startBlinking();
  bleuart.setInitialMessage("ICM42670_VL53L1X_Stepper_BLE\n Send me 'cal', 'att',  'dst',  'mot', 'oal',  or 'stp'");
  //bleuart.begin(BLE_DEVICENAME, 123456); //uses passkey 123456
  bleuart.begin(BLE_DEVICENAME); //without passkey
  while (!bleuart.isConnected()) delay(100);
  builtinled.stopBlinking();
  delay(1000);

  //*** initialize stepper motors ***
  stepperR.setPins(Pin_R_DIR, Pin_R_STEP, Pin_Enable);
  stepperR.disableDriver();
  stepperR.setControlValue(operatingFrequency, MinPulseperiod); //200[Hz] 40[us]
  stepperR.enableDriver();
  stepperL.setPins(Pin_L_DIR, Pin_L_STEP, Pin_Enable);
  stepperL.disableDriver();
  stepperL.setControlValue(operatingFrequency, MinPulseperiod); //50[msec] 40[us]
  stepperL.enableDriver();

  microsPrevious = micros() - operatingPeriod ;

  Serial.println("end of setup");
  bleuart.transferString("end of setup");
}

void loop() {
  float ax, ay, az; //[mm/s2] acceleration xyz
  float gx, gy, gz; //[deg/sec] angular velocity xyz
  float pitch, roll, yaw; //[deg]Euler angle
  static int distance = 0; //[mm]
  static VL53L1X::RangeStatus diststatus = VL53L1X::RangeValid;
  unsigned long microsNow, microsDifference;
  String message;

  const double Motor_Drive_dutyRatios[] = {-0.8, -0.6, -0.4, -0.2, 0., 0.2, 0.4, 0.6, 0.8};
  const static int nRatios = sizeof(Motor_Drive_dutyRatios)/sizeof(double);
  static int increment = 1; //デューティー比番号dutyRatioIndex変更用変数
  static int dutyRatioIndex = (nRatios>>1);

  static int cntr = 0;

  powermonitoring(); //リポバッテリ電圧監視 LED点滅で電圧低下を知らせてくる

  if (!bleuart.isConnected()) {
    request = ERequest::non;
    return;
  }

  if (bleuart.ReceivedStringReady()) { //Polling
    processReceivedString(bleuart.getReceivedString());
  }

  if (request == ERequest::non) {
    stepperR.drive(0.);
    stepperL.drive(0.);
    return;
  }

  if (request == ERequest::cal) {
    stepperR.drive(0.);
    stepperL.drive(0.);
    request = ERequest::non;
    executeCalibration();
    return;
  }

  microsNow = micros();
  microsDifference = microsNow - microsPrevious;
  if (microsDifference >= operatingPeriod ) {
    if (microsDifference > operatingPeriod  * 1.5) {
      microsPrevious = microsNow;
    } else {
      microsPrevious = microsPrevious + operatingPeriod ;
    }

    //Must be executed at the operatingFrequency.
    Attitude_Detector.getAttitude(ax, ay, az, gx, gy, gz, pitch, roll, yaw);

    if (vl53l1x.dataReady()) {
      distance = vl53l1x.read(false);
      diststatus = vl53l1x.ranging_data.range_status;
    }

    cntr++;
    if (cntr < operatingFrequency) return;
    cntr = 0;

    if (request == ERequest::att
        || request == ERequest::oal) {
      Serial.println("request == ERequest::att");
      message = "acc: " + String(ax) + ", " + String(ay) + ", " + String(az);
      bleuart.transferString(message);
      message = "angvel: " + String(gx) + ", " + String(gy) + ", " + String(gz);
      bleuart.transferString(message);
      message = "Eulang: " + String(pitch) + ", " + String(roll) + ", " + String(yaw);
      bleuart.transferString(message);
    }

    if (request == ERequest::dst
        || request == ERequest::oal) {
      Serial.println("request == ERequest::dst");
      message = "distance: " + String(distance) + " status: " +String(diststatus);
      bleuart.transferString(message);
    }

    if (request == ERequest::mot
        || request == ERequest::oal) {
      Serial.println("request == ERequest::mot");
      double dutyRatio_Value = Motor_Drive_dutyRatios[dutyRatioIndex];
      stepperR.drive(dutyRatio_Value);
      int stepsL = stepperL.drive(dutyRatio_Value);
      message = "motor duty: " + String(dutyRatio_Value) + ", " + String(stepsL);
      bleuart.transferString(message);

      dutyRatioIndex += increment;
      if (nRatios-1 <= dutyRatioIndex) increment = -1;
      if (dutyRatioIndex < 1) increment = 1;
    }
  }
}

//Command Parser
void processReceivedString(String str) {
  Serial.println("RX_Value = " + str);
  //開発初期の確認用エコーバック 不要になったらコメントアウトして送信を減らす
  bleuart.transferString(str);
  previousrequest = request;
  if (str.indexOf("cal") == 0) request = ERequest::cal;
  else if (str.indexOf("att") == 0) request = ERequest::att;
  else if (str.indexOf("dst") == 0) request = ERequest::dst;
  else if (str.indexOf("mot") == 0) request = ERequest::mot;
  else if (str.indexOf("oal") == 0) request = ERequest::oal;
  else if (str.indexOf("stp") == 0) request = ERequest::non;
}

//Calibration of Attitude_Detector Gyro
void executeCalibration()
{
  Serial.println("Received your calibration request.");
  Serial.println("Keep the sensor motionless.");
  bleuart.transferString("Received your calibration request.");
  bleuart.transferString("Keep the sensor motionless.");
  Attitude_Detector.calibrateGyro();
  int16_t gxc, gyc, gzc;
  Attitude_Detector.getGyrOffset(gxc, gyc, gzc);
  String result = "calibration value " + String(gxc) + ", " + String(gyc) + ", " + String(gzc);
  bleuart.transferString(result);
  Serial.println("Calibration has done.");
  bleuart.transferString("Calibration has done.");
}

void powermonitoring() {
  static int ledind = 0;
  //power monitoring begin
  const double ADCfactor = (30. + 4.7) / 4.7; 
  const int thresholdVoltage = 11000;  //[mV]
  int analogVolts = analogReadMilliVolts(POWER_MONITORING_PIN); //[mV]
  int EstimatedPowerSupplyVoltage = analogVolts * ADCfactor;
  if (EstimatedPowerSupplyVoltage <= thresholdVoltage && ledind == 0) {
    builtinled.startBlinking2();
    ledind = 1;
  } else if (thresholdVoltage <EstimatedPowerSupplyVoltage && ledind == 1) {
    builtinled.stopBlinking();
    ledind = 0;
  }
}

6 実行の様子

androidスマホのSerial_Bluetooth Terminalを使ってBLE-UART通信で作業をします。
 androidスマホのSerial_Bluetooth Terminalについては次を参照してください。
BLE通信のページへ ≫

ESP32S3のシリアル通信先(ArduinoIDEのシリアルモニタなど)には次のように表示されます。I2Cデバイスとの通信がうまく行かず,それらの初期化に失敗すると,表示が途中で停まってしまします。
ICM42670の起動に失敗した場合は「3.3 ICM42670の初期化失敗に対する対処」を参考にしてください。



スマホなどのBLEターミナルがBLE通信できるようになると,スマホなどの表示は次のような実行例になります。
$$で始まる行はBLEターミナルが送信した内容です。
ESP32S3と接続が完了したら,calコマンドにより最初にキャリブレーションしています。
その後,attコマンドにより姿勢表示,dstコマンドにより距離表示,motコマンドによりステッパモータデモを順番に行い,最後に通信を切断しています。
姿勢取得,距離取得は5msecごとに行われていますが,表示は間引きされていて1000msecごとになっています。またモータ指令値変更と指令値表示も1000msecごとに行われています。

    WinPCのBLE_Serial_Terminal実行画面のログ

16:39:28: >connecting CartBase ..
16:39:30: >connected
16:39:31: ICM42670_VL53L1X_Stepper_BLE
Send me 'cal', 'att',  'dst',  'mot',  'oal',  or 'stp'
16:39:36: $$cal
16:39:36: cal
16:39:36: Received your calibration request.
16:39:36: Keep the sensor motionless.
16:39:37: calibration value -108, -1, -120
16:39:37: Calibration has done.
16:39:41: $$att
16:39:41: att
16:39:42: acc: 0.76, 0.12, 9.85
16:39:42: angvel: 0.00, 0.02, 0.02
16:39:42: Eulang: -4.39, 0.74, -0.02
16:39:43: acc: 0.74, 0.14, 9.86
16:39:43: angvel: -0.08, -0.07, 0.02
16:39:43: Eulang: -4.33, 0.76, -0.02
16:39:44: acc: 0.75, 0.14, 9.85
16:39:44: angvel: 0.08, -0.04, 0.03
16:39:44: Eulang: -4.39, 0.80, -0.03
16:39:45: acc: 0.76, 0.12, 9.86
16:39:45: angvel: -0.14, 0.04, -0.14
16:39:45: Eulang: -4.40, 0.68, -0.02
16:39:45: $$stp
16:39:45: stp
16:39:48: $$dst
16:39:48: dst
16:39:48: distance: 1064 status: 0
16:39:49: distance: 1070 status: 0
16:39:50: distance: 1066 status: 0
16:39:51: distance: 1066 status: 0
16:39:52: distance: 1077 status: 0
16:39:53: $$stp
16:39:53: stp
16:39:55: $$mot
16:39:55: mot
16:39:55: motor duty: 0.00, 0
16:39:56: motor duty: 0.20, 50
16:39:57: motor duty: 0.40, 100
16:39:58: motor duty: 0.60, 150
16:39:59: motor duty: 0.80, 200
16:40:00: motor duty: 0.60, 150
16:40:01: motor duty: 0.40, 100
16:40:01: $$stp
16:40:01: stp
16:40:04: >disconnected

distance: 1064 status: 0
これのstatus=0のときに正常動作ですので,1064mmという計測値は信用できます。
status=0でないときの距離計測値は信用できません。
なお,ここではoalの要求は行っていません。


7 考察

ここでの設定
const uint32_t operatingFrequency = 200; //[Hz] Base operating frequency 200[Hz]
const uint32_t MinPulseperiod = 20; //[us] Minimum period of the step pulse for stepper motors
は,ステッパモータを駆動してみると,駆動周波数が高くなりすぎのように思えます。
const uint32_t operatingFrequency = 100; //[Hz] Base operating frequency 200[Hz]
const uint32_t MinPulseperiod = 60; //[us] Minimum period of the step pulse for stepper motors
くらいが適正ではないかと思えます。実際の動作を確かめながら,設定を考えたほうがよさそうです。

クラスStepperMotorESP32ではPWM信号発生にAPI ledcを使用しています。
PWM周波数切り替え時に,関数ledcChangeFrequency()を使用しています。
この関数はPWM周波数切り替え時に初期化PWM信号発生機構を初期化してしまい,およそ,340μsecだけPWM信号をLにしてしまいます。
100Hzくらいの頻度で関数ledcChangeFrequency()を実行すると,意図したPWM信号とは少し異なるパルスが発生します。
頻繁にPWM周波数切り替える用途には向かないステッパモータ駆動法と考えられます。
詳細は次を参照してください。

Xiao ESP32S3 でPWM信号発生テスト ≫


8 まとめ

Xiao ESP32S3を使って,BLE-UART通信が可能で,6軸センサICM42670P,TOF距離センサVL53L1Xを搭載し, 2つのステッパモータで移動可能なカートを想定したデモプログラムを作成しました。

★重要 このデモプログラムではステッパモータ向けのパルス信号をESP32のAPI Ledcを用いて生成しています。
「7 考察」の問題点を考慮して,頻繁なPWMパルス周波数変更があるような用途にAPI ledcを使用した本ページ記述の方法は避けるべきと考えます。

このデモプログラムは,動作としては意味を持ちませんが, BLE-UARTでの無線リモコンで操作できるカートのひな型となっています。
姿勢データや距離データをもとにモータ指令値を生成し, デモプログラムのように5msecごとにステッパモータドライバに与えるようにすれば, 自律動作を行うカートも製作できると思います。
コマンド文字列の割り当ては,ここのプログラム例にとらわれることなく, 動作に即したものになるよう自由に決めればよいと思います。

このデモプログラムをひな型にして,改造するときは,各要素のプログラミングに慣れる必要があります。
そのときには,「1 はじめに」で紹介している,各要素の使い方ページを参照するとよいと思います。