背景
AliExpressの怪しいマイコン(STM32F103)届いた
— 茶音 麦粉@育児休業中 (@lscyane) November 5, 2022
1個あたり180円 ✕ 10個
果たして使えるのか… pic.twitter.com/CSeQqpvi5X
AliExpressの怪しいマイコンを購入

2022/10/24に購入し、11/5に到着
こちら商品を購入
※色選択操作でマイコンが異なります。
「STM32F103C6T6」を選択
円安の時期を考慮すると割高ですがそれでも2022/10/24時点で1個188円、送料とのバランスを考えて10個注文しました。
価格については、このIC自体は生産終了品みたいなのでそれがどこぞから流れてきたのかもしれません、知らんけど。
CubeIDEプロジェクトの立ち上げ
CubeIDEのバージョン:1.10.1
(Pleiadesによる日本語化をしています)
- ファイル -> 新規 -> STM32 Project
- MCU/MPU Selector タブ
Commercial Part Nuber ドロップダウンボックスから「STM32F103C6T6A」を選択
- 右ペインの「MCUs/MPUs List」
「STM32F103C6T6A」を選択して「次へ」<
- プロジェクトのセットアップウインドウが出てくるので
プロジェクト名を設定 (ここでは「GamePad_F103」とする)
Targeted Languageはお好みで(C++は使わないのでここでは「C」を選択)
Targeted Binary Typeは「実行可能」
Targeted Project Typeは「STM32Cube」
あとは既定で良いので「完了」をボタンを押下
初期設定
- 続いてクロックソースの設定
- USBの設定
- 続けてUSBミドルウェアの設定
- RCCでHSEを設定することにより、Clock Configration で外部クロックを選択できるようになるので、クロックを設定していきます。
特に省電力を求めるとかでなければ絞る必要は無いので最大速度にします。
USBを設定した時点でクロック設定がおかしい(初期値ではUSB供給クロックが正しくない)ので Clock Configration を開いた時点で、自動で補正するかとダイアログが出てきますがどとみち設定するのでここでは「No」を選択しておきます。
(Yesを選択してしまっても大丈夫です) - メインループ調整用にタイマーも設定しておきます。
適当に待ちを入れても良いのですが、時間調整が難しくなるのでタイマーを使います。 - あとはPC13にLEDがオンボードされてますのでGPIO出力にしておくのと、
動作テスト用の入力にPA0をGPIO入力にしておきます。 - プルアップ/ダウンはハードによりお好みですが、ここでは簡易にGNDに落とすだけでONとなるように内蔵プルアップを設定しています。
- ここまで設定したら一旦保存してコード自動生成を走らせます。
ハードの変更
と、ここでコーディングに入る前に、高い確率でハードの変更が必要です。
R10のチップ抵抗に 103 と書かれています。
R10はUSBのD+ピンで、USBデバイス側はFull-speed時にはD+ピンに1.5kΩでプルアップする必要があるのに10kΩでプルアップされています。
この状態で認識してくれるかどうかはホスト次第ですが私の環境ではUSBとして認識してくれなかった(デバイスマネージャも反応なしだった)ので素直に対策します。
R10を取り外して1.5kΩに差し替えるのも良いですが、如何せん難易度が高いと思われますので簡易的にはPA12を1.8kΩで3.3vにプルアップすれば抵抗の並列接続となり、1.525…kと大体1.5kΩになります。
私は1.8kΩは手元に持ち合わせていなかったので近い2.2kΩでプルアップしましたが認識してくれましたので、持ち合わせが無くとりあえずお試しであればそれっぽい値で試してみるのも良いかと思われます。
(2.2kΩを並列接続した場合は約1.8kΩ)
A12から2.2kΩを3.3vに接続
マウスデバイスを作って動作確認
まずは記述量の少ないマウスデバイスでUSBデバイスとして動作するかテストしてみます
プロジェクトエクスプローラーから
GamePad_F103(上記で作成したプロジェクト名) -> Core -> Src -> main.c を開く。
USBのAPIを呼ぶために、USER CODE BEGIN Includes に以下をインクルード
/* USER CODE BEGIN Includes */
#include "usbd_def.h"
#include "usbd_hid.h"
/* USER CODE END Includes */
ループ制御用の変数の宣言と、
USBデバイスのハンドルをexternしておきます。
externはお作法的にはあまりよろしくはありませんが、CubeMXが吐き出すコードを有効に使うにはやむを得ませんので責任をもって管理します。
/* USER CODE BEGIN PV */
static volatile uint8_t interrupt;
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE END PV */
割り込みのコールバックを記述します。
割り込みが発生したら interrupt 変数を 1 にします。
メインループ内で interrupt 変数が 1 になったことを検知したら0に戻してループが進むようにして一定時間毎にループするように制御します。
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3) {
interrupt = 1;
}
}
/* USER CODE END 0 */
メイン関数を記述していきます。
4行目以降はマウス用の構造体・変数宣言なので後で消します。
/* USER CODE BEGIN 1 */
interrupt = 0; // ループ制御用
struct mouseHID_t {
uint8_t buttons;
int8_t x;
int8_t y;
int8_t wheel;
};
struct mouseHID_t mouseHID;
mouseHID.buttons = 0;
mouseHID.x = 0;
mouseHID.y = 0;
mouseHID.wheel = 0;
/* USER CODE END 1 */
ペリフェラル等の初期化が終わった後に割込モードでタイマーをスタートさせます
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */
メインループの処理を記述していきます
PA0がGNDに接続(Low)になったときに、マウスポインタを右に5動かすようにしています。
/* USER CODE BEGIN WHILE */
while (1)
{
while(interrupt == 0); // 割り込みで1になるまで待つ
interrupt = 0;
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == 0) {
mouseHID.x = 5;
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, 1);
} else {
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, 0);
}
USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&mouseHID, sizeof(struct mouseHID_t));
mouseHID.x = 0;
/* USER CODE END WHILE */
書き込み&実行
ST-Linkを使ってSWDで書き込みます。
ST-Link V3はマイコンボードのデバッグピンと順番が違うので注意。
マイコンボード側は下図上から GND/CLK/DIO/VCC です。

