mbedによるロボット制御

- NucleoF446REの説明
- 環境構築手順
- main.cppをつくる
- デフォルト設定の上書き
- 接続、書き込み
- mbed OSのAPIを使いこなす
- DigitalOut
- DigitalIn
- PwmOut
- Wait
- HighResClock
- CAN
- Lチカ
- C620
- オムニ

Nucleo F446RE
非常に優秀なマイコン。Arduinoよりこいつを使うべき。[1][2]
- ピン数が多い
- 計算速度が早い(最大クロック180MHzかつFPU付き)
- CANが喋れる
- 全ピン割り込み対応(16chまで)
- ユニバーシティプログラムで無料配布される
ピンアサインについて、基本的にはMbedが公開しているサイトを見ながら決める。
紺色で示されたピンなどデフォルトでは使用できないピンがあるので注意すること。[3]
リセットは黒いボタン。
四角形のLEDはステータスLEDとなっている。
Nucleo F303K8
小さめのNucleo。[4][5]
CAN MDなどのECUにこのチップを使用しがち。
ピン数と性能は少し下がるが、ユニバーシティプログラムで大量にもらうことができる。

Nucleo G474RE
F446REより優秀かもしれん。
値段は200円くらいしか変わらず、FDCANが3つ使える。
しかも三角関数の計算を早くできる。
ただしMbed OSではこいつの性能を活かし切ることができないので、全性能を活かしたければCubeのHALを使うことになる。

環境構築手順
VS Code
VS Codeはプログラムを書くためのエディタ。
ダウンロードリンク
Windows向け
ダウンロードリンクよりインストーラをダウンロード。
インストーラを実行してインストール。
WindowsはPythonも必要?
Ubuntu
ダウンロードリンクよりcode_<バージョンとかの文字>.deb
をダウンロード。
コマンドよりインストール。
sudo apt install code_<バージョンとかの文字>.deb
Platform IO拡張機能
Platform IO(以降pio)というVS Codeの拡張機能を入れる。
pioはマイコン向けのプログラムを書くために必要。
Linuxユーザ向け追加手順
Linuxでpioを使用するためにはpython3-venv
が必要なので追加でinstallする。
Ubuntuなら下記コマンドでインストール可能。
sudo apt install python3-venv
python3-pip
も必要かも?
プロジェクトを作成。
「PlatformIO Home」の「New Project」をクリック。
Boardを「Nucleo F446RE」、Frameworkを「Mbed」として作成。

main.cppをつくる
プロジェクトを作成すると以下のようなディレクトリ構造が作成される。
.
├── include/
│ └── README
├── lib/
│ └── README
├── src/
│ └── main.cpp
├── test/
│ └── README
└── platformio.ini
src/main.cpp
を開いて以下を貼り付ける。もし無ければ作る。
#include <mbed.h>
int main() {
// put your setup code here, to run once:
// 1度しか走らないセットアップのコードをここに書く。
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
// 繰り返し実行するメインのコードをここに書く。
}
}

