ラズパイで作れるよ

【中級者への第一歩?】ラズパイでオルゴールを作ろう!

RaspberryPiでオルゴールを作ってみる(PWM応用編)

スポンサーリンク

こんにちは!駆け出しエンジニアのまっきーです。

今回は電子ピアノと電子オルゴールの作成です!なかなか本格的になってきました。

そもそもPWMって何?という方は過去の記事をご覧ください。

取り組む課題は以下の2つです。課題はこちらのサイトでダウンロードできます。

課題一覧

  1. 電子ピアノプログラム
  2. 電子オルゴールプログラム

電子回路

回路図は、前回と同様です。

電子ピアノプログラム

ハードウェア方式PWMで電子ピアノを模したものを作成します。

条件は以下の通りです。

・ デューティ比 約 50%
・ PWM 信号の階調(range) 100
・ PWM 信号の波形のモード PWM_MODE_MS
・ 1 本指による操作とし、和音には対応しなくてよい。

タクタルスイッチと音階の対応図

タクタルスイッチと音階の対応表

以前私が作成した「ドレミを鳴らす」プログラムに似ています。

しかし、設定する分周値があらかじめ与えられているので、計算はいらないですね!

ソースコード

//6-4ハードウェア方式で電子ピアノのプログラム
//条件として、デューティー比50%、range=100

#include <stdio.h>
#include <wiringPi.h>

#define PWM 18


int led[4] = {23,24,20,21};//LED0,LED1,LED2,LED3
int sw[8] = {25,17,27,5,6,13,19,26};//SW0~SW7


int main(void){
	int i;
	int range = 100;//階調
	int divisor[] = {367,389,436,490,550,582,654,734};

	//wiringpi初期化
	if(wiringPiSetupGpio() == -1) return 1;
	
	//LEDを出力に設定
	for(i=0;i<4;i++){
		pinMode(led[i],OUTPUT);	}
	//SWを入力、プルアップに設定
	for(i=0;i<8;i++){
		pinMode(sw[i],INPUT);
		pullUpDnControl(sw[i],PUD_UP);
	}

	//PWM初期設定
	pinMode(PWM,PWM_OUTPUT);//ピンのモードをPWMに
	pwmSetClock(divisor[0]);
	pwmSetRange(range);
	pwmSetMode(PWM_MODE_MS);//マークスペースモードに設定



	while(1){
		
			for(i=0;i<8;i++){
				if(digitalRead(sw[i]) == 0){
					pwmSetClock(divisor[i]);
					pwmWrite(PWM,range/2);
				}else{
					pwmSetClock(divisor[i]);
					pwmWrite(PWM,0);
				}
			}
		}
	return 0;
}

実際のソースコードがこちらです。

分周比を配列に格納しました。そして、for文の中でスイッチの読み取りとPWM出力を行うようにしました。しかし…

全く動きませんでした。

スイッチの読み取りとPWM出力が重なっているからダメなのかと思い、スイッチを読み取る関数を作成して再度挑戦

#include <stdio.h>
#include <wiringPi.h>
#define PWM 18

int led[4] = {23,24,20,21};//LED0,LED1,LED2,LED3
int sw[8] = {25,17,27,5,6,13,19,26};//SW0~SW7

int sw_control(void){
	int i;
		for(i=0;i<8;i++){
			if(digitalRead(sw[i]) == 0){
				return i;
			}else{
				return -1;
			}
		}
}
int main(void){
	int i;
	int sw_check=0;
	int range = 100;//階調
	int divisor[] = {367,389,436,490,550,582,654,734};

	//wiringpi初期化
	if(wiringPiSetupGpio() == -1) return 1;
	
	//LEDを出力に設定
	for(i=0;i<4;i++){
		pinMode(led[i],OUTPUT);	}
	//SWを入力、プルアップに設定
	for(i=0;i<8;i++){
		pinMode(sw[i],INPUT);
		pullUpDnControl(sw[i],PUD_UP);
	}

	//PWM初期設定
	pinMode(PWM,PWM_OUTPUT);//ピンのモードをPWMに
	pwmSetClock(divisor[0]);
	pwmSetRange(range);
	pwmSetMode(PWM_MODE_MS);//マークスペースモードに設定



	while(1){
		sw_check = sw_control();
			switch(sw_check){
				case 0: 
					pwmSetClock(divisor[0]);
					pwmWrite(PWM,range/2);
					break;
				case 1: 
					pwmSetClock(divisor[1]);
					pwmWrite(PWM,range/2);
					break;
				case 2: 
					pwmSetClock(divisor[2]);
					pwmWrite(PWM,range/2);
					break;
				case 3: 
					pwmSetClock(divisor[3]);
					pwmWrite(PWM,range/2);
					break;
				case 4: 
					pwmSetClock(divisor[4]);
					pwmWrite(PWM,range/2);
					break;
				case 5: 
					pwmSetClock(divisor[5]);
					pwmWrite(PWM,range/2);
					break;
				case 6: 
					pwmSetClock(divisor[6]);
					pwmWrite(PWM,range/2);
					break;
				case 7: 
					pwmSetClock(divisor[7]);
					pwmWrite(PWM,range/2);
					break;
			default:
					pwmWrite(PWM,0);
					break;
				}	
				
			}
				
			
		
	return 0;
}

