はじめに
どうも、初めまして、こんにちは、こんばんわ。筑波大学のつくばろぼっとサークルで回路と独ステとか作ってるKenpi(けんぴー)と申します。
弊サークルはコロナの流行った年を境に技術継承が途絶えてしまいました。またコロナ後の先輩方の技術継承も乏しく、技術を後世に残すために誰でも見れる記事として書きました。ぜひ参考にしてみてね。私自身まだまだ未熟な身ですので暖かい目で見てもらえると幸いです。
さて、本題に入りますがロボコンで使う 4ch DCモータードライバ基板(4chMD) を設計・実装しました。今回作成したMD基板は主に385モーターや吸着で使うような真空ポンプを駆動することを目的としています。そこまで高性能なMDでなくて良いので、IR2302などを使って外部Hブリッジを作らずに一つの素子でモーターを動かすような構成としました。とりあえず手軽に使えるMDが欲しかったというのもあります。
この記事では、回路の考え方、基板設計で気をつけたこと、そしてブラウザだけで動作確認できるテスト環境(Web Serial)を作った話をまとめます。
最後に、実装後に見つかった「リセットスイッチを付けたらマイコンが一切起動しない」というハマりどころも書いています。マジで回路設計は深夜にしないほうがいい。同じ失敗をする人が減ればうれしいです。

この記事でわかること
- BD64950 を 4 個使った 4ch DCモータードライバの回路構成
- フォトカプラでロジック電源とモーター電源を絶縁するときの注意点(PWM周波数の上限)
- レジスタ直書きの STM32G431 で PWM(スローディケイ)と CAN を動かす方法
- Web Serial API で「ドライバ不要・インストール不要」のテストGUIを作る方法
- タクトスイッチのフットプリントでハマった話
仕様
| 項目 | 内容 |
|---|---|
| サイズ | 80 × 50 mm、2層基板、M3穴 4隅 |
| マイコン | STM32G431KBT6(Cortex-M4, 170MHz max / 今回は HSI 16MHz で使用) |
| モータードライバ | ROHM BD64950EFJ × 4(1ch あたり 1~3A 程度で使用) |
| モーター電源 | 12〜24V(XT30)、ヒューズ付き |
| ロジック電源 | 5V(RJ45 または USB-C)→ 3.3V LDO |
| 絶縁 | ロジック ↔ モーター間をフォトカプラ TLP293-4 で絶縁 |
| 通信 | CAN(MCP2562FD、RJ45 でデイジーチェーン)、UART(CH340E → USB-C) |
| 書き込み | ST-LINK(SWD)または USB-C 経由の UART ブートローダ |
| 出力 | ターミナルブロック × 4 |
回路設計
全体構成

ロジック側とモーター側の GND は完全に分離しています。両者をまたぐ信号はフォトカプラの 8 本だけです。
モーターのノイズや GND の揺れがマイコンや CAN に回り込まないようにするのが目的です。
(また、記事を書くつもりですが、BLDC FOC MD基板はロジック電源を使わずアクチュエータ電源を降圧してロジック電位を取り出すようにしています。その際はCANは絶縁のものを使用しています。)
モータードライバROHM BD64950(秋月電子で120円)
BD64950 は IN1/IN2 の 2 本で制御するシンプルな H ブリッジです。
秋月電子にて120円で買える最強コスパドライバだと思っています。ROHMには頭が上がらないです。
| IN1 | IN2 | 動作 |
|---|---|---|
| L | L | フリー(出力オープン) |
| H | L | 正転 |
| L | H | 逆転 |
| H | H | ブレーキ |
PWM はスローディケイで駆動しています。正転なら IN1 を H 固定にして IN2 に PWM を入れます。
IN2 が L の間は「正転」、H の間は「ブレーキ」になるので、オフ期間に電流がゆっくり減衰し、低速でもトルクが出やすくなります。
今回は電流計測をしないので、RNF(電流検出抵抗の端子)はそのまま GND に落としました。
VREF は開放すると動作が不定になるので、10k/10k で 2.5V に固定しています。
フォトカプラ絶縁と PWM 周波数の上限
フォトカプラは TLP293-4(4回路入り)を 2 個使っています。出力側はエミッタフォロワです。

