sophiaの書

なんか書く

指先カイロ

(2020.7.27追記:世の中には塗るタイプのカイロや温感クリームというものがあるようです。指先カイロを考えたときは調査不足だったので知りませんでした。私が欲しかったのは前者であり、指先カイロではない気がします。)

寒い環境の中でも私は本のページをめくったりスマホを操作したりするのですが、指先が冷たすぎると指を思うように動かせません。細かい作業をするときは当然手袋をはずさないといけませんが、手袋をはずすとますます指先が冷たくなってしまいます。寒くても素手で指先を使った軽作業をしやすくできないかと考えて「指先カイロ」を作りました。

指先カイロ装着図

指先カイロ装着図

指を温める仕組み

  • 体の温かい部分から直接熱をもらう方法

息を手に吹きかけたり首などの温かい部分を触ったりして手を温めます。普段私はこの方法を一番よく使います。指を温めるためには一度作業を中断しなければなりません。

  • 血行をよくする方法

グーパーして指の血行をよくして温めます。この方法は私の指には無効でした。

  • 手首を温める方法

手首を温めることで指先までも温めようとします。この方法は私の指には無効でした。

  • あらかじめ温めて置いた物体から熱を伝える方法

温かい飲み物が入った入れ物を触るなどして指を温めます。もし小型軽量で熱容量が大きい物質があればかなりよい方法です。なぜなら使用後にその物体が冷たくなっても体温で温め直せば再び使えるからです。今回はそのような物質があるかどうかの調査は行いませんでした。

  • 摩擦熱による方法

手のひらをこすりあわせ、摩擦熱で手を温めます。指を温めるためには一度作業を中断しなければなりません。

  • ジュール熱による方法

火傷を防ぐために温度を制御する必要があり、電源や配線をどうするかという問題があります。さらに、複数回使用するなら洗濯をどうするかという問題があります。

  • 反応熱による方法

火傷を防ぐために高温になりすぎないように反応速度を調節する必要があります。そして環境にやさしい必要があります。

先行事例

  • 指のない手袋

作業はしやすいですが指先は冷たくなります。

  • 手首を温める方法

私の指は手首を温めたとしても冷たいままでした。

  • 電気で手袋を温める方法

手袋をしていたら作業が困難です。

  • 手袋の中に温かいものを挿入する方法

手袋をしていたら作業が困難です。

今回提案するカイロの特徴

私の指で試した結果、指に直接熱を供給しないかぎりは指が温まりませんでした。そして温めた部分だけ局所的に温かくなって、そうでない部分は冷たいままでした。温めるのを止めるとすぐに冷たくなりました。したがって熱源を直接指に取り付けて温め続けなければならないと分かりました。この実験結果と寒い中で指先を使った軽作業をしやすくするという目的とを考えあわせて以下に挙げる4点を満たすものを作ろうと考えました。

  • 指先を温める
  • 素手に装着して使う
  • 手作業の邪魔にならない
  • 少なくとも数分間は温かさが持続する

これらを実現するために自ら発熱する小さな熱源を第2指節の手の甲側に取り付ければよいと考えました。今回は熱源として酢酸ナトリウム三水和物の凝固熱を使用しました。酢酸ナトリウム三水和物を入れた小型容器をゴムバンドの輪っかで指に装着するだけで使えるので楽です。汚れるのはゴムバンドだけなので洗濯するなり交換するなりすれば繰り返し使用できます。

実際に使うとなれば安さも重要になってきますが、この方法なら安く作れます。

製作方法