これでもうまくいきませんでした。delayを入れたりしてみたのですが、それでもうまくいかず…

著者のコードをコピペしても鳴りませんでした…

原因がまだわかっていません。

もしわかる方がいらっしゃいましたら、教えて頂きたいです。

一度諦めて、次の問題に進みます。

電子オルゴール

SW0を押したら電子オルゴールが動作するプログラムを作成します。

自分の好みの歌をSW0を押すことで、スタートさせます。オルゴールの曲は、「きらきら星」を選びました。

条件は以下の通りです。

・ デューティ比 約 50%
・ PWM 信号の階調(range) 100
・ PWM 信号の波形のモード PWM_MODE_MS

仕様(簡易版)

かなり複雑でしたので、著者のサイトのヒントを参考に簡単な仕様にまとめてみました。

オクターブと鍵盤の対応

きらきら星の楽譜

                                    https://ototama.com/music/folksong/score.php?id=232

今回は、自分一人で一から作成というのは厳しかったので、著者のサンプルコードに手を加えてきらきら星のオルゴールを作成しました。

実際のオルゴールの様子

少し音が割れているところがありますが、それっぽくはなったと思います。

*音が高すぎたので200Ωの抵抗をつけました。

ソースコード

#include <stdio.h>
#include <wiringPi.h>
#include <math.h> //pow関数を使用するため

#define PWM 18
#define PI3_CLK 19200000 //Pi3B/3B+の内部クロック Hz
#define RANGE 100 //PWM range
#define OL 3 //1 オクターブ下
#define OC 4 //中央のオクターブ
#define OH 5 //1 オクターブ上
#define REST 0 //休符
#define DO 1 //ド
#define DOS 2 //ド#
#define RE 3 //レ
#define RES 4 //レ#
#define MI 5 //ミ
#define FA 6 //ファ
#define FAS 7 //ファ#
#define SO 8  //ソ
#define SOS 9 //ソ#
#define RA 10 //ラ
#define RAS 11 //ラ#
#define SI 12 //シ

//#define  DEBUG    // マクロ定義により#ifdef #endifで挟まれた命令が有効になります。
                    // 命令を無効にする場合は、このマクロ定義をコメント文にしてビルドします。

static volatile int g_state;

int sw[8] = {25,17,27,5,6,13,19,26};//SW0~SW7


void g(int oct, int solfa, int length);
void IntSw1(void);


int main(void){
	int i;
	int solfa;
	int oct;
	int bpm = 80; //楽譜のテンポ
	int qN = 60000 / bpm;//4分音符の長さ(quarter note)
	int hN = qN * 2;//(half note)
	int musicNo;
	int music[][3] = {
		{OC,DO,qN},
		{OC,DO,qN},
		{OC,SO,qN},
		{OC,SO,qN},
		{OC,RA,qN},
		{OC,RA,qN},
		{OC,SO,hN},

		{OC,FA,qN},
		{OC,FA,qN},
		{OC,MI,qN},
		{OC,MI,qN},
		{OC,RE,qN},
		{OC,RE,qN},
		{OC,DO,hN},

		{OC,SO,qN},
		{OC,SO,qN},
		{OC,FA,qN},
		{OC,FA,qN},
		{OC,MI,qN},
		{OC,MI,qN},
		{OC,RE,hN},

		{OC,SO,qN},
		{OC,SO,qN},
		{OC,FA,qN},
		{OC,FA,qN},
		{OC,MI,qN},
		{OC,MI,qN},
		{OC,RE,hN},

		{OC,DO,qN},
		{OC,DO,qN},
		{OC,SO,qN},
		{OC,SO,qN},
		{OC,RA,qN},
		{OC,RA,qN},
		{OC,SO,hN},

		{OC,FA,qN},
		{OC,FA,qN},
		{OC,MI,qN},
		{OC,MI,qN},
		{OC,RE,qN},
		{OC,RE,qN},
		{OC,DO,hN},
		
		{OC,REST,qN},
	};
	//wiringpi初期化
	if(wiringPiSetupGpio() == -1) return 1;

	//SWを入力、プルアップに設定
	for(i=0;i<8;i++){
		pinMode(sw[i],INPUT);
		pullUpDnControl(sw[i],PUD_UP);
	}
	//PWM初期設定
	pinMode(PWM,PWM_OUTPUT);//ピンのモードをPWMに
	pwmSetRange(RANGE);
	pwmSetMode(PWM_MODE_MS);//マークスペースモードに設定
	//割り込み設定
	wiringPiISR(sw[1],INT_EDGE_FALLING,(void*) IntSw1);

	musicNo = sizeof music / ((sizeof music[0][0])*3);//配列の要素数の計算(音符の数)
	#ifdef DEBUG
        printf("%d \n",musicNo);
    #endif

	while(1){
		if(digitalRead(sw[0]) == 0){
			g_state = 0;
			for(i = 0;i <musicNo;i++){
	#ifdef DEBUG
                printf("%d  ",i+1);
    #endif
	    oct = music[i][0];      //オクターブの取得
                solfa =  music[i][1];   //階名の取得
                qN = music[i][2];       //音の長さの取得
                g(oct,solfa,qN);        //1音を再生
                if(g_state == 1){       //SW1が押されたら再生停止
                    g(OC,REST,qN);      //音の出力を停止
                    break;
				}
			}
		}
	}
	return 0;
}