ST-Link側のVCCはデバイスの電源検知用で、ST-Linkからは給電されませんので、ST-Link、マイコンボードの両方にmicro-USBを接続してあげてください。
ビルド前にビルド設定を「Release」にしておきます。
CubeIDEのメニューバーから
プロジェクト(P) -> ビルド構成 -> アクティブにする -> Release を選択
STM32F103C6T6はROMが32kBしかありませんので、USBミドルウェアを抱え込んだ状態でDebugビルドするとそれだけで32kB近く使うのでちょっとしたコード追加でたちまちビルドできなくなってしまいます。
どうしてもデバッグしたい場合は最適化をかけてROM使用量を減らしてください。
但し最適化をかけると意図した所でブレーク出来なかったり、コード通りにステップ実行出来なくなるので注意。
デバッグ実行が出来ないのであれば、後は空いているシリアル通信やGPIOを使った実働デバッグしか無いのでうまいこと活用しましょう。
メニューバーから
実行(R) -> 実行(S) -> STM32 C/C++ Application を選択
または、ツールバーの実行を選択します

初回実行時は、起動構成のプロパティ編集ウインドウが出てきます。
ビルド設定を「Release」に変更した際には、操作によっては書き込みのターゲットがDebugのままになっていたりするので、書き込み対象ファームが意図したものになっているか確認しておくと良いと思います

デバッガ設定はST-Linkを使っていれば今回の構成では特に変更する必要は無いはずですが、よくあるミスとして「デバッグプローブ」と「インターフェース」の設定が意図したものになっているかは確認しておくと良いと思います。
別の接続方法の場合はその環境に合った設定をしてください。