デフォルト設定の上書き
platformio.ini
は以下のような感じになっていると思う。(一部省略)
[env:nucleo_f446re]
platform = ststm32
board = nucleo_f446re
framework = mbed
これを編集して下記の通りにする。
+[platformio]
+build_cache_dir = .cache
[env:nucleo_f446re]
platform = ststm32
board = nucleo_f446re
framework = mbed
+monitor_speed = 115200
+monitor_filters =
+ log2file
+ time
+ default
+build_flags = -std=gnu++17
+build_unflags = -std=gnu++14
mbed_app.jsonをつくる
mbedのデフォルト設定では、ビルドが遅い&printfが貧弱なので設定を上書きする。[1]
mbed_app.json
というファイルを作成して、下記内容を貼り付け。
{
"requires": [
"bare-metal"
],
"target_overrides": {
"*": {
"target.c_lib": "std",
"platform.minimal-printf-enable-64-bit": true,
"platform.minimal-printf-enable-floating-point": true,
"platform.minimal-printf-set-floating-point-max-decimals": 6,
"platform.stdio-baud-rate": 115200
}
}
}
上記設定の説明
-
build_cache_dir = .cache
によってビルドにキャッシュを使用。これで2回目以降のビルドが高速化する。使用しないキャッシュは容量を食う原因なので、容量が気になるなら.cache
をディレクトリごと消せばよい。
ビルドがうまくいかないときも.cache
を削除すると解決することがある。 -
monitor_speed
、platform.stdio-baud-rate
によってマイコンとPCの通信速度を9600→152000に上げる。 -
monitor_filters
でログの時間を表示、ログをファイルに残す。 -
build_flags
、build_unflags
でバージョンをC++14→C++17とする。ここはお好みだがC++14だと使いたい機能が使えないことが多い。build_flags = -std=gnu++2a
とすればC++20の機能も一部使える。またC++17にするとWregister
で警告がでるが無視してよい。気になるならextra_scripts
を使用する。 -
"requires"
を"bare-metal"
のみとすることで、不要なWiFi等の機能を省く。必要なら各自追加のこと。 -
"platform.minimal-printf-***"
のあたりはfloat等のprintf出力を有効化するもの。メモリ使用量が気になるなら消してよい。

接続、書き込み
Linuxの場合
pioのドライバをインストール
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules
USBケーブルでマイコンとPCを繋ぐ。
するとpioがマイコンを認識するので左下の「Upload」ボタン、またはCtr+Alt+U
を押して自身の書いたプログラムをマイコンに書き込む。
右下の「Monitor」ボタン、またはCtr+Alt+M
を押してシリアルモニタを起動する。そしてマイコンのリセットボタンを押す度に1度だけ"setup"と表示される。

Lチカ
Nucleo F446REにはユーザボタンとLEDが乗っているため、それらを利用してLチカを行う。
API Listより、DigitalIn
とDigitalOut
を見つけ出して開く。
コード例を参考にしつつ、ユーザボタンが押されていない間のみLEDが点灯するコードを書く。
コード例
#include <mbed.h>
DigitalIn button{BUTTON1};
DigitalOut led{LED1};
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
if(!button) {
led = !led;
ThisThread::sleep_for(100ms);
}
}
}
PwmOut
を使用してボタンが押されるたびにLEDの明るさが変わるコードを書く。
コード例
#include <mbed.h>
DigitalIn button{BUTTON1};
DigitalOut led{LED1};
bool pre_button = false;
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
if(!pre_button && button) {
float out = led.read() + 0.1;
if(out > 1) {
out = 0;
}
led = out;
ThisThread::sleep_for(100ms);
}
}
}

Waitについて
MbedはWait APIも提供している。
使用できる関数は
ThisThread::sleep_for(chrono::duration sleep_duration)
wait_us(int us)
wait_ns(unsigned int ns)
使用例
// 1秒待つ
ThisThread::sleep_for(1s);
// 10ミリ秒待つ
ThisThread::sleep_for(10ms);
// 100マイクロ秒待つ
ThisThread::sleep_for(100us);
// 15マイクロ秒待つ
wait_us(15);
// 250ナノ秒待つ
wait_ns(250);
waitを使用してしまうと他のコードの実行をブロックしてしまうので注意する。HighResClock
とif文を組み合わせれば、センサ読み取りなどを続けたままwaitを行うことができる。
HighResClock
HighResClockはマイコンが起動してからの経過時刻を取得できるAPI。
std::chronoのClockと同じはたらきをする。
現在時刻と前回実行時の時刻を比較することで、一定時間ごとに実行するコードを作成できる。
#include <mbed.h>
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
auto now = HighResClock::now();
static auto pre = now;
printf("ここでブロックされない処理を実行する\n");
// 10msごとに実行↓
if(now - pre > 10ms) {
printf("ここで10msごとに実行する\n");
pre = now;
}
}
}