void g(int oct, int solfa, int length){
int d = 0; //整数
int f = 0; //周波数
int divisor; //分周値 pwmSetClock 関数の引数
int value; //Ton 時間 pwmWrite 関数の引数
if(solfa !=0){
d = solfa - 10;
 if (oct == OL){ //1 オクターブ下のとき
 d -= 12;
 }
 if(oct == OH){ //1 オクターブ上のとき
 d+=12;
 }
 f = 440 * pow(2,(double)d/12)+0.5; //べき乗計算の pow 関数

 divisor = PI3_CLK / (f*RANGE); //Pi 3/3B+のとき
 pwmSetClock(divisor); //内部クロックの分周の設定
 value = RANGE/2; //デューティ比 50%
}else{
 value = 0;
}
#ifdef DEBUG
        printf("d=%d  f = %d\n",d,f);
#endif
pwmWrite(PWM, value);
delay(length);
}
void IntSw1(void){
	g_state = 1;
}

ビルドするときは、新しくmath.hを使用しているので、ビルドコマンドに-lmを付け加えてください。

gcc -Wall -o "%e" "%f" -lwiringPi -lpthread -g -O0 -lm 

ソースコード解読

1~23行目 ヘッダーファイルのインクルードとマクロ定義

25行目 デバック処理の有効化、無効化を設定できます

28~34行目 グローバル変数を定義、プロトタイプ宣言です

37~95行目 必要な変数の定義です 二次元の配列に音符を格納しているイメージです

96~109行目 SW、割り込み等の初期設定です

111行目 for文のカウントで使用するために音符の格納されている2次元配列の個数を調べています 

116~135行目 音符を順番に鳴らしていくための処理です SW1の割り込みが入った際は音を止めるように設定しています

137行目~ 音名、休符を作る関数です

142~149行目 solfa(階名)が0の時は休符ですので、それ以外の時 solfa-10をすることによりdの値が求められます。下の表と見比べながら見てみましょう。例えば、solfaがRE(3)だった場合3-10=-7ですよね。これで、REの周波数を求めるためのdがわかるわけです。

また、1オクターブ上の時は12乗(つまりd=12)することで表せます。下の時はー12乗です。

150行目 上の式を表しています。+0.5となっているのは四捨五入をするためのものです。int型ですので、小数部分は全て落とされてしまいます。ですが、あらかじめ0.5を足しておくと四捨五入したようにあらわすことができます。

152~166行目 PWMの出力設定です。もし、ラズパイ4を使っている方は、内部クロックが異なりますので、PI3_CLK の部分を 54000000 に変更してください。

#define DEBUG

#ifdef DEBUG
        printf();
#endif

このように定義することで、デバッグ機能のようなものをつけることができるということを初めて知りました。これはとっても便利ですね。

また、static volatile int g_stateという宣言がありました。

staticには関数と変数の参照範囲を限定的にする効果があります。つまり、ほかのファイルからの参照を禁止(保護)していることがわかります。

また、 volatile にはコンパイラの最適化を抑制する効果があります。

ここからは私の考えになるのですが、g_stateはwhile文の中では常に0です。割り込み処理があった場合のみ1になります。ですので、コンパイラはg_stateを使用されていない変数だと判断して最適化してしまう。ですのでvolatileで抑制しないといけないのかと思います。

volatile修飾子は組込みソフトウエアでは非常に重要な機能です。

まとめ・反省

・プログラムが長くなってくると、デバッグの仕組みを組み込んでおく必要が出てくる

・コンパイラが最適化してしまうと考えられるもの(レジスタなどの必要な変数のみ)はvolatile修飾子で最適化する必要がある。

・電子ピアノのシステムの不具合がなぜだかわからなかった。

→ハード側の問題を考慮できていなかったのでそちらを確かめてみる。

今回はかなり長くなってしまいました。一度でうまくいくことは、現場ではほとんどないと思うので、そのミスを見つけられるような仕組みをソフトウェア側で作ることもかなり重要なんだということがわかりました。

何とかある程度のコードは読めるようになってたので、そろそろ監視カメラ等の制作を始めていきたいですね。

最後までありがとうございました。

スポンサーリンク

-ラズパイで作れるよ