材料

  • 酢酸ナトリウム無水物(Amazon
  • 水(水道水)
  • 醤油用の鯛型プラスチック容器(100均)
  • ゴムバンド(100均)
  • 小さいタッパー(100均)

熱源

今回は酢酸ナトリウム三水和物の過冷却現象を利用し、その凝固熱をカイロの熱源とします。

酢酸ナトリウム無水物82[g]と水54[g]の割合でペットボトルに入れ、湯煎にかけながら酢酸ナトリウム三水和物を作ります。酢酸ナトリウム三水和物を買ってもいいかもしれません。

最小目盛りが10[g]のはかりしか持っていなかったので重さはほぼ目分量で計りました。湯煎にかけると水が蒸発して減ってしまうので水は54[g]より少し多めに入れました。蒸留水を用意するのが面倒だったので水道水を使いました。

この作業は次のサイトを参考にして行いました。

http://www.onsenmaru.com/topics/T-150/T-177-jisakukairo-B.htm

カイロ本体

液体状態の酢酸ナトリウム三水和物を醤油用の鯛型プラスチック容器に入れて冷まします。ペットボトルに残った酢酸ナトリウム三水和物も冷まして衝撃を加え凝固させ、タッパーに入れておきます。

取り付け部

ゴムバンドを適当な長さに切ります。(私の指の場合9cmくらい。)ゴムバンドの両端がほつれないようにライターであぶります。ゴムバンドで輪を作り、縫い代1cmくらいで縫います。

使用方法

発熱開始

液体状態の酢酸ナトリウム三水和物入り鯛型プラスチック容器のふたを開け、酢酸ナトリウム三水和物の結晶をひとかけら入れると、そのかけらを起点として凝固が進行し、凝固熱が発生します。発熱が確認できたら容器のふたを閉めます。

装着

ゴムバンドの輪を手の指の第2指節にはめ、指の背とゴムバンドの隙間に、発熱している鯛型容器を挟みます。このページの一番上の写真(指先カイロ装着図)を参考にしてください。

再使用

凝固し終わって冷えたカイロを容器ごと湯煎にかけて中身を液体状態に戻し、冷まします。

湯煎の様子

湯煎の様子

以降同じことの繰り返しです。

苦労した点

酢酸ナトリウム無水物から酢酸ナトリウム三水和物をつくるのにかなり時間がかかるので大変でした。

酢酸ナトリウム無水物と水の分量が適当だったために水が多すぎたり少なすぎたりしました。そのせいか、一番最初に作った酢酸ナトリウム三水和物はなかなか凝固しなかったので困りました。

酢酸ナトリウム三水和物を入れる容器探しでだいぶ苦労しました。試行錯誤の末に見つけた醤油用の鯛型プラスチック容器は、その丸みのために指の背の曲面によく合うので、意外にも結構いい容器です。

酢酸ナトリウム三水和物を使ったカイロ自体はすでに製品化されています。既存のカイロは衝撃を加えることで発熱を開始させます。しかし今回作った酢酸ナトリウム三水和物入り鯛型容器にどんなに衝撃を加えても凝固が開始しなかったので別の方法を考えなけばなりませんでした。凝固はなんらかのきっかけがないと開始しないので、あらかじめ作っておいた酢酸ナトリウム三水和物の小さい結晶を容器に入れればよいのではないかと思いつき、試してみたらうまくいきました。手間がかかりますが、この方法が確実です。

左:凝固前、中:結晶入りタッパー、右:凝固後

左:凝固前、中:結晶入りタッパー、右:凝固後

改善点

酢酸ナトリウム三水和物を使えばカイロを家庭で作りやすいですし、再使用可能、軽量、制御不要、安全(酢酸ナトリウムは食品添加物なので口に入っても大丈夫ですし融点が58℃なので熱くなりすぎない)という利点があります。しかし発熱開始のための操作が面倒ですし、今回使った醤油用の鯛型プラスチック容器のように完全に密閉されていない容器だと酢酸ナトリウムの酸っぱいにおいがしますし、酢酸ナトリウム三水和物を温めて融かさなければ再使用できないので面倒という欠点があります。

「指先カイロ」をより使いやすいものとするためには発熱部分を変更する必要があります。私は市販のカイロのような鉄の酸化熱を利用した発熱部分がよいと考えています。ただし市販のカイロの大きさは手のひらサイズなので指先サイズの特注品である必要があります。カイロの入ったビニール袋を開けるだけで発熱が開始するので楽ですし、使い捨てなので楽ですし、無臭です。鉄粉自体は安全ではないですが市販のカイロのような入れ物に入っていれば安全です。市販のカイロは変形しやすいので鯛型容器よりも指にフィットしてより広範囲に指を温められます。

まとめ

カイロ会社が指先サイズのカイロも発売してくれればいいのに。

"楽器"としてのステッピングモーター入門

モーターを鳴らして演奏することが流行っているので自分でもやってみました。ついでにステッピングモーターについて理解を深めました。

はじめに

プログラムにC言語を使いました。

この記事はすでにステッピングモーターを回すことができる人向けです。

今回は2相ユニポーラのハイブリッドステッピングモーターを使いました。

2020.5.31追記 図がところどころ間違っていたので直しました。

ステッピングモーターについて

ステッピングモーターとは[1]「パルス信号によって回転角度・回転速度を正確に制御できるモーター」です。このパルス信号の周波数を変えると回転速度を変えられます。速く回転させると高い音が鳴り、遅く回転させると低い音が鳴ります。ということは、このモーターは周波数を調整すればドレミファソラシドの音を鳴らせて音楽を演奏できるモーターと言い換えることができます。

※[1], [2], ・・・は参考文献番号。以下同様。

駆動方式

2相ステッピングモーターには2つコイルが使われています。巻き方はモノファイラ巻きとバイファイラ巻きの2種類あります。[2]一つの極に一つのコイルを一方向に巻くのが「ユニファイラ巻き」で、一つの極に2本の線を重ねて巻くのが「バイファイラ巻き」です。

ユニファイラ巻きのステッピングモーターはバイポーラ駆動で動かします。一方でバイファイラ巻きのステッピングモーターはユニポーラ駆動で動かします。

今回使うステッピングモーターはユニポーラ駆動で動かすステッピングモーターです。

励磁方式

[3],[4]各相のコイルへ決まった順番で電流を流す方式を励磁方式といいます。

1相励磁

主にユニポーラモータで使用します。 1相ずつ励磁位相を進める方式で、フルステップ駆動ともいいます。 消費電力は少なくなりますが、振動が発生し易くなります。

1相励磁

1相励磁

1相励磁波形

1相励磁波形
2相励磁

バイポーラモータでのフルステップ駆動方式です。 2相ずつ励磁位相を進める方式です。 ユニポーラモータの1相励磁よりも、 出力トルクが大きくダンピング性に優れており、より滑らかに動作します。

2相励磁波形

2相励磁

2相励磁波形

2相励磁波形
1-2相励磁

1相励磁と2相励磁を交互に繰り返す方式で、ハーフステップ駆動ともいいます。 モータのステップ角分解能は、1相や2相励磁方式の半分になります。 2相励磁よりも更に滑らかに動作します。

1-2相励磁

1-2相励磁

1-2相励磁波形

1-2相励磁波形
マイクロステップ駆動

フルステップ、ハーフステップ駆動が、2つのコイル電流をON/OFFし一定角ずつ回転させるのに対し、 マイクロステップ駆動は、2つのコイル電流比率を階段状に変化させることで、 更に細かいステップ角で回転させることができます。

ステッピングモーター用モタドラ

今回はSLA7073MPRTというモタドラを使いました。マイコンでパルス波を出力し、回転方向(CW/CCW)と励磁方法を決めてモタドラに入力すると、モタドラからユニポーラ駆動のステッピングモーターを回すために必要な電気信号4つ( A相, Aバー相, B相, Bバー相)が出力されます。

音が鳴る原理

モタドラからの周期的なパルス電気信号が磁界の中にあるコイルを流れると、ステッピングモーターのローター(回転子。これには磁石が付いています。)とステータ(固定子。これにはコイルが付いています。)に周期的に力が働くので振動します。これがステッピングモーター音の原因と推測されます。

2019.9.3追記 推測しただけであって未検証です。

参考文献[5]によるとステッピングモーターをマイクロステップ駆動すると静音化できるようです。マイクロステップ駆動では単純なON/OFFのパルス波ではなく比較的滑らかな電気信号でモーターを駆動するから振動が抑えられたのでしょう。

2019.9.1追記 マイクロステップ駆動による静音化については自分で試しておりません。各自試してみてください。

2020.5.16追記 マイクロステップ駆動を試してみました。そして1-2相励磁のときとマイクロステップ駆動のときとでステッピングモーターが出す音を耳で確かめました。そのときモタドラへ入力するパルス信号の周波数は両方とも同じにしました。その結果、マイクロステップ駆動のときのほうが音が小さかったです。音の高さは両方とも同じでした。しかし、少し音が小さいだけでした。マイクロステップ駆動でも無音化できないようです。

ドレミファソラシド

ネットで調べればドレミファソラシドの周波数が調べられます。今回は参考文献[6]を使って必要な音階の周波数を求めました。

曲をプログラミングするためには

ステッピングモーターで演奏するために必要なのは音の高さと長さの2つです。だからプログラムにおける楽譜は音の高さとその長さを格納した配列で表せます。ステッピングモーターを駆動する電圧を音ごとに変更すれば音の大きさを調整できるかもしれませんが今回はやりませんでした。あと、音色は変更できません。

C言語に使える文字に「#」と「♭」は含まれていません。そこでドイツ音階を代わりに使いました。ドイツ音階なら全てアルファベットで書き表せるからです。

対応表

ファ

臨時記号

なし

C

ツェー

D

デー

E

エー

F

エフ

G

ゲー

A

アー

H

ハー

#

Cis

ツィス

Dis

ディス

Eis

エイス

Fis

フィス

Gis

ギス

Ais

アイス

His

ヒス

Ces

ツェス

Des

デス

Es

エス

Fes

フェス

Ges

ゲス

As

アス

B

ベー

ドはCではなくDO、ド#はCisではなくDO_SHARP、レはDではなくREとすれば少し読みやすいプログラムになったかもしれませんが、楽譜を全部手打ちしたため一音が短いほうが楽だったのでドイツ音階を採用しました。

パルス波(としてPWMを使います)の周期を変えることで色々な高さの音を鳴らします。

PWM周期レジスタに入れる値

鳴らしたい音階の周波数(これをf[Hz]とおきます)を元にして実際にPWM周期レジスタに入れる値を計算します。

\frac{タイマのクロック周波数}{f}\tag{1}

式(1)の「タイマのクロック周波数」の単位は[Hz]です。どのマイコンを使うかとクロックをどう設定するかとタイマをどう設定するかによってこの値は異なるので自分で調べましょう。

今回はSTM32F303K8T6マイコンをシステムクロック周波数64[MHz]に設定し、プリスケーラでタイマのクロック周波数を1[MHz]にして使いました。このマイコンにはFPU(浮動小数点演算装置)がついているので、遠慮なく小数を含む計算をマイコンで行います。

この計算は次のようにマクロを使って実現しました。2行目のマクロのXには音階の周波数(実数)が入ります。

#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X))

マクロ

音の高さと長さをマクロで表します。音の高さには鳴らしたい音の周波数を、音の長さには実際に伸ばす時間[ms]を使います。

音階

音階マクロを例示します。ドレミファソラシとド♯レ♯ファ♯ソ♯ラ♯です。例えばレ♭はド♯と同じ音なのでマクロにする必要はありません。

5はオクターブの番号です。音が高いオクターブほど大きい数字を使います。どの数字を使うかは実際に鳴らして決めてください。

休符はRESTとしました。

ここでは1オクターブ分だけしか示していないので、必要があれば自分で補ってください。

音が低すぎても高すぎてもきれいにモーターが鳴りません。実際に鳴らしてどのオクターブならきれいに鳴るかを確かめてみてください。

#define C5 523.251//ド #define Cis5 554.365//ド#/レb #define D5 587.330//レ #define Dis5 622.254//レ#/ミb #define E5 659.255//ミ #define F5 698.456//ファ #define Fis5 739.989//ファ#/ソb #define G5 783.991//ソ #define Gis5 830.609//ソ#/ラb #define A5 880.000//ラ #define Ais5 932.328//ラ#/シb #define H5 987.767//シ #define REST 0

2020.4.2追記:Shift_JIS文字コードで表現された「ソ」の2バイト目は0x5Cであり、プログラムをコンパイルするときに「\」と認識されてしまいます。このように2バイト目が0x5Cである文字を駄目文字と呼ぶそうです。意図通りにコンパイルするためにはコメント「//ソ」を「//そ」に書き換えるなどの工夫が必要です。

長さ

長さマクロを示します。全音符と全休符の長さを表すlen1が1536[ms]である理由は3で割り切れる数字のほうがいいかと思ったからです。ちょうどいい長さであればいくつでもいいと思います。

#define LEN1 1536		//全音符と全休符の長さ。2^9*3[ms]
#define LEN2 LEN1 / 2		//二分音符と二分休符の長さ
#define LEN2d LEN1 / 4 * 3	//符点二分音符と符点二分休符の長さ
#define LEN4 LEN1 / 4		//四分音符と四分休符の長さ
#define LEN4d LEN1 / 8 * 3	//符点四分音符と符点四分休符の長さ
#define LEN3_4 LEN1 /2 / 3	//三連四分音符と三連四分休符の長さ
#define LEN8 LEN1 / 8		//八分音符と八分休符の長さ
#define LEN3_8 LEN1 / 4 / 3	//三連八分音符と三連八分休符の長さ
#define LEN8d LEN1 / 16 * 3	//符点八分音符と符点八分休符の長さ
#define LEN16 LEN1 / 16		//十六分音符と十六分休符の長さ

楽譜

例として「かえるの合唱」の楽譜について説明します。

音の高さと長さを別々の配列に格納しました。二元配列を使って一つの配列に音の高さと長さの両方を格納してもいいと思います。この2つを合わせて楽譜配列と呼びます。

frog_notesには音階マクロを格納します。この配列を音階配列と呼びます。

frog_lenには長さマクロを格納します。この配列を長さ配列と呼びます。

音階配列frog_notes[i]に格納した音を長さ配列frog_len[i]に格納した長さ分伸ばして演奏します。

ヘッダファイルfrog_notes.hに音階をまとめて記述し、ヘッダファイルfrog_len.hに音の長さをまとめて記述します。そして配列宣言時に配列の中に音と長さを格納します。

const float frog_notes[] = {
		#include "frog_notes.h"
};
const uint16_t frog_len[] = {
  		#include "frog_len.h"
};

音階配列の中身であるヘッダファイルfrog_notes.hには次のように書いて下さい。コメントの数字は小節番号を表しています。

C5	,	//1
D5	,
E5	,
F5	,
E5	,	//2
D5	,
C5	,
REST	,
E5	,	//3
F5	,
G5	,
A5	,
G5	,	//4
F5	,
E5	,
REST	,
C5	,	//5
C5	,
C5	,	//6
C5	,
C5	,	//7
C5	,
D5	,
D5	,
E5	,
E5	,
F5	,
F5	,
E5	,	//8
D5	,
C5	,

長さ配列の中身であるヘッダファイルfrog_len.hには次のように書いて下さい。

LEN4	,	//1
LEN4	,
LEN4	,
LEN4	,
LEN4	,	//2
LEN4	,
LEN4	,
LEN4	,
LEN4	,	//3
LEN4	,
LEN4	,
LEN4	,
LEN4	,	//4
LEN4	,
LEN4	,
LEN4	,
LEN2	,	//5
LEN2	,
LEN2	,	//6
LEN2	,
LEN8	,	//7
LEN8	,
LEN8	,
LEN8	,
LEN8	,
LEN8	,
LEN8	,
LEN8	,
LEN4	,	//8
LEN4	,
LEN2	,

実はステッピングモーター演奏で一番大変なのが楽譜を書く作業です。まず曲を聴きながら楽譜に書き起こし、プログラムに書き込みます。めちゃくちゃ面倒なので途中で何度も止めようかと思いましたが馬鹿力でやりきりました。だれか楽に演奏できるようにするための方法を知っていたら教えてほしいです。ちなみに私は「コンバットマーチ」と「ようこそジャパリパークへ」と「アンダー・ザ・シー」と「ゲゲゲの鬼太郎」と「エレクトリカルパレード」と「君をのせて(天空の城ラピュタ)」と「カエルの歌(二重奏)」をプログラムに書き込みました。しんどかったです。

耳コピした内容が間違ってないか確認するためにDOMINOというフリー作曲ソフトで音を確認しました。このようなソフトを使ってうまくやる方法がないか考え中です。

楽譜読み込み

楽譜配列をfor()文で読み込みます。ms_wait()はミリ秒待機する自作関数です。レジスタ構造体にHALライブラリを使っているので注意してください。今回は2個のステッピングモーターを同時に同じように鳴らします。TIM16によるPWM波で1個目のステッピングモーターを制御し、TIM17によるPWM波で2個目のステッピングモーターを制御します。

まず、音階配列に格納した周波数[Hz]からPWM波の周期を計算し、TIM16->ARRレジスタに代入します。タイマカウンタの比較一致値をPWM波の周期の半分とします。(必ずしもタイマカウンタの比較一致値をPWM波の周期の半分にする必要はありません。)そのあとに長さ配列に格納した伸ばす長さ[ms]分だけ待機します。最後にTIM16の2つのレジスタに0を代入して音を消し、少しだけ待機します。(今回は10[ms]待機しました。)こうすることで音と音が区切れて音楽が聴きとりやすくなります。TIM17についても同様です。以上の流れにより音を1音鳴らせます。次の音を鳴らしたいときは配列の添え字iをインクリメントして楽譜配列の次の音を読みに行きます。

ここで休符マクロRESTを0にした効果が現れます。場合分けしなくても休符のときはARRレジスタに0が代入されるので音を消せます。

for(i = 0; i < 31; i++) {
	TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);
	TIM16->CCR1 = TIM16->ARR / 2;
	TIM17->ARR = TIM16->ARR;
	TIM17->CCR1 = TIM16->CCR1;
	ms_wait(frog_len[i]);
	TIM16->ARR = 0;
	TIM16->CCR1 = 0;
	TIM17->ARR = 0;
	TIM17->CCR1 = 0;
	ms_wait(10);
}