OKボタンを押下すると、ビルドが行われ、エラーがなければそのまま書き込まれます。
環境によってはマイコンボードのUSBを一旦外して再接続しないとうまく動かないかもしれません。
うまくいけばPC13(緑LED)が点灯し、PA0をGNDに繋げると緑LEDが消灯し、マウスポインタが右に動きだします。
ジョイパッドを作る
ハードに問題が無いことが確認できたので、ジョイパッドを作る作業に入ります。
- プロジェクトエクスプローラーから「GamePad_F103.ioc」を開き、
コードも色々変更が必要になります。
HIDからカスタムHIDに変わったのでインクルードも変更します。
/* USER CODE BEGIN Includes */
#include "usbd_def.h"
#include "usbd_customhid.h" // ← 変更
/* USER CODE END Includes */
メイン関数の宣言部も、確認用だったマウスの処理を削除してジョイパッド用に必要な変数を用意します。
多分これが一番楽だと思います。
/* USER CODE BEGIN 1 */
interrupt = 0; // ループ制御用
uint8_t old_input = 0; // 入力テスト用
uint8_t new_input = 0; // 入力テスト用
uint8_t buttons[2]; // USBに通知するジョイパッドデータ
/* USER CODE END 1 */
メインループは以下のようにして、入力の度に次のボタンがONされるようにして全ボタンテストします。
/* USER CODE BEGIN WHILE */
while (1)
{
// ループ制御
while(interrupt == 0);
interrupt = 0;
// 入力
new_input = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, new_input);
// OFF→ONに変わったときだけ処理
if ((old_input != 0)
&& (new_input == 0)
) {
// ジョイパッド入力を1つずらす
if ((buttons[0] == 0) && (buttons[1] == 0)) {
buttons[0] = 1;
} else if (buttons[0] != 0) {
buttons[0] <<= 1;
if (buttons[0] == 0) {
buttons[1] = 1;
}
} else {
buttons[1] <<= 1;
if (buttons[1] >= 4) {
buttons[1] = 0;
}
}
}
old_input = new_input;
// USBに通知
USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *)buttons, sizeof(buttons));
/* USER CODE END WHILE */
最後にレポートディスクリプタを記述します。
プロジェクトエクスプローラーから、
USB_DEVICE -> App -> usbd_custom_hid_if.c
を開くと、Usb HID report descriptor. を記述する箇所があります。
/* USER CODE BEGIN 0 */
0x05, 0x01, // 05:USAGE_PAGE 01:Generic Desktop
0x09, 0x05, // 09:USAGE 05:Game Pad
0xA1, 0x01, // a1:COLLECTION 01:Application
0x05, 0x09, // 05:USAGE_PAGE 09:Button
0x19, 0x01, // 19:USAGE_MINIMUM 01:Button1
0x29, 0x08, // 29:USAGE_MAXIMUM 08:Button8
0x15, 0x00, // 15:LOGICAL_MINIMUM 00:0
0x25, 0x01, // 25:LOGICAL_MAXMUM 01:1
0x95, 0x08, // 95:REPORT_COUNT 08:8
0x75, 0x01, // 75:REPORT_SIZE 01:1bit
0x81, 0x02, // 81:INPUT 02:Data,Var,Abs
0x05, 0x09, // 05:USAGE_PAGE 09:Button
0x19, 0x09, // 19:USAGE_MINIMUM 09:Button9
0x29, 0x0a, // 29:USAGE_MAXIMUM 0a:Button10
0x15, 0x00, // 15:LOGICAL_MINIMUM 00:0
0x25, 0x01, // 25:LOGICAL_MAXMUM 01:1
0x95, 0x08, // 95:REPORT_COUNT 08:2
0x75, 0x01, // 75:REPORT_SIZE 01:1bit
0x81, 0x02, // 81:INPUT 02:Data,Var,Abs
/* USER CODE END 0 */
USER_CODEの外にある END_COLLECTION を含めて 39 byte です。
ここのサイズを変える場合は、CubeMXの USBD_CUSTOM_HID_REPORT_DESC_SIZE もサイズに合わせて変える必要があります。
個人的に躓いたポイントとして、ボタン10個はまとめて設定することが出来ず、8ボタン毎に分ける必要があるようです。
LOGICAL_MINIMUM/MACIMUMを01~0a、REPORT_COUNT を 0a にしてまとめて設定しようとしていましたがこれは駄目なようです。
(デバイスマネージャで「レポートがバイト配列ではありませんでした。」と怒られました)
結果
チャタリング対策をしていないので、入力の当て方によってはスキップされたかのように見えることもありますが、
入力の度にボタン1~10が入力されている状態を確認できると思います

あとは用途に応じてポート設定とメインループをいじるだけです。
本記事では10ボタンのUSBジョイパッドの作例ですが、軸入力やアナログ値などもレポートディスクリプタの設定と、送信データの部分を変更すれば追加できます。
そのあたりは私が今必要としていないので今回は省略します。
参考文献
本記事制作にあたり、以下の文献を参考にさせていただきました。