これで MCU の H がそのまま H として伝わります(非反転)。
ただし、TLP293 はフォトトランジスタ出力なので遅いです。立ち上がり・立ち下がりに数十µs かかります。
PWM を速くすると実効デューティがずれるので、PWM は 1kHz 以下で使います。
また、極端に短いパルスは潰れてしまうので、ファーム側で「30µs 未満のパルスはブレーキ扱い」にしています。
⚠️ 高速な PWM が必要なら、フォトカプラは高速タイプ(デジタルアイソレータなど)を選びましょう。
信号反転式の高速フォトカプラも使ったりしてますが、少し価格も高いのでTLP293-4に落ち着いてるという現状()。TLP293-4は個人的にも使い慣れているのもあります。
電源
- ロジック 5V は RJ45(ロジック用の電源基板からLANケーブルで給電)か USB-C から。スライドスイッチで RJ45 側を切れるようにしています。デバッグをしやすい構成にしています。
- 3.3V は NJM2391(TO-252, 1A)を使用し、5Vから降圧。
- フォトカプラの出力側に使う 5V は、モーター電源の 24V から小さな LDO で作っています。(MCP1792)
こうすることで、ロジック側の 5V とは完全に別系統になります。もし、サージや突入電流が入っても、ロジック側は守られるので、ロボットの暴走や、ロジックへの影響がないようにしています。弊サークルのアクチュエータ電源基板で、CANからアクチュエータ電源をリレーによって切れるようにしてあるので上流側でしっかり監視しておけば異常時に自動停止もできます。
CAN
CANトランシーバは MCP2562FD です。RJ45の2連タイプのもので、LAN ケーブルで基板を数珠つなぎにできるようにしました。まぁ..PoEとかではないので規格違反な気が(以下略。
LANケーブルが手に入りやすく、様々な長さのものが売っているのも採用理由です。5VやGNDはLANケーブル内の2本の線を一本として使っています。LANケーブルは内部に線が8本あるので、CANと5V、GNDを使っても2本余ります。現在はRS485で作動信号を通すかどうか検討しています。
終端抵抗 120Ω はスライドスイッチで ON/OFF できるので、バスの端の基板だけ ON にします。
MCP2562FDを使ってるのはFD-CANに対応させつつ、秋月電子で取り扱いがあるためです。10/20現在入荷が未定になっており、アセアセしてるのは内緒です///
基本的に弊サークルは秋月電子や、千石電商を利用しています。TX(つくばエクスプレス)で秋葉原に行きやすいのもその理由です。
基板設計で気をつけたこと
KiCad 9 で設計しました。2層基板で気をつけたのは次のあたりです。
- ベタ GND を 2 つに分ける:ロジック GND とモーター側の 24- をそれぞれ両面ベタにし、境界をフォトカプラがまたぐ配置にしました。
- 大電流ラインは太く:24V とモーター出力は 1.0mm 以上、クリアランス 0.3mm のネットクラスを作りました。
- USB-C の下にビアを打たない:差動ペアの下の GND を途切れさせないためです。
- GND ビアは配線に沿って等間隔に:表裏の GND をしっかりつなぎます。
- パスコンは IC のピンの直近に:特にドライバの VCC。
- コネクタは基板端から少しはみ出させる:XT30・USB-C などは抜き差ししやすさを優先しました。(普通にスペースが足りなかったとは言えない)
- 裏面の実装素子を無くす:実装しやすいように基板の裏面には素子を実装しないようにしました。そのおかげで裏面にヒートシンクをつけられるので嬉しい誤算です。
テストファーム(レジスタ直書き)
HAL を使わず、レジスタを直接叩く小さなファームを書きました(約7KB)。
UART で 1 行のコマンドを受け取り、1 行の JSON で状態を返します。
PWM(スローディケイ)
タイマは PWM モード1 です。CCR = ARR+1 で常時 H、CCR = 0 で常時 L になることを使っています。
/* Slow decay: forward = IN1 H / IN2 PWM, reverse = IN2 H / IN1 PWM (IN low part = drive). */
static void motor_set(motor_t *m, int32_t duty) /* duty: -1000..1000 */
{
uint32_t mag = (uint32_t)(duty < 0 ? -duty : duty);
uint32_t on = (mag * period + DUTY_FS / 2) / DUTY_FS;
uint32_t minp = min_pulse_us * (TICK_HZ / 1000000U);
if (mag == 0U || on < minp) { set_ccr(m, period, period); return; } /* 短すぎるパルスはブレーキ */
if (period - on < minp) on = period;
uint32_t off = period - on;
if (duty > 0) set_ccr(m, period, off); /* IN1 = H, IN2 = PWM */
else set_ccr(m, off, period); /* IN2 = H, IN1 = PWM */
}
2 つの CCR を同じ周期で切り替えるため、書き換え中は UDIS で更新イベントを止めています。
static void set_ccr(motor_t *m, uint32_t in1, uint32_t in2)
{
m->tim->CR1 |= TIM_CR1_UDIS; /* 2ch が同じ周期で切り替わるように */
*m->ccr_in1 = in1;
*m->ccr_in2 = in2;
m->tim->CR1 &= ~TIM_CR1_UDIS;
}
CAN(FDCAN1 をクラシック CAN で)
STM32G4 の FDCAN は、メッセージ RAM の配置が固定です(SRAMCAN_BASE から、Rx FIFO0 = +0x0B0、Tx バッファ = +0x278、1要素 18 word)。
ここを知らないと送受信できずに悩みます。
ビットタイミングは 16MHz / プリスケーラ / 16tq にして、プリスケーラだけで 125k〜1Mbps を切り替えています。
/* 16 tq per bit: sync 1 + seg1 13 + seg2 2 -> sample point 87.5 % */
FDCAN1->NBTP = ((2U - 1U) << FDCAN_NBTP_NSJW_Pos) | ((brp - 1U) << FDCAN_NBTP_NBRP_Pos)
| ((13U - 1U) << FDCAN_NBTP_NTSEG1_Pos) | ((2U - 1U) << FDCAN_NBTP_NTSEG2_Pos);
内部ループバック(CCCR.TEST/MON + TEST.LBCK)を使えば、相手の基板がなくても送受信の自己テストができます。
また、相手がいないと ACK が返らず送信が永遠にリトライされるので、テスト用ファームでは DAR(自動再送なし)にしています。
安全機能
- 通信タイムアウト:500ms コマンドが来なければ、全出力をフリーにします。
- 非常停止:
Xで全出力フリー+ラッチします。Rで解除するまで駆動コマンドを受け付けません。
コマンド一覧
| コマンド | 動作 |
|---|---|
M<ch> <-1000..1000> | デューティ(‰)。ch=0 で全ch |
B<ch> / C<ch> | ブレーキ / フリー |
X / R | 非常停止 / 解除 |
F <Hz> | PWM 周波数 |
CB <kbps> / CT <id> <data> | CAN ビットレート / 送信 |
? | 状態を JSON で返す |
ブラウザで動くテスト GUI(Web Serial API)
テストのたびにシリアルターミナルでコマンドを打つのは面倒です。そこで、HTML 1 ファイルで完結するテスト GUI を作りました。最近はプログラミングが苦手な私(回路班)でもAIのおかげでデバッグができて嬉しい限りです。今回はClaudeでGUIを作成してもらいました。
Chrome / Edge の Web Serial API を使うと、ブラウザから直接シリアルポートを開けます。
アプリのインストールも、サーバーも不要で、ファイルをダブルクリックするだけで動きます。

接続部分
const port = await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x1a86 }] }); // CH340
await port.open({ baudRate: 115200 });
const writer = port.writable.getWriter();
const reader = port.readable.getReader();
// 1行ずつ JSON として読む
const dec = new TextDecoder();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
if (line) onLine(JSON.parse(line));
}
}
filters に CH340 のベンダー ID(0x1A86)を入れておくと、ポート選択画面に基板だけが出てくるので迷いません。
できること
- 4ch それぞれのスライダー、メーター、回転アニメーション、デューティの履歴グラフ
- 非常停止(Space / Esc キー)
- 基板図の上で、動いている出力が光る
- 3.3V 電源電圧と MCU 温度のテレメトリ(MCU 内部の VREFINT と温度センサで測定)
- CAN の送受信、ループバック
- 自動テスト:通信 → 3.3V → LED → CAN → 各出力の正転/逆転 → タイムアウト → 非常停止を順番に確認し、レポートを保存
- デモモード:基板がなくても、JS で書いた疑似基板を相手に全機能を試せる
GUI は 10Hz で状態を問い合わせていて、これが基板側のタイムアウトの「生存確認」も兼ねています。
タブを閉じたり USB が抜けたりすると、500ms 後に基板側でモーターが止まります。
💡 Web Serial は
file://で開いた HTML からも使えます(Chrome / Edge)。
ハマったところ:リセットスイッチを付けたらマイコンが起動しない
実装後、USB-C 経由の UART ブートローダで書き込もうとしたら、STM32 から一切応答がありませんでした。
- PC から送ったデータで CH340 の LED は点滅する → PC→基板は届いている
- 3.3V も出ている
- でも NRST が 3.3V になっていない
原因は、タクトスイッチのフットプリントでした。
4 本足のタクトスイッチは、中で 2 本ずつ常時つながっています。
データシートの回路図を見ると、つながっているのは「基板レイアウト上で左右に向かい合う足」でした。
ところが使用したフットプリントでは、左の 2 パッドを GND、右の 2 パッドを NRST にしていたため、
常時つながっている足が GND と NRST をショートさせていました。つまりマイコンはずっとリセット状態です。
回路を設計している時は寝不足でしたので全く気づきませんでした。マジで、回路設計は意識がしっかりしている時にするべき(そらそう)。DRCも仕事しろ!(他責)
最近はリセットをジャンパー線にしたり、自動リセット回路を作ったりしてます。
データシートの回路図 自作フットプリント(誤り)
A ─┬─ ○ ○ ─┬─ C GND ■ ■ NRST
│ (SW) │ ↖ 常時導通 ↗
B ─┘ └─ D GND ■ ■ NRST
(A-B, C-D が常時導通)
応急処置としてスイッチを外したら、NRST が内部プルアップで 3.3V になり、無事に書き込めました。
完成した基板では、テスターでスイッチの未押下時に導通がないか確認すると安心です。
ついでに、UART ブートローダ(BOOT0 = H でリセット → 0x7F を送ると 0x79 が返る)は、
PC からの最初の 1 バイトで通信速度を合わせる仕組みです。
うまくいかないときは「BOOT0 を H にしてからリセット → すぐに書き込みツールを実行」の順を守りましょう。
まとめ
- BD64950 ×4 + STM32G431 で、絶縁付きの 4ch DCモータードライバを作りました。
- フォトカプラ絶縁は安心ですが、PWM 周波数の上限に注意が必要です。
- Web Serial を使うと、HTML 1 ファイルでリッチなテスト GUI が作れます。部員の誰でもブラウザを開くだけでテストできるのが便利です。
- 回路設計は、データシートの内部接続図まで確認しましょう。あと、ちゃんと寝ること。

コメントコーナー