main関数

main関数はこんな感じになります。init()関数は各種初期化を行う自作関数です。

int main(void)
{
	init();
	GPIOB->ODR &= ~GPIO_ODR_7; // ステッピングモータ励磁ON
	uint8_t i;
	for(i = 0; i < 31; i++) {
		TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);
		TIM16->CCR1 = TIM16->ARR / 2;
		TIM17->ARR = TIM16->ARR;
		TIM17->CCR1 = TIM16->CCR1;
		ms_wait(frog_len[i]);
		TIM16->ARR = 0;
		TIM16->CCR1 = 0;
		TIM17->ARR = 0;
		TIM17->CCR1 = 0;
		ms_wait(10);
	}
	GPIOB->ODR |= GPIO_ODR_7; // ステッピングモータ励磁OFF
	while(1);
	return 0;
}

プログラム全景

マイコンの初期設定についてはここでは説明しませんが、ソースコードを載せておきます。色々とコメントをつけておいたので参考にしてください。

2019.8.15追記:SW4STM32(開発環境)に最初からついてくるスタートアップファイル(?)を使うと、このコードをコンパイルできない可能性があります。参考にするだけにしてください。

2020.4.2追記:Shift_JIS文字コードで表現された「ソ」の2バイト目は0x5Cであり、プログラムをコンパイルするときに「\」と認識されてしまいます。このように2バイト目が0x5Cである文字を駄目文字と呼ぶそうです。意図通りにコンパイルするためにはコメント「//ソ」を「//そ」に書き換えるなどの工夫が必要です。

#include "stm32f3xx.h"

//音階[Hz]
#define C5 523.251//ド
#define Cis5 554.365//ド#/レb
#define D5 587.330//レ
#define Dis5 622.254//レ#/ミb
#define E5 659.255//ミ
#define F5 698.456//ファ
#define Fis5 739.989//ファ#/ソb
#define G5 783.991//ソ
#define Gis5 830.609//ソ#/ラb
#define A5 880.000//ラ
#define Ais5 932.328//ラ#/シb
#define H5 987.767//シ

//休符
#define REST 0

//伸ばす長さ[ms]
#define LEN1 1536		//全音符と全休符の長さ。2^9*3
#define LEN2 LEN1 / 2		//二分音符と二分休符の長さ
#define LEN2d LEN1 / 4 * 3	//符点二分音符と符点二分休符の長さ
#define LEN4 LEN1 / 4		//四分音符と四分休符の長さ
#define LEN4d LEN1 / 8 * 3	//符点四分音符と符点四分休符の長さ
#define LEN3_4 LEN1 /2 / 3	//三連四分音符と三連四分休符の長さ
#define LEN8 LEN1 / 8		//八分音符と八分休符の長さ
#define LEN3_8 LEN1 / 4 / 3	//三連八分音符と三連八分休符の長さ
#define LEN8d LEN1 / 16 * 3	//符点八分音符と符点八分休符の長さ
#define LEN16 LEN1 / 16		//十六分音符と十六分休符の長さ

//PWM周期計算
#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X))

//音階配列
const float frog_notes[] = {
		#include "frog_notes.h"
};
//長さ配列
const uint16_t frog_len[] = {
  		#include "frog_len.h"
};