CAN
CAN APIを使ってCAN通信を行う。
CAN Busの初期化
下記コードでCAN1を初期化。通信速度は1MHzに設定する。
#include <mbed.h>
// CAN1を使う場合
CAN can{PA_11, PA_12, (int)1e6};
// CAN2を使う場合はこっち
CAN can{PB_12, PB_13, (int)1e6};
CANMessage構造体の作成
CANMessage構造体によってメッセージの授受を行う。
// 中身が空のメッセージ
CANMessage msg;
// 送信データの作成
uint32_t id = 0x200;
uint8_t data[8] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
// CANMessage構造体の作成
CANMessage msg{id, data, sizeof(data)};
// 送信データの作成(int16_t)
uint32_t id = 0x200;
int16_t data[4] = {4000, 2000, 0, -5000};
// CANMessage構造体の作成
CANMessage msg{id, reinterpret_cast<const uint8_t *>(data), sizeof(data)};
CANメッセージの送信
can.write
関数に送信したいメッセージ(msg
)を渡すことでCANメッセージの送信を行う。
can.write(msg)
while(1) {
// put your main code here, to run repeatedly:
auto now = HighResClock::now();
static auto pre = now;
// 10msごとに実行
if(now - pre > 10ms) {
// 送信データの作成
uint32_t id = 0x200;
uint8_t data[8] = {};
// CANMessage構造体の作成
CANMessage msg{id, data, sizeof(data)};
for(int i = 0; i < 8; ++i) {
printf("%2x", data[i]);
}
// メッセージの送信
if(!can.write(msg)) {
printf("failed to write can msg");
}
printf("\n");
pre = now;
}
}
CANメッセージの受信
can.read(msg)
を使用して送られてきたCANメッセージを読み取る。
can.read
関数はメッセージを受け取ったときだけ、msg
の中身を上書きしてtrue
を返す。
while(1) {
// put your main code here, to run repeatedly:
CANMessage msg;
if(can.read(msg)) {
// IDの確認
printf("%d\n", msg.id);
// データは`msg.data[0]〜[7]`に入っている。
printf("%d\n", msg.data[0]);
}
}
CANメッセージの送受信 全体像
#include <mbed.h>
// CAN1を使う場合
CAN can{PA_11, PA_12, (int)1e6};
// (通信速度を1MHzに設定)
// CAN2を使う場合はこっち
// CAN can{PB_12, PB_13, (int)1e6};
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
auto now = HighResClock::now();
static auto pre = now;
// CANメッセージの受信
CANMessage msg;
if(can.read(msg)) {
// IDの確認
printf("%d\n", msg.id);
// データは`msg.data[0]〜[7]`に入っている。
printf("%d\n", msg.data[0]);
}
// 10msごとに実行
if(now - pre > 10ms) {
// 送信データの作成
uint32_t id = 0x200;
uint8_t data[8] = {};
// CANMessage構造体の作成
CANMessage msg{id, data, sizeof(data)};
// メッセージの送信
can.write(msg);
pre = now;
}
}
}

CAN通信のトラブルシューティング
CANの初期化に失敗というエラーメッセージが出た場合
ピン指定が間違っている
CANトランシーバに電源が供給されていない
CANの初期化には成功するが通信できない場合
can.write
の返り値がfalseになっていると思う。
基本的には回路がおかしいので回路を再チェックする。5Vの給電、終端抵抗など。

