そもそも任意の周波数を作るならpwmでいいんじゃない?
Arduinoで任意の周波数を作るならpwmを使うのが主流のようですし、生成AIに聞いてもたいていpwmとレジストリで実現する方法を教えてくれます。
例えばこんな記事が私にはわかりやすかったです。
しかし、Attiny13Aのようなマイコンだと
'TCCR1B' was not declared in this scope
のようなエラーでコンパイルができません。
私にはPWMは難しすぎる:ATTinyではpwd+レジストリが使えなかった
そこで、仕方なく単純にForループで時間調整をすることにしたのですが、最初は周波数に合わせてForで回す回数を調整するのに勘を頼っていたのでなかなか正確な周波数にならないし、複数の周波数のためのパラメーターを見つけるためには種類分試行錯誤を繰り返す必要がありました。
Forで周波数を調整:勘頼りの調整はやめよう!
試行錯誤だと精度が出なかったことと、作業ががあまりに大変だったので、演繹的にパラメーターを決める方法を考えました。
1. 最初に下記ソースのwavelengthに適当に散らばらせた数字を代入して出力される周波数を測ります。
#define SP 0
const unsigned int calibration[]={
5, 10, 20, 40, 80, 200, 1000
};
unsigned int wavelength, j;
int timespan;
unsigned char i;
void setup() {
}
void loop() {
timespan=10; playList(calibration, ARRAY_LENGTH(calibration));
}
void playList(const unsigned int *list, int arrayLength){
for(i=0; i<arrayLength; i++){
wavelength=list[i];
my_tone();
}
}
void my_tone(){
unsigned long cycle_number=timespan*285714/(wavelength+2); // forループで回す総数
for(; cycle_number>0; cycle_number--){
PORTB |= _BV(SP); // LEDピンをHIGH
for(j=wavelength; j>1; j--){}
PORTB &= ~_BV(SP); // LEDピンをLOW
for(j=wavelength; j>1; j--){}
}
}
キャリブレーションとか言いながら、my_tone関数のcycle_numberのところで使っている285714とか2とかいう値は勘で決めています(汗
2. wavelengthと周波数のプロットを作成して近似直線を引きます。私はオシロスコープを使って1周期の長さを測定しました。下の図では両対数でグラフを書いています。下の絵はLibreOfficeで作成しました。
3. 近似直線(y=ax+b)の定数を求めます。
ここではLibreOfficeの関数を用いて以下のように求めました。
傾き a=LINEST($C$3:$C$9,$B3:$B9, 1, TRUE())
切片 b=INDEX(LINEST($C$3:$C$9,$B3:$B9, TRUE(), TRUE()), 1,2)
精度 R^2=INDEX(LINEST($C$3:$C$9,$B3:$B9, TRUE(), TRUE()), 3,1)
Attiny13A 4.8MHzとかなら -LOG(1-R^2) が6以上になれば合格といっていいでしょう。下記スクリーンショットはAttiny13A 4.8MHzでキャリブレーションした結果です。
4. あとは任意の周波数からwavelengthに代入すべき周波数が簡単に計算できます。
wavelength_float=(1000000/周波数-b)/a
このままだと小数点が付いてしまうので四捨五入して整数にします。
wavelength_int=int(wavelength_float+0.5)
最後に小数点を四捨五入した結果出力される予定の周波数を確認します。
出力予定の周波数=1000000/(wavelength_int*a+b)
周波数が高い領域では目的の周波数に近づけるのが難しくなるので、そういう時はマイコンの周波数を9.6MHzとかにあげると周波数を刻む間隔が小さくなるので精度が上げやすくなります。
私はキャリブレーションにオシロスコープを使いましたが、テスターの周波数測定機能を使っても同様のことはできると思います。