//関数宣言
void init(void);
void sysclk_init(uint8_t multiple);
void drive_init(void);
void ms_wait(uint32_t ms);

int main(void) {
	init();//マイコンの初期設定
	GPIOB->ODR &= ~GPIO_ODR_7; //ステッピングモータ励磁ON
	uint8_t i;//8ビット符号なし整数 for()文で使う
	for(i = 0; i < 31; i++) {//楽譜配列を読み込む
		TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);//PWM波の周期
		TIM16->CCR1 = TIM16->ARR / 2;
		TIM17->ARR = TIM16->ARR;
		TIM17->CCR1 = TIM16->CCR1;
		ms_wait(frog_len[i]);//待機
		TIM16->ARR = 0;//消音
		TIM16->CCR1 = 0;//消音
		TIM17->ARR = 0;//消音
		TIM17->CCR1 = 0;//消音
		ms_wait(10);//待機
	}
	GPIOB->ODR |= GPIO_ODR_7; // ステッピングモータ励磁OFF
	while(1);
	return 0;
}

//ミリ秒待機するための関数
void ms_wait(uint32_t ms) {
	SysTick->LOAD = 8000 - 1;	// reset value for count-down timer
	SysTick->VAL = 0;			// initial value
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;	// count start

	uint32_t i;
	for(i = 0; i < ms; i++){
		while( !(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) );
	}
	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;	// count stop
}

//初期設定関数
void init(void) {
	sysclk_init(16);//システムクロックを64[MHz]に設定
	drive_init();//ステッピングモーター制御のための初期設定
}

//システムクロック設定関数
void sysclk_init(uint8_t multiple) { // max clock = 64MHz
	if( multiple <= 0 || multiple > 16 ) {
		return;
	}
	RCC->CFGR |= RCC_CFGR_PLLSRC_HSI_DIV2; // PLL <== HSI/2 = 4MHz
	RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = PLL / 2 = 32MHz
	//RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2 = PLL / 2 = 32MHz
	RCC->CFGR |= ( (multiple - 1) << RCC_CFGR_PLLMUL_Pos);
	FLASH->ACR |= FLASH_ACR_LATENCY_1; // flash access latency for 48 < HCLK <= 72. This statement must be placed immediately after PLL multiplication.
	RCC->CR |= RCC_CR_PLLON;
	while(!(RCC->CR & RCC_CR_PLLRDY)); // wait until PLL is ready
	RCC->CFGR |= RCC_CFGR_SW_PLL; // PLL as system clock
	while( (RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL ); // wait until PLL clock supply starts
}

//ステッピングモーター制御のためのタイマ設定
void drive_init(void) {
	//====モータードライバ関係のGPIO設定====
	RCC->AHBENR |= RCC_AHBENR_GPIOBEN; // IO portB clock enable

	GPIOB->MODER &= ~GPIO_MODER_MODER7_Msk; // initialize PB7 function
	GPIOB->MODER &= ~GPIO_MODER_MODER6_Msk; // initialize PB6 function
	GPIOB->MODER &= ~GPIO_MODER_MODER3_Msk; // initialize PB3 function

	GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER7_Pos); // set PB7 as GPIO for output
	GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER6_Pos); // set PB6 as GPIO for output
	GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER3_Pos); // set PB3 as GPIO for output

	GPIOB->ODR |= GPIO_ODR_7;	//ステッピングモータ励磁OFF

	GPIOB->ODR |= GPIO_ODR_3;	//モータの回転方向を設定
	GPIOB->ODR |= GPIO_ODR_6;	//モータの回転方向を設定

	//====PWM出力に使うタイマの設定====
	/*--------------------------------------------------------------------
		TIM16 : 16ビットタイマ。左モータの制御に使う。出力はTIM16_CH1(PB4)
	--------------------------------------------------------------------*/
	RCC->APB2ENR |= RCC_APB2ENR_TIM16EN; // Clock supply enable

	TIM16->CR1 = TIM_CR1_CEN;			//タイマ有効
	TIM16->CR2 = 0;
	TIM16->DIER = TIM_DIER_UIE;			//タイマ更新割り込みを有効に
	TIM16->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;	//PWMモード1
	TIM16->CCER = TIM_CCER_CC1E;		//TIM16_CH1出力をアクティブHighに
	TIM16->BDTR = TIM_BDTR_MOE;			//PWM出力を有効に

	TIM16->CNT = 0;					//タイマカウンタ値を0にリセット
	TIM16->PSC = 63;				//タイマのクロック周波数をシステムクロック/64=1MHzに設定

	TIM16->EGR = TIM_EGR_UG;			//タイマ設定を反映させるためにタイマ更新イベントを起こす

	RCC->AHBENR |= RCC_AHBENR_GPIOBEN; // IO portB clock enable

	// PB4 を Alternative Function に設定する
	GPIOB->MODER &= ~(1 << 8);
	GPIOB->MODER |= 1 << 9;
	// PB4 を AF1 に設定する
	GPIOB->AFR[0] |= (GPIO_AF1_TIM16 << GPIO_AFRL_AFRL4_Pos);

	/*--------------------------------------------------------------------
		TIM17 : 16ビットタイマ。右モータの制御に使う。出力はTIM17_CH1(PB5)
	--------------------------------------------------------------------*/
	RCC->APB2ENR |= RCC_APB2ENR_TIM17EN; // Clock supply enable

	TIM17->CR1 = TIM_CR1_CEN;			//タイマ有効
	TIM17->CR2 = 0;
	TIM17->DIER = TIM_DIER_UIE;			//タイマ更新割り込みを有効に
	TIM17->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;	//PWMモード1
	TIM17->CCER = TIM_CCER_CC1E;		//TIM16_CH1出力をアクティブHighに
	TIM17->BDTR = TIM_BDTR_MOE;			//PWM出力を有効に

	TIM17->CNT = 0;					//タイマカウンタ値を0にリセット
	TIM17->PSC = 63;				//タイマのクロック周波数をシステムクロック/64=1MHzに設定

	TIM17->EGR = TIM_EGR_UG;			//タイマ設定を反映させるためにタイマ更新イベントを起こす

	RCC->AHBENR |= RCC_AHBENR_GPIOBEN; // IO portB clock enable

	// PB5 を Alternative Function に設定する
	GPIOB->MODER &= ~(1 << 10);
	GPIOB->MODER |= 1 << 11;
	// PB5 を AF10 に設定する
	GPIOB->AFR[0] |= (GPIO_AF10_TIM17 << GPIO_AFRL_AFRL5_Pos);
}

発展

ステッピングモーターを2個別々に鳴らして二重奏しましょう。2個のモーターで同じ音を鳴らしていた先ほどよりも複雑になります。楽譜配列がモーター1用音階配列、モーター2用音階配列、待機時間配列、モーター1用処理配列、モーター2用処理配列の5つ必要だからです。

モーター1, 2が鳴っている(鳴)または鳴ってない(静)状態の組み合わせは(1, 2)=(静, 静), (鳴, 静), (静, 鳴), (鳴, 鳴)の4通りあります。ある状態がどれくらいの長さ続くかを格納した配列が待機時間配列です。この待機時間とはモーター1と2のどちらかで状態(鳴/静)が変化してから次に変化が起こるまでの時間です。ある状態から次の状態に移るときどういう命令をしてやればよいかを格納してある配列がモーター1, 2用処理配列です。ここで言っている処理とはある状態の始まりに行われる命令と終わりに行われる命令の組のことです。(始, 終)=(鳴らす, 消す), (鳴らす, そのまま), (そのまま, そのまま), (そのまま, 消す)の4通りあります。

これらを扱う関数を作ってください。この関数の名前を二重奏関数と呼ぶことにします。二重奏関数が呼び出されるとまずモーター1, 2用処理配列を見てモーター1, 2に別々の命令をします。待機時間分待機したらモーター1, 2に別々の命令をします。そして二重奏関数を抜けます。二重奏関数を実行している間は上記の4通りの状態のうちどれか一つにいることになります。そして再び二重奏関数を呼び出します。この繰り返しによって二重奏を実現します。

大変ですね。だからこの項目は発展にしたのです。一応状態遷移を示しておきます。この図は左から始まって時間が進み、右で終わる図です。各列が各状態を表し、時間が経過して次の列に移るたびに二重奏関数が呼び出されます。

二重奏状態遷移図