DJIモーターの制御
M3508を動かすためにC620とCAN通信を行う。[1][2]
src/C620.hpp
を作成。
src/C620.hpp
#ifndef RCT_C620_HPP
#define RCT_C620_HPP
/// @file
/// @brief Provides the C620 class for controlling the motor driver for M3508.
/// @copyright Copyright (c) 2024 Yoshikawa Teru
/// @license This project is released under the MIT License.
#include <mbed.h>
#include <array>
/// @brief The C620 motor driver class for M3508.
struct C620 {
static constexpr int max = 16384;
void set_current(float current) {
raw_current_ = max / 20.0 * current;
}
void set_raw_current(int16_t raw_current) {
raw_current_ = raw_current;
}
uint16_t get_angle() const {
return rx_.angle;
}
int16_t get_rpm() const {
return rx_.rpm;
}
float get_actual_current() const {
return 20.0 / max * rx_.actual_current;
}
int16_t get_actual_raw_current() const {
return rx_.actual_current;
}
uint8_t get_temp() const {
return rx_.temp;
}
int16_t get_raw_current() const {
return raw_current_;
}
void parse_packet(const uint8_t data[8]) {
rx_.angle = uint16_t(data[0] << 8 | data[1]);
rx_.rpm = int16_t(data[2] << 8 | data[3]);
rx_.ampere = int16_t(data[4] << 8 | data[5]);
rx_.temp = data[6];
}
private:
/// @brief The packet structure of the C620 motor driver.
struct C620Packet {
uint16_t angle;
int16_t rpm;
int16_t actual_current;
uint8_t temp;
} rx_ = {};
int16_t raw_current_ = {};
};
/// @brief The C620 motor driver array for M3508.
struct C620Array {
void parse_packet(const CANMessage& msg) {
if(msg.format == CANStandard && msg.type == CANData && msg.len == 8 && 0x200 <= msg.id && msg.id <= 0x208) {
arr_[msg.id - 0x201u].parse(msg.data);
}
}
auto to_msgs() -> std::array<CANMessage, 2> const {
uint8_t buf[16];
for(int i = 0; i < 8; i++) {
buf[i] = arr_[i].get_raw_current() >> 8;
buf[i + 1] = arr_[i].get_raw_current() & 0xff;
}
return {CANMessage{0x200, buf}, CANMessage{0x1FF, buf + 8}};
}
auto& operator[](int index) {
return arr_[index];
}
auto begin() {
return std::begin(arr_);
}
auto end() {
return std::end(arr_);
}
private:
C620 arr_[8] = {};
};
#endif /// RCT_C620_HPP
src/main.cpp
を作成。
#include <mbed.h>
#include "C620.hpp"
CAN can{PB_12, PB_13, (int)1e6};
DigitalIn button{BUTTON1};
C620Array c620{};
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
auto now = HighResClock::now();
static auto pre = now;
if(CANMessage msg; can.read(msg)) {
c620.parse_packet(msg);
}
// 10msごとに実行↓
if(now - pre > 10ms) {
int16_t out = 0;
if(!button) out = 6000;
printf("% 7d ", out);
for(auto& e: c620) {
printf("% 7d ", c620.get_rpm());
}
for(auto& e: c620) {
e.set_raw_current(out);
}
auto msgs = c620.to_msgs();
if(!can.write(msgs[0]) || !can.write(msgs[1])) {
printf("failed to write c620 msg ");
}
printf("\n");
pre = now;
}
}
}