二重奏 状態遷移図

豆知識

ステッピングモーターを励磁させなくても小さな音が鳴ります。マイコンから出力されるパルス波の音だと思います。

2相励磁で音を鳴らしてみると1-2相励磁のときと比べて1オクターブ高い音が鳴りました。前のほうで示した励磁波形の図を見てください。1-2相励磁のパルス波の周期は2相励磁のパルス波の周期の2倍です。つまり2相励磁のパルス波の周波数は1-2相励磁のパルス波の周波数の2倍です。周波数が2倍になると、音が1オクターブ高くなるので2相励磁のときは1-2相励磁のときより1オクターブ高い音が鳴ります。

参考文献

[1]ステッピングモーターの概要 - ステッピングモーターとは|製品情報 |オリエンタルモーター株式会社、2019年2月7日閲覧

[2]ユニポーラ駆動とバイポーラ駆動、2019年2月7日閲覧

[3]ステッピングモータの駆動運転 - indexPro、2019年2月7日閲覧

[4]モータを廻す、2019年2月7日閲覧

[5]気圧計のバグつぶしと、ステッピングモーターの静音化: がた老AVR研究所、2019年2月7日閲覧

[6]音階と周波数の対応表、2019年2月7日閲覧

回せば文字が浮かび上がる!マウス型LEDライト

振ると文字が浮かび上がるLEDライト、ステッパマウス、高速回転。これらが頭の中で結びついたとき、「ステキなもの」ができました。このブログはその製作記録です。

はじめに

ステッパマウスをその場で高速回転させられる前提で話を進めます。

あと必要な技術はLチカだけです。

LEDを縦に5個並べてステッパマウスに取り付け、ステッパマウスを高速回転させながらLEDをちょうどいいタイミングで点滅させ、「HELLO WORLD ! 」という文字列を表示させます。

LED文字列予想図

LED文字列予想図

文字表示原理

LEDを5個使って文字を表示させる原理を、例として「H」を取り上げることで説明します。

以下では5個のLEDを上から順にLED1, LED2, LED3, LED4, LED5という番号で呼ぶことにします。また、文字を構成する点(LEDのパルス発光によるもの)をドットと呼ぶことにします。

次に示す原理説明図を見てください。これは各LEDが担当するドットの位置と発光タイミングを表しています。赤マルはドットでありLEDのパルス発光を表し、何もないマスはLEDが光ってないことを表しています。LEDxの行はLEDxが担当します(x=1,2,3,4,5)。ステッパマウスが左回転しているときにLED計5個を1列目、2列目、3列目、4列目、5列目の順にパルス発光させると、残像効果で人の目にはHと表示されているように見えます。

原理説明図

原理説明図

用意したもの

材料

ステッパマウス 1個

マイコン(とマイコンを使うために必要な電子部品たち) 1セット

片面ユニバーサル基板(穴数36×27) 2枚

スペーサー 数個

LED(5mm砲弾型、今回は赤色) 5個

保護抵抗1kΩ 5個

空中配線用の導線(ポリウレタン銅線または被覆線。場合によってはすずメッキ線)

アングル(TAMIYAのユニバーサルプレートLの付属品) 1個

養生テープ 少し

黒いビニールテープ 少し

熱収縮チューブ 少し

M3ボルト(ねじ部の長さは6mm以上) 3個

M3ナット 3個

道具

基板を切るために必要なもの(糸鋸など)

φ3.2の穴を開けるために必要なもの(ドリルビットと手回しドリル)

はんだ付けに必要なもの(はんだごてとはんだ)

ニッパー

工作

作業の流れ

LEDを縦に5個並べる基板(以降LED基板と呼びます。)の加工

LED5個を光らせるためのマイコンのための基板(以降第2基板と呼びます。(第1基板は元からマウスに付いている基板のことです。))の加工

LED基板と第2基板を固定するためのアングルの加工

第2基板にマイコンと書き込み回路と電源回路をはんだ付け

LED基板にLEDと抵抗をはんだ付け

LED基板と第2基板を固定

第2基板とLED基板を空中配線

余計な光を遮断

マウスに設置

作業の説明

LED基板加工

まず、LED基板を加工しました。穴数27×8の大きさで片面ユニバーサル基板を糸鋸で切り取ってφ3.2の穴を2つ開けました。穴の位置はランドの面がこちら側を向くようにして基板が縦長になるように見たとき、右下の穴から数えて上に5個目で左に2個目の穴と、右下の穴から数えて上に5個目で左に6個目の穴です。φ3.2のドリルビットを使って基板に元から開いている穴を広げました。この加工が終わると下図のようになります。

LED基板加工図

LED基板加工図
第2基板加工

次に第2基板を加工しました。第2基板が必要となった理由は、私のマウスに元から付いているマイコンだけだとピンが足りなかったので新たにマイコンを追加しなければならず、そのためのスペースが第1基板上にはなかったからです。第2基板をマウスに取り付けるために必要な加工をし、さらにφ3.2の穴を1つ開けました。この穴はLED基板と接続するためのものなので位置はどこでもいいですが、マウス本来の機能(迷路探索)の邪魔にならない位置にしました。(下図ではLED基板取り付け用の穴を開けただけの第2基板の様子を示します。)

第2基板加工図

第2基板加工図
アングル製作

次にLED基板と第2基板を固定するためのアングルを作りました。

TAMIYAのユニバーサルプレートLの付属品

TAMIYAのユニバーサルプレートLの付属品

TAMIYAのユニバーサルプレートLの付属品をニッパーで必要な大きさに小さく切り取りました。このアングルを使ってLED基板と第2基板を直交させた状態で互いに固定します。私はこんな感じに切り取りました。

アングル

アングル
第2基板はんだ付け

次に第2基板にマイコンと書き込み回路と電源回路をはんだ付けしました。第2基板上の電源スイッチとリセットボタンと書き込みピンを第2基板の端に配置し、その位置に対応する第1基板の一部を切り取りました。こうすることによって、あとで第1基板と第2基板を重ねてマウスに設置したときに第2基板を操作しやすくなります。下図は第2基板をマウスに取り付けたあとの写真で、上側の基板が第1基板、下側の基板が第2基板です。

第2基板操作部

第2基板操作部

もう1つ工夫した点は第1基板と第2基板のGNDと+3.3V電源を電気的に接続したところです。ピンソケットとピンヘッダを使いました。下図のように、第1基板の下側のピンソケットと第2基板の上側のピンヘッダが第1基板と第2基板を重ねたときにはまるような位置にはんだ付けしました。今回は7ピン分はんだ付けしましたが使っているのはGNDと+3.3V電源用の2ピンだけです。残りの5ピンは少なくとも今のところは使ってません。

基板間電気的接続

基板間電気的接続
LED基板回路

次にLED基板にLEDと抵抗をはんだ付けしました。以下に回路図と配線図を示します。回路図内のPIN1~PIN5はそれぞれマイコンの汎用入出力ピンを表しています。配線図内のLED基板の向きは上述したLED基板加工図とは反対で、ランドがある面がむこう側を向いていることに注意してください。

回路図

回路図

配線図

配線図
第2基板とLED基板の固定

次にLED基板と第2基板をアングルによって固定しました。ボルトとナットで固定すると下図のようになります。(このCAD画像では簡単のためにLEDなどの電子部品は省略してあります。)

LED基板と第2基板の固定

LED基板と第2基板の固定
第2基板とLED基板の電気的接続

次に第2基板上のマイコンのピンとLED基板上のLEDのアノードを空中配線し、さらに第2基板のGNDとLED基板のGNDを空中配線しました。実際に空中配線したところを撮った写真を示します。左がLED基板で右が第2基板です。銅色のポリウレタン銅線が第2基板からLEDのアノードにつながっていて、銀色のすずメッキ線が第2基板からLED基板のGNDにつながっているのが見えると思います。

空中配線図

空中配線図
遮光

次に不要な発光部分を隠しました。LEDの光で文字を表示させるわけですから、余計な光があっては困るからです。黒いビニールテープは使いやすいくて遮光性能が高いですが、ベトベトするのでそのまま使ったらマウスもベトベトしてしまいます。そこでマウスに養生テープを貼って、その上から黒いビニールテープを貼りました。LED基板の背面(LEDがない側)を黒いビニールテープで遮光しました。マウスに元から付いているLEDも邪魔なので隠しますが、完全に隠してしまったらマウスとして使うことができなくなってしまいます。そこで熱収縮チューブを使って遮光しました。こうすれば熱収縮チューブの開口部だけから光が漏れるので、マウス横方向からは光が見えないけれど上からみれば光が見えるという状況を作り出せます。その他の不要な発光部も隠しました。

黒いビニールテープで遮光

黒いビニールテープで遮光

LEDを熱収縮チューブで遮光

LEDを熱収縮チューブで遮光
第2基板をマウスに設置

最後に第2基板をマウスに取り付けました。これでハードは完成です。スペーサーを使って第1基板の下に重ねて設置しました。マウスの全体像を示します。写真右側の縦に長い基板がLED基板です。

回転LED文字列マウス

回転LED文字列マウス

プログラム

これから第2基板のマイコンに書き込むプログラムの説明をしていきます。第1基板用のプログラムの説明は省略します。C言語を使います。

待機マクロ

ここではLEDの発光時間待機と消灯時間待機をfor文の空ループで実現します。今回第2基板用に使ったマイコンはLPC1114FN28/102(偶然持ってたから)であり、その動作周波数は48[MHz]らしい(適当)です。ということは4800000を何倍かした回数だけループを回せば1秒間待機できます。試しに24000000回ループを回したら10秒間待機できました。つまり2クロックで1ループですね。私はまだ機械語を話せないのでなぜ2クロックなのかは分かりませんが、その理由を知らなくても今回のプログラムは書けました。

さて、この考え方を使って望みの時間待機するためのマクロを作ります。ループ回数は試行錯誤して決めました。このマクロについてはあとで説明します。

#define DOT_WIDTH_WAIT for(uint32_t i = 0; i < 2400; i++) // wait 1[ms]
#define INTERVAL_ADJUSTMENT for(uint32_t i = 0; i < 33600; i++) // wait 14[ms]
#define LETTER_SPACING_WAIT for(uint32_t i = 0; i < 24000; i++) // wait 10[ms]
#define SPACE_WIDTH_WAIT for(uint32_t i = 0; i < 204000; i++) // wait 85[ms]
#define EXCLAMATION_MARK_WAIT for(uint32_t i = 0; i < 120000; i++) // wait 50[ms]

LED点灯消灯関数

文字列を表示するためにはLED5個を別々に点灯・消灯できなければなりません。「HELLO WORLD !」という文字列を表示するためには次の15個の関数が必要です。これらをまとめてLED点灯消灯関数と呼ぶことにします。

void led00000();
void led00001();
void led00010();
void led00100();
void led00110();
void led01001();
void led01100();
void led01110();
void led10001();
void led10100();
void led10110();
void led10101();
void led11000();
void led11101();
void led11111();

例としてLEDを全部消灯させる関数led00000()と、LED5だけ点灯させる関数led00001()と、LED4だけ点灯させる関数led00010()について説明します。関数名に数字(0または1)が5個ありますが、それぞれ別のLEDを表しています。一番左の数字はLED1、左から2番目の数字はLED2、(略)という順に対応しています。数字の1は点灯、0は消灯を表します。

void led00000(){
	led1(OFF);
	led2(OFF);
	led3(OFF);
	led4(OFF);
	led5(OFF);
	DOT_WIDTH_WAIT;
}

void led00001() {
	led1(OFF);
	led2(OFF);
	led3(OFF);
	led4(OFF);
	led5(ON);
	DOT_WIDTH_WAIT;
	led00000();
}
void led00010() {
	led1(OFF);
	led2(OFF);
	led3(OFF);
	led4(ON);
	led5(OFF);
	DOT_WIDTH_WAIT;
	led00000();
}

一度光らせたあとに必ず全消灯しています。ledx()関数はLEDxを点灯・消灯する自作関数です(x=1,2,3,4,5)。

LED文字関数

次にLED点灯消灯関数を使って文字を表示させる関数を作りました。'H', 'E', 'L', 'O', 'W', 'D', '!'を表示する関数が必要です。これらをLED文字関数と呼ぶことにします。

void led_char_H() {
	led11111();
	led00100();
	led00100();
	led00100();
	led11111();
}
void led_char_E() {
	led11111();
	led10101();
	led10101();
	led10101();
	led10001();
}
void led_char_L() {
	led11111();
	led00001();
	led00001();
	led00001();
	led00001();
}
void led_char_O() {
	led01110();
	led10001();
	led10001();
	led10001();
	led01110();
}
void led_char_W() {
	led11000();
	led00110();
	led00001();
	led00010();
	led01100();
	led00010();
	led00001();
	led00110();
	led11000();
}
void led_char_R() {
	led11111();
	led10100();
	led10110();
	led10101();
	led01001();
}
void led_char_D() {
	led11111();
	led10001();
	led10001();
	led10001();
	led01110();
}
void led_char_exclamation_mark() {
	led11101();
}

LED文字列関数

次にLED文字関数を使って文字列を表示させる関数を作りました。この関数をLED文字列関数と呼ぶことにします。文字と文字の間に隙間がないと読みづらいのでスペースを挟みます(LETTER_SPACING_WAIT)。文字列のあとにもスペースをつけます(SPACE_WIDTH_WAIT)。文字列"HELLO"と"WORLD"の長さをそろえるためにもスペースをつけます(INTERVAL_ADJUSTMENT)。びっくりマーク'!'専用のスペースも使います(EXCLAMATION_MARK_WAIT)。

void led_str_HELLO() {
	led_char_H();
	LETTER_SPACING_WAIT;
	led_char_E();
	LETTER_SPACING_WAIT;
	led_char_L();
	LETTER_SPACING_WAIT;
	led_char_L();
	LETTER_SPACING_WAIT;
	led_char_O();
	INTERVAL_ADJUSTMENT;
	SPACE_WIDTH_WAIT;
}
void led_str_WORLD() {
	led_char_W();
	LETTER_SPACING_WAIT;
	led_char_O();
	LETTER_SPACING_WAIT;
	led_char_R();
	LETTER_SPACING_WAIT;
	led_char_L();
	LETTER_SPACING_WAIT;
	led_char_D();
	SPACE_WIDTH_WAIT;
}
void led_str_exclamation_marks() {
	led_char_exclamation_mark();
	EXCLAMATION_MARK_WAIT;
}

main()関数

main()関数を示します。init()関数のなかでLチカ用の初期設定を行います。次にマウスの回転が安定するまで待機します。そして文字列"HELLO"を表示します。少し待機したあとに文字列"WORLD"を表示します。少し待機したあとにびっくりマーク"! ! ! ! !"を表示し続けます。

int main(void) {

	init(); // setup for LEDs

	for(uint32_t i = 0; i < 12000000; i++); // wait 5[s]

	for(uint8_t i = 0; i < 20; i++) {
		led_str_HELLO();
	}

	for(uint32_t i = 0; i < 2400000; i++); // wait 1[s]

	for(uint8_t i = 0; i < 20; i++) {
		led_str_WORLD();
	}

	for(uint32_t i = 0; i < 2400000; i++); // wait 1[s]

	while(1) {
		led_str_exclamation_marks();
	}

	return 0 ;
}

調整

あと必要なのは根気だけです。2つのマイコン間で通信し合っていないため、自動でマウス回転速度とLED点滅間隔を調整することはできません。文字列が読み取れるようになるまでマウスの回転速度とLED点滅間隔を少しずつずらしながら地道に調整を繰り返すしか方法はありません。私はマウス回転速度を固定してLED点滅間隔を少しずつずらしていったのですが、始めのうちはLED点滅間隔が長すぎるのか短すぎるのかすら分からない状態でした。何度か試行錯誤を繰り返すうちにちょうどいいLED点滅間隔を見つけられました。

LED点滅間隔を調整するためには先ほど示した待機マクロを使います。待機マクロに使われているループ回数を少しずつ変更しながらちょうどいいループ回数を見つけました。ドットサイズを調節するためにはDOT_WIDTH_WAITを使います。INTERVAL_ADJUSTMENTはDOT_WIDTH_WAITの8倍の回数ループさせます(と、思いきや14倍がちょうどよかったです。意味不明。)。文字間隔LETTER_SPACING_WAITは自分が読みやすいと思う回数ループさせます。SPACE_WIDTH_WAITによって文字列が移動する速さを変えます。EXCLAMATION_MARK_WAITで'!'の数と移動する速さを変えます。

 当然ながら第1基板上のマイコン(マウス用)にはマウス用のプログラムを書きこみます。ただしその場高速回転プログラム付きですが。待機マクロを変更したら第2基板に書き込んで実験という動作を繰り返し、必要に応じてマウス回転速度を変更して第1基板に書き込んで実験しました。