PID制御
PID制御の詳しい解説はこちらを参照のこと。
PID制御を行う
ここからダウンロードするか、下からコピペしてsrcの中にVelPid.hpp
を作成する。[1]
PID制御クラスの中身
#ifndef VEL_PID_HPP
#define VEL_PID_HPP
/// @file
/// @brief Provides the VelPid class for velocity PID control.
/// @copyright Copyright (c) 2024 Yoshikawa Teru
/// @license This project is released under the MIT License, see [LICENSE](https://github.com/teruyamato0731/Chassis/blob/main/LICENSE).
#include <algorithm>
#include <chrono>
#include <cmath>
/// @brief Gains for a PID controller.
struct PidGain {
float kp;
float ki;
float kd;
};
/// @brief Gains and limits for a PID controller.
struct PidParam {
PidGain gain;
float min = NAN;
float max = NAN;
};
/// @brief Velocity Form PID controller.
template<class T = float>
class VelPid {
public:
/// @brief Constructor with the specified gains and limits.
/// @param param The parameters for the PID controller.
VelPid(const PidParam& param) : param_{param} {}
/// @brief Calculates the output of the PID controller.
T calc(const T target, const T actual, const std::chrono::duration<float>& dt) {
return calc(target - actual, dt);
}
/// @brief Calculates the output of the PID controller.
T calc(const T error, const std::chrono::duration<float>& dt) {
using namespace std;
const auto sec = dt.count();
const auto prop = (error - pre_error_) / sec;
const auto deriv = isnan(pre_prop_) ? 0 : (prop - pre_prop_) / sec;
pre_error_ = error;
pre_prop_ = prop;
low_pass_deriv_ += deriv / 8;
const auto du = param_.gain.kp * prop + param_.gain.ki * error + param_.gain.kd * low_pass_deriv_;
output_ = clamp(output_ + du * sec, param_.min, param_.max);
return output_;
}
/// @brief Resets the PID controller.
void reset() {
pre_error_ = 0.0;
pre_prop_ = NAN;
low_pass_deriv_ = 0.0;
output_ = 0.0;
}
/// @brief Sets the parameters for the PID controller.
void set_param(const PidParam& param) {
param_ = param;
reset();
}
/// @brief Sets the gains for the PID controller.
void set_gain(const PidGain& gain) {
param_.gain = gain;
reset();
}
/// @brief Sets the limits for the PID controller.
void set_limit(const float min, const float max) {
param_.min = min;
param_.max = max;
}
private:
PidParam param_;
T pre_error_ = 0.0;
T pre_prop_ = NAN;
T low_pass_deriv_ = 0.0;
T output_ = 0.0;
};
#endif // VEL_PID_HPP
#include <mbed.h>
#include "C620.hpp"
#include "VelPid.hpp"
CAN can{PB_12, PB_13, (int)1e6};
// Kp = 0.5, Ki = 0.2, Kd = 0.05
// min = -0.7, max = 0.7
VelPid pid{{{0.5, 0.2, 0.05}, -0.7, 0.7}};
C620Array c620;
constexpr float target_rpm = 2000;
int main() {
// put your setup code here, to run once:
printf("setup\n");
while(1) {
// put your main code here, to run repeatedly:
auto now = HighResClock::now();
static auto pre = now;
auto dt = now - pre;
if(CANMessage msg; can.read(msg)) {
c620.parse_packet(msg);
}
// 10msごとに実行↓
if(dt > 10ms) {
auto actual_rpm = c620[0].get_rpm();
const float out = pid.calc(target_rpm, actual_rpm, dt);
c620[0].set_current(out);
printf("output: %d ", c620[0].get_raw_current());
auto msgs = c620.to_msgs();
if(!can.write(msgs[0]) || !can.write(msgs[1])) {
printf("failed to write c620 msg ");
}
printf("\n");
pre = now;
}
}
}

三輪オムニの制御
移動速度
モーター1,2,3の回転方向を、x方向を基準としたy方向への傾き
図より
TODO: 図に表す
初めに
r = hypot(vec.x, vec.y);
phi = atan2(vec.y, vec.x);
さらにこの値を使ってモーターへの出力を計算する
ただしここで
C/C++で計算すると以下のようになる。
float output[3];
for(int i = 0; i < 3; ++i) {
output[i] = r * cos(phi + theta[i]);
}
さらに回転速度
ただしここで
C/C++で計算すると以下のようになる。
float output[3];
for(int i = 0; i < 3; ++i) {
output[i] = r * cos(phi + theta[i]) + k * omega;
}
またロボットのヨー角
float output[3];
for(int i = 0; i < 3; ++i) {
output[i] = r * cos(phi + yaw + theta[i]) + k * omega;
}
なお以下定数を得ることで比例定数を決定できる。
四輪オムニの制御
同様にして速度ベクトル
図より
TODO: 図に表す
float output[4];
for(int i = 0; i < 4; ++i) {
output[i] = r * cos(phi + yaw + theta[i]) + k * omega;
}