動画

実際に動かしてみた様子を示します。この動画では右から左へ文字列"HELLO"が流れたあとに文字列"WORLD"が流れ、最後に"! ! ! ! ! !"と表示されます。

ラジペン代わりの平ヤットコ

お薦め工具を紹介します。

私は電子工作をするときにラジオペンチの代わりに平ヤットコを使っています。

平ヤットコ

平ヤットコ

平ヤットコは丸ヤットコとニッパーと合わせて手芸によく使われる道具です。見た目はラジオペンチに似ていますが、ラジオペンチと違って物をつかむ部分がギザギザしていないという特徴があります。

以前電子工作のためにラジオペンチを使っていたとき、ギザギザで電子部品などに傷が付いてしまうことに対して不満を抱いていました。何かもっといいラジオペンチがないか探しに東急ハンズを訪れ、偶然立ち寄った手芸コーナーで見つけたのがこの平ヤットコです。買ってみて、使ってみたら使いやすかったのでそれ以来ずっと使っています。ラジオペンチは物によりますが2000円くらいなのに対し、平ヤットコはだいたい500円くらいで買えてしまいます。安くて使いやすいので満足しています。

この平ヤットコには次のような利点があります。

  • 安い(約500円)
  • さびない
  • 先が細い
  • ちゃんとバネが付いてる
  • 物をつかむ部分がギザギザしてない

1年くらい前に買った物ですので今も(2019年2月25日現在)東急ハンズにあるのかネットで調べてみましたが、見つけられませんでした。今は取り扱ってないのかもしれません。

平ヤットコとは別に100均で売っているラジオペンチも使っていますが、先が太くてすぐさびます。電子工作に使えないわけではありませんがなんか微妙です。

2019.5.30追記

しばらく平ヤットコを使っていたらバネが取れやすくなってしまいました。

はずれたバネ

はずれたバネ

バネがはずれてなくなってしまうと困るので、糸で平ヤットコ本体に結びつけてみました。

糸でバネ紛失防止

糸でバネ紛失防止

バネを再び平ヤットコ本体にはめたら応急処置は完了です。

応急処置完了

応急処置完了


 

割り込み音楽

東京理科大MICEの方のクラシックマウスが走りながら曲を鳴らしているのを見て自分でもやってみたくなりました。演奏関連の処理のうち初期設定以外を全てタイマ割り込み関数内で行うことで実現しました。

注意

この記事はマイコンのクロック設定とPWM設定とタイマ割り込み設定ができる人向けです。だからそれらに関する説明はしません。

今回はSTM32F303C8T6という48ピンのマイコンを使って鳴らしました。他のマイコンのことは知りませんが基本的な考え方は同じだと思います。

プログラムにC言語とHALライブラリを使っています。

スピーカー

UGCT7525AN4というスピーカーを使いました。秋月電子で手に入り、表面実装でき、小さくてそこそこ大きい音が鳴るからです。しかし騒音が大きい場所では音がかき消されてしまうかもしれません。

表面実装用ダイナミックスピーカー UGCT7525AN4: パーツ一般 秋月電子通商-電子部品・ネット通販

データシートによるとこの図のように配線するといいみたいです。Vinをマイコンのピンにつなげます。今回はダイオードと抵抗とトランジスタは適当に買ったものを使いました。このダイオードはスピーカーに逆電圧がかからないようにするためにつけるみたいです。

スピーカ配線図

スピーカ配線図

プログラムの説明

音楽の教科書などに載っている楽譜には音の高さや長さ、テンポや強弱など、たくさん記号が書かれていますが、曲をプログラムする際には音の高さと長さ以外の情報を全て無視します。

PB9ピンからスピーカーに入力するパルス波が出力されます。

ここでは曲の例として「ド・レ・ミ・ファ・ソ・ラ・シ・休み」を繰り返すプログラムを示します。

楽譜

ドレミファソラシの音の周波数をネットなどで調べてマクロにします。このマクロの単位は[Hz]です。休符はRESTとし、適当な数字な数字を割り当てます。0にすると音の周波数と被らずに済みます。

#define DO 261.626
#define RE 293.665
#define MI 329.628
#define FA 349.228
#define SOL 391.995
#define LA 440.000
#define SI 493.883
#define REST 0

次に音の長さのマクロです。八分音符の長さをLEN8というマクロで表します。長さは適当に1000[ms]とします。

#define LEN8 1000 // 八分音符の長さ[ms]

楽譜を配列として表します。二元配列を使い、1行目に音の高さと休符、2行目に音の長さを格納します。

const float music[2][8] = {
		{DO, RE, MI, FA, SOL, LA, SI, REST},
		{LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8}
};

PWM

スピーカーに入力するパルス波をマイコンのPWM機能を使って生成します。今回はTIM17というクロックをPWM用に使います。CLK_FREQはタイマクロックの周波数という意味です。今回はタイマクロックの周波数を1[MHz]に設定するので1000000にします。パルス波の周期をタイマカウンタのカウント上限を指定することによって自由に変更するためにFREQ2PWMPERIOD(X)というマクロを使って音の周波数からカウント上限を計算します。このマクロで-1している理由は、タイマカウンタが0から数え始めるためそのままだと1カウント多くなってしまうからです。

#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X) - 1)

タイマ割り込み

1[ms]ごとに関数を呼び出します。今回はTIM6というタイマを使い、TIM6_DAC1_IRQHandler()という関数をタイマ割り込みで呼び出します。この関数と、音の長さを1[ms]単位で計るsound_timerという変数と、楽譜配列のどの音符を鳴らしているかを示すnote_numという変数を使って演奏に必要な処理を行います。関数が呼び出されたとき、もし休符の最中でなければパルス波の周期を指定します。そしてその値のちょうど半分をコンペアマッチ用の値として使ってHIGHとLOWを反転させます。そうすると1周期の間のHIGHとLOWの時間は同じ長さになります。もし休符の最中であれば音が鳴らないようにします。関数が呼び出されたとき、もし指定した長さに音を鳴らし終わっていたらsound_timerを0にしてnote_numをインクリメントします。こうすることで次に関数が呼び出されたときに次の音を鳴らし始められます。関数が呼び出されたとき、もし楽譜配列の最後まで鳴らし終わっていたら再び楽譜配列の最初から鳴らし始めます。

uint16_t sound_timer = 0;
uint8_t note_num = 0;

void TIM6_DAC1_IRQHandler(void) {

	if( !(TIM6->SR & TIM_SR_UIF) ){
		return;
	}

	sound_timer++;
	if(music[0][note_num] != REST) {
		TIM17->ARR = FREQ2PWMPERIOD(music[0][note_num]);
		TIM17->CCR1 = TIM17->ARR / 2;
	}else{
		TIM17->CCR1 = 0;
	}
	if(sound_timer > music[1][note_num]) {
		sound_timer = 0;
		note_num++;
	}
	if(note_num == 8) {
		note_num = 0;
	}

	TIM6->SR &= ~TIM_SR_UIF;
}

 main関数

システムクロック設定とPWM設定とタイマ割り込み設定をして無限ループに入ります。演奏に必要な処理は全てタイマ割り込み関数内で行われるため、main関数が非常に簡潔になります。

int main(void) {
	init();
	while(1);
	return 0;
}

 

 

ソースコード

たとえ同じマイコンを使ったとしても開発環境が異なれば違うプログラムを書かなければいけないのでソースコードを丸ごと載せてもあまり参考にならないかもしれませんが一応載せておきます。コメントがぐちゃぐちゃと汚いですが、これは色々なブログなどからの寄せ集めだったり自分のメモだったりするからですね。PWMモード2を使っていますがこのコードだとモード1でも音は変わらないはずです。

2019.8.15追記 SW4STM32(開発環境)に最初からついてくるスタートアップファイル(?)を使うと、このコードをコンパイルできない可能性があります。参考にするだけにしてください。

#include "stm32f3xx.h"

#define DO 261.626
#define RE 293.665
#define MI 329.628
#define FA 349.228
#define SOL 391.995
#define LA 440.000
#define SI 493.883
#define REST 0

#define LEN8 1000 // 八分音符の長さ[ms]

#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X)) - 1

const float music[2][8] = {
		{DO, RE, MI, FA, SOL, LA, SI, REST},
		{LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8}
};