三角関数を使用しない方法
オフセット角を与えないならば、
三輪オムニ
これを行列で表現すると
四輪オムニ
これを行列で表現すると

モーター出力の決定
モーターの電圧制御・電流制御、どちらの場合でも入力が角速度に比例すると考えて実用上そこまで問題はない。
速度の精度が必要な場合はモーターの角速度に対してPID制御を行えばよい。

オムニのPID制御(カスケード制御)
目標速度ベクトル
実際の速度
目標角速度
実際の角速度
auto vec = pid1.calc(vec_ref, velocity, dt);
float r = hypot(vec.x, vec.y);
float phi = atan2(vec.y, vec.x);
float omega_ref[3];
for(int i = 0; i < 3; ++i) {
omega_ref[i] = r * cos(phi + yaw + theta[i]) + k * omega;
}
float output[3];
for(int i = 0; i < 3; ++i) {
output[i] = pid2[i].calc(omega_ref[i], omega_actual[i], dt);
}

ロータリーエンコーダの使い方
うちのロボ部でよく使用するAMT102について解説する。
AMT102の解説
AMT102はインクリメンタル式のロータリーエンコーダである。ロータリーエンコーダは回転する軸に取り付けて、その軸の回転数や回転速度を計測するものである。略称:エンコーダ・ロリコン。エンコーダにはアブソルート式とインクリメンタル式があるが、AMT102は安くて簡単に使えるインクリメンタル式である。
一般的なインクリメンタル式のエンコーダにはA層・B層の2枚の金属板が入っており、その2枚が位相を90°ずらして配置されている。[1]軸が回転するごとにA・Bの金属板が回転し、金属板に入ったスリットを光が通り抜けることで、フォトトランジスタからパルスが発生する。A層・B層それぞれのパルスから現在の位相を読み取ることで、前回読み取った際の位相からどれだけ移動したかを読み取ることができる。[2]
エンコーダには分解能(resolution)というものが存在する。分解能とはセンサで取得できる情報の細かさのことである。エンコーダの分解能は取得できる角変位の細かさであり、軸を1回転させた場合に出力されるパルス数のことである。[3]A層・B層それぞれに立ち上がりと立ち下がりがあるので、分解能の4倍の精度で角度を取得できる。[4]ただし分解能を上げると角速度があがったときにパルスの読み飛ばしが発生するため、分解能は軸の角速度に応じて設定する必要がある。AMT102の場合は裏のスイッチを切り替えることで分解能を変更できる。[5]
Z, X, あるいはIndexと書かれたピンはエンコーダが1回転したときに1度だけパルスを発生させるピンである。インクリメンタル式のエンコーダには積算誤差が発生するが、インデックスを使用することで積算誤差を打ち消すことができる。
AMT102にはA層、B層、X(インデックス)、GND、5Vのピンがある。それぞれをよしなにマイコンに配線する。
エンコーダの読み取り
いくつかの手法があるのでそれぞれの方法と利点を解説する。
手法0. 回転方向が不要で速度のみを計測する
A層またはB層あるいはその両方のピンにピン割り込みを設定する。
割り込みのたびにカウンタをインクリメントする。
コード例
#include <mbed.h>
InterruptIn a_phase{D4};
InterruptIn b_phase{D5};
volatile int32_t omega = 0;
int64_t pos = 0;
void increment() {
omega = omega + 1;
}
int main() {
a_phase.rise(increment);
a_phase.fall(increment);
b_phase.rise(increment);
b_phase.fall(increment);
while(1) {
auto tmp = omega;
omega = 0;
pos + = tmp;
printf("pos:%d vel:%d\n", pos, tmp);
ThisThread::sleep_for(1s);
}
}
手法1. mainループでreadする
コード例
#include <mbed.h>
uint8_t status = 0;
int64_t count = 0;
int get_increment(uint8_t status) {
switch(status) {
case 1: case 7: case 8: case 14:
return 1;
case 2: case 4: case 11: case 13:
return -1;
default:
return 0;
}
}
int main() {
while(1) {
status = a_phase << 3 | b_phase << 2 | status >> 2;
count += get_increment(status);
printf("pos:%d", pos);
}
}
手法2. 割り込みでインクリメントする
手法0や手法1と同様の手法でも読み取れるが、より効率の良い手法を示す。
ただし以下例ではA層のみがピン割り込みの設定である。B層にもピン割り込みを設定する場合は、同様にコールバック関数を作って渡せばよい。
FallとRiseを区別する手法
volatile int64_t counter = 0;
void a_rise() {
if(b_phase) {
counter += 1;
} else {
counter -= 1;
}
}
void a_fall() {
if(b_phase) {
counter -= 1;
} else {
counter += 1;
}
}
int main() {
a_phase.rise(a_rise);
a_phase.fall(a_fall);
while(1) {
printf("%d\n", counter);
}
}
補足:Lookup テーブルについて
Lookupテーブルを作成すればif文を細かく書かずに、ピンの状態を配列の添字に与えるだけで出力が得られる。
手法3. マイコン側の機能で読み取る
STMマイコン等にはエンコーダ読み取りの機能があるため、Cube等を使用すればマイコン側でエンコーダを読み取ることができる。