uint16_t sound_timer = 0;
uint8_t note_num = 0;

void init();
void sysclk_init();
void speaker_pwm_init();
void interrupt_init();

void TIM6_DAC1_IRQHandler();

int main(void) {
	init();
	while(1);
	return 0;
}

void init() {
	sysclk_init();
	speaker_pwm_init();
	interrupt_init();
}

void sysclk_init() {
	RCC->CFGR |= RCC_CFGR_PLLSRC_HSI_DIV2; // PLL <== HSI/2 = 4MHz
	RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = PLL / 2 = 32MHz
	//RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2 = PLL / 2 = 32MHz
	RCC->CFGR |= ( (16 - 1) << RCC_CFGR_PLLMUL_Pos); //sysclk = 64MHz
	FLASH->ACR |= FLASH_ACR_LATENCY_1; // flash access latency for 48 < HCLK <= 72. This statement must be placed immediately after PLL multiplication.
	RCC->CR |= RCC_CR_PLLON;
	while(!(RCC->CR & RCC_CR_PLLRDY)); // wait until PLL is ready
	RCC->CFGR |= RCC_CFGR_SW_PLL; // PLL as system clock
	while( (RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL ); // wait until PLL clock supply starts
	SystemCoreClockUpdate();
}

void speaker_pwm_init(void) {

	RCC->AHBENR |= RCC_AHBENR_GPIOBEN; // IO portB clock enable

	// PB9 を Alternative Function に設定する
	GPIOB->MODER &= ~(1 << 18);
	GPIOB->MODER |= 1 << 19;
	// PB9 を AF1に設定する
	GPIOB->AFR[1] |= (GPIO_AF1_TIM17 << GPIO_AFRH_AFRH1_Pos);

	RCC->APB2ENR |= RCC_APB2ENR_TIM17EN; // Clock supply enable

	TIM17->CR1 = TIM_CR1_CEN;						//タイマ有効
	TIM17->CR2 = 0;
	TIM17->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0 |
			TIM_CCMR1_OC1PE;	//PWMモード2,CH1
	TIM17->CCER = TIM_CCER_CC1E;		//TIM17_CH1出力有効
	TIM17->BDTR = TIM_BDTR_MOE;

	TIM17->CNT = 0;					//タイマカウンタ値を0にリセット
	TIM17->PSC = 64-1;					//タイマのクロック周波数をシステムクロック/64=1MHzに設定
	TIM17->ARR = 0;	//タイマカウンタの上限値。取り敢えず
	TIM17->CCR1 = 0;				//タイマカウンタの比較一致値
}

void interrupt_init(void) {

	RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // Clock supply enable
	TIM6->CR1 = TIM_CR1_CEN;		//タイマ有効
	TIM6->CR2 = 0;
	TIM6->DIER = TIM_DIER_UIE;		//タイマ更新割り込みを有効に
	TIM6->CNT = 0;					//タイマカウンタ値を0にリセット
	TIM6->PSC = 64-1;				//タイマのクロック周波数をシステムクロック/64=1MHzに設定
	TIM6->ARR = 1000-1;				//タイマカウンタの上限値。1000に設定。ゆえに1msごとに割り込み発生

	TIM6->EGR = TIM_EGR_UG;			//タイマ設定を反映させるためにタイマ更新イベントを起こす

	NVIC_EnableIRQ(TIM6_DAC1_IRQn);			//タイマ更新割り込みハンドラを有効に
	NVIC_SetPriority(TIM6_DAC1_IRQn, 1);	//タイマ更新割り込みの割り込み優先度を設定
}

void TIM6_DAC1_IRQHandler(void) {

	if( !(TIM6->SR & TIM_SR_UIF) ){
		return;
	}

	sound_timer++;
	if(music[0][note_num] != REST) {
		TIM17->ARR = FREQ2PWMPERIOD(music[0][note_num]);
		TIM17->CCR1 = TIM17->ARR / 2;
	}else{
		TIM17->CCR1 = 0;
	}
	if(sound_timer > music[1][note_num]) {
		sound_timer = 0;
		note_num++;
	}
	if(note_num == 8) {
		note_num = 0;
	}

	TIM6->SR &= ~TIM_SR_UIF;
}

早稲田大学の機械科学・航空学科と総合機械工学科の違い

よくある質問

早稲田大学の機械科学・航空学科と総合機械工学科は何が違いますか?

答え

早稲田大学には機械科が2つあります。基幹理工学部の機械科学・航空学科(通称:機航)と創造理工学部の総合機械工学科(通称:総機)です。もともと機械工学科という1つの学科でした。それが分裂し2つの学科になったため、同一の内容を学ぶ授業が多いです(授業は別々に行われます)。しかし研究面、教育面の双方において大きく異なります。

研究面における最大の違いは、機航ではロボットの研究は行われていないことです(例外あり)。対して、総機では航空機の研究は行われていません(例外あり)。重複している研究分野もありますがあくまで違う学科なので研究分野が全て一致するわけではありません。また、機航は数理科学が基本となっており、総機では実学を扱っています。
教育面における顕著な違いは、機航は基礎(または理論)を重視し、総機は実践を重視することです。機航の学生は1年次にはまだ学科に配属されていません。(基幹理工学部の学生は2年次から学科に配属されるため)。そのため1年次は専門科目を学びません。また、専門科目の授業は全て日本語で行われます。一方で総機では実践を重視する傾向にあるため、体験型学習とグループワークが多いです。1年次から専門科目を学び、専門科目の授業では英語で書かれた教科書を使う例もあります。

加えて、研究室配属時期についても違いがあります。機航では4年生で、総機では3年生で研究室に配属されます。

ここから私の偏見も含めた両学科の印象を列挙します。

機航では基礎的なことをしっかり学べます。そのため教え方がうまい先生が多く、教育面で比較すると優れています。しかし実践性が弱く、授業で習うことが実際の研究にどのように結びついているかが分かりにくいです。また、1年次は学科に配属されていないため、これから自分とは別の学科に進む知り合いができます。しかし一般に大学生同士の人間関係は希薄なので過度な期待しないほうがいい、というのが私の持論です。加えて、悪い点を挙げるなら、レポートは手書き指定の場合が多いです。専門について述べると、制御工学は使うものであって研究するものではないという考えがあるようです。(2019.11.29追記 分野によります。制御工学を研究している研究室もあります。教授によって制御工学に対する考えが異なるようです。)対して材料・流体・エネルギー・応用数理が強いです。実学は誰にでもできるのだから大学から教わるのではなく自力で学べという方針のようです。また、ある分野の専門家を育てたいという考え方を基にしたカリキュラムが組まれており、そもそも現場技術のような仕事は大卒の人間がやるべきではないといったポリシーのようです。

総機では実践的な技術と知識がたくさん身につきます。しかし教授陣は学生に理解してもらおうとは考えてないようです。付いてこられない学生は振り落とされるため、並みのやる気、能力しか備えていない人は有能な人にへばりついて大学生活を乗り切ります。また、課題が多過ぎて早稲田理工の忙しい学科ランキングでトップ3に入るくらいです(ただし、バイトとサークルをやるくらいの余裕はあるみたいです)。学生の進捗を鑑みず、次の課題が課されます。(2020.7.31追記 成績上位者でさえ他の人と協力し合わなければ課題の洪水を乗り切れません。)授業面での特徴は、グループワークが多く、学科内では仲良くなりやすいことが挙げられます。しかしグループで共通の制作物をつくる、発表をするといった場合に、有能な人ほど不満を持つという事例もあります。実践を重視しているため、授業で習うことがどのように研究に結びついているか教えてもらえますが、ロボットを前面に押し出しているのでロボットに興味ない人またはロボット以外の分野の研究室に入る人にとっては興味を惹かれないかもしれません。教育方針としては、大学では実学を大量に教えるから興味を持った分野の理論は自力で学べという考えを持っているようです。学生を専門家にはしないかわりにあらゆる分野をつまみ食いさせることで社会に出て様々な場面で助言できるように育てたいらしいです。また、授業名がカタカナで表されているので驚きますが、直訳すればただの機械工学科の授業です。カタカナのほうが格好良いという理由でしょうか。長々と述べましたが、一言で表現するなら「総機は社畜育成学科」です。

(注)この記事は機航B3と総機B3の数名で書きました。当たらずとも遠からず、いや、だいぶ近いと思います。

参考にするといい文献

www.waseda.jp