足回りのオドメトリ
オドメトリとは自己位置推定の手法の1つで、車輪の回転角度からそれぞれの移動量を求め、その和を積算することによりロボットの位置を推定する手法の総称である。
オドメトリはタイヤが滑らないことを仮定するが、タイヤは滑るものなので移動するごとに少しずつ誤差が蓄積する。よってオドメトリには誤差が含まれることを前提として、制御する必要がある。
誤差への対応は後述する。
はじめにエンコーダ3つの場合を解説し、エンコーダ4つの場合についても式を示す。
制御周期ごとに、エンコーダによって各車輪の移動量
ロボット中心の座標を
なお後に示す単位変換により、移動量の単位は吸収されるため、あまり考慮する必要はない(すなわち、パルス数をそのまま用いてよい)。
以上をモデル図に示す。
オドメトリのモデル図
車輪角度のみに注目した図も示す。
エンコーダ3つのオドメトリ
各車輪の初期角度に、現在の姿勢角
各車輪の移動量を並進成分と回転成分に分解する。
エンコーダ4つのオドメトリ
エンコーダ4つの場合も同様に、移動量の和を積算することで推定できる。
数式の一般化
エンコーダ数を
単位変換について
移動量
プログラムを書く
C++によるコード例を示す。
#include <array>
#include <cmath>
/// エンコーダ数
static constexpr int N = 3;
/// @brief エンコーダ変位の変化量より、自己位置の変化量を計算する。
/// @param enc エンコーダ変位の変化量
/// @param offset_rad 姿勢角[rad]
/// @return 移動量[dx, dy, dθ]
std::array<float, N> odometry(const std::array<float, N>& enc, float offset_rad) {
constexpr float pi_n = 2 * M_PI / N;
std::array<float, N> dx = {};
for (int i = 0; i < N; ++i) {
dx[0] += enc[i] * std::sin(offset_rad + i * pi_n);
dx[1] += enc[i] * std::cos(offset_rad + i * pi_n);
dx[2] += enc[i];
}
return dx;
}
// [x, y, θ] を0で初期化
float x_est[3] = {};
auto enc = /* 値の取得 */;
auto dx = odometry(enc, x_est[2]);
for (int i = 0; i < 3; ++i) {
x_est[i] += dx[i];
}
積算誤差について
移動距離が伸びるほど、積算誤差も大きくなっていく。一定の周期で、壁にリミットスイッチを当てることにより、推定値の誤差をリセットする。