1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Longan Nanoを使ってみる 14 ~PWMとサウンド~

Last updated at Posted at 2022-03-09

前の記事

Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー

全体の目次

Longan Nanoを使ってみる 1 ~ビルド環境の構築~
Longan Nanoを使ってみる 2 ~デバッガの環境設定~
 Sipeed RISC-V Debugger
Longan Nanoを使ってみる 3 ~デバッガの使用方法~
Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~
Longan Nanoを使ってみる 5 ~ゲームのプロジェクトを作成~
Longan Nanoを使ってみる 6 ~文字出力~
 Longan Nanoを使ってみる ~FONTX2ファイルを作る~
Longan Nanoを使ってみる 7 ~外枠とブロックを書く~
 Longan Nanoを使ってみる ~謎の画像表示関数~
Longan Nanoを使ってみる 8 ~ボールを動かす~
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~
Longan Nanoを使ってみる 10 ~パドルを動かす~
Longan Nanoを使ってみる 11 ~ボタンの入力
Longan Nanoを使ってみる 12 ~ボールのロス~
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー~
Longan Nanoを使ってみる 14 ~PWMとサウンド~
Longan Nanoを使ってみる 15 ~音楽を鳴らす~
Longan Nanoを使ってみる 16 ~とりあえずのまとめ~

はじめに

ここでは、ボールが反射したときなどに、音を出す処理を追加していく。前回はまた電子工作とはあまり関係がなかったが、今度はまた少しだけ電子工作っぽくなる。

今回は、圧電スピーカー(圧電サウンダ)を、Longan-Nanoに追加し、PWMという方法で音を出していく。

注意

このページは、quiita.com で公開されています。URLがqiita.com以外のサイト、例えばjpdebug.comなどのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しい記事は、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d からリンクしています。

無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。

用意するもの

これまでの作業で必要だったもの

  • Windows PC (Windows10/11)
  • Longan Nano 本体
  • microSD (16GBytes程度あれば十分すぎる)
  • USB Type-Cケーブル (PCとLongan Nanoを接続する)
  • Sipeed Risk V Debugger
  • 接続ケーブル。Debugger付属、もしくは自作
  • 可変抵抗器(10KΩ、Bカーブ)
  • ボタン(プッシュスイッチ、タクトスイッチなど。)モーメンタリ動作のもの。
  • ユニバーサル基盤。小さくてよい。45mm程度あれば十分。
  • 抵抗。プルアップ用。10KΩ程度。家にあるので良い。
  • 半田ごて、線材など

このページで必要になるもの

圧電スピーカーには、スピーカータイプとブザータイプが存在する。ブザータイプは電源をつなぐだけで「ブー」と鳴る。今回はこれは使用できない。必ず、スピーカー型を使用する。秋月の場合「電子ブザー」など、はっきりとわかるように書いてあるが、わかりにくい(「他励式(スピーカー)」「自励式(ブザー」など)場合もあるので注意。

接続回路

今回は、GPIOAのポート3 (A3)に圧電スピーカーを接続する。圧電スピーカーは音質が悪く音量が小さいが、頑丈で鳴らすのが簡単、電気をほとんど消費せず、インピーダンスが高い(接続してもほかの回路への影響が少ない)。
本当は、圧電スピーカと直列に抵抗を入れてLonganNano側を保護するが、なくても問題なく動作する。

というわけで今回の接続図は次の通り。

sch3.png

PWMの使い方

概要

サウンドを鳴らすには、GPIO A3に、一定間隔でON-OFFを出力する。ソフトウェアで実行することもできるが、それをやってしまうとオン/オフのために相当の能力を犠牲にすることになってしまう。

そこで、PWM(pulse wave modulation)という機能を使用する。この機能は、事前に設定したパラメータで、あるポートにオン/オフの繰り返し信号を出し続けるという機能。

この機能を使うと、事前に設定を行った後、一度オンにするだけで、自動的にオン/オフが繰り返される。下の図では、赤い矢印のところで、プログラムから指定を行うだけで、あとは何もしないでも一定間隔の矩形波を出力することができる。

PWM1.png

PWMには、非常に多くのパラメータがあるが、PWMをサウンド出力(矩形波)に使うとき、気にしなければならないパラメータは2つだけ、パルス幅と、コンペア値。 

このパルス幅、コンペア値はそれぞれ、周波数とデユーティ比に相当する。TimerX_CARで指定したパルス幅の1/2の値を、TIMERX_CHxCVで指定するコンペア値に指定すれば、デューティ比50%、1/4を指定すれば、デューティ比25%になる。(Xやxは、使用するタイマーとチャンネルにより異なる。例えば、今回使用するTIMER4、チャンネル3であれば、TIMER4_CARとTIMER4_Ch3CVになる)

PWM3.png

具体的な動作として、PWMは、プリスケーラ(タイマーの使い方でも設定した)で指定した分周比で動くカウンターを使用し、次のような動作を繰り返す。

(PWM MODE 0、EAPWM、アップカウントモードの場合)

(1) カウンターがゼロになると、出力をオンにする。
(2) カウンターが一定の値(CAR)になるとカウンターをリセットして0にする。
(3) カウンターがコンペア値として一定の値(CHxCV)になると、出力はオフになる。

この動作は、GD32VF103 User Manualの Figure 15-16に図示されている。左側が、アップカウント―モードの様子で、階段状にカウンタがアップしていくと、それに応じてPWM出力が変化する。

pwm2.png

PWMでは、そのほか、最初のカウントが始まるまでの遅延時間や、繰り返しの条件など、さまざまに指定できるが、音を出すために使用するのは、パルス幅、コンペア値と、そのおおもとになる分周比だけでよい。

いわゆる、「ピコピコ音」と表現されるレトロなゲーム音は、今回PWMで作成する、デューティ比50%の矩形波になる。

初期化

PWMを使うには、まずタイマーを用意し、一定間隔でカウンタを回す必要がある。これは、タイマーの使い方と同じ処理となる。

  • メインの処理を回すのにタイマー割込みでTIMER5を使用したので、TIMER4をPWMに使用する。TIMER4について、これも54MHzのバスに接続されているが、108Mhzとして計算される。
  • TIMER4は、200kHzくらいでカウントさせる。この周波数に深い意味はないが、例えば65.406Hz(C2、ドの音)を出したい場合、200Hz / 65.406 ≒ 3058 となるので、CARに3058を指定する。200Khzをあまり低くしてしまうと、計算誤差が大きくなって正確な音が出しにくくなる。

これらの指定をAPIで行うのは次のような処理になる。

timer_parameter_struct timer_initpara;              // タイマーパラメータ構造体の宣言
timer_deinit(TIMER4);                               // TIMER4の初期化
timer_struct_para_init(&timer_initpara);            // タイマーパラメータ構造体の初期化       

rcu_periph_clock_enable(RCU_TIMER4);         // TIMER4にクロックを供給

timer_initpara.prescaler = 539;                    // 108MHz/540 で、およそ200KHzが得られるハズ。
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;   // カウンタのアラインメントモードをEDGE(0,1,2…9,0,1,2…)にする
timer_initpara.counterdirection  = TIMER_COUNTER_UP; // アップカウントモードにする

timer_initpara.period            = 3;		         // 200KHz/4 で、おおよそ50KHzが得られるはず。これはテスト用の値で、実際は使用時に出したい音に合わせて変更される。
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;     // デッドタイムで使用する。ここでは使わない。
timer_initpara.repetitioncounter = 0;                // 繰り返しカウンタ。毎回イベントが発生するよう0を指定	
timer_init(TIMER4, &timer_initpara);  			   // Apply settings to timer   

この例では、プリスケーラによって200KHzごとにカウンタが+1されていき、4まで上がったら0に戻る。つまり、200KHzの4回に1回の割合、50KHzでPWM信号が変化することになる。

タイマー割込みだと、あとは割り込みの指定をするだけだったが、PWMの場合、作ったタイマーを、どのように出力するかを指定する。ここでは、ほとんど指定するものはない。特に、TIMER0以外では、使用できる機能がないので指定する内容は「タイマーを使用する」というものだけ。

timer_channel_output_struct_para_init(&timer_ocinitpara);  // タイマーチャンネル出力構造体を初期化
timer_ocinitpara.outputstate  = TIMER_CCX_ENABLE;          // このチャンネルを使用するフラグ(チャンネルはtimer_channel_output_configの引数で指定される)
timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;        // コンプリメンタりのチャネルは使用しない。コンプリメンタリのチャネルはTIMER_0にだけ用意されているのでTIMER3を使用する場合は無視される。

timer_ocinitpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;   // TIMER_0以外は使用できない。チャンネルの出力極性。アクティブでHigh。
timer_ocinitpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;   // TIMER_0以外は使用できない。コンプリメンタルチャンネルの出力極性。アクティブでHigh。
timer_ocinitpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW;  // TIMER_0以外は使用できない。チャンネルの出力極性。アイドル状態でLow。
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;  // TIMER_0以外は使用できない。コンプリメンタルチャンネルの出力極性。アイドルでLOW

timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara);   // この設定(実質的にチャンネルを使用するというフラグをオンのみ)をTIMER_4のチャンネル3に適用する

基本的な設定を終えたら、PWMとしてタイマーを使用する。timer_channel_output_pulse_value_configでは、デューティ比をのための値(CH3VAL)を指定するが、あとでどうせ別の値を上書きするので今はゼロになっている。デューティ比を50%にする場合、timer_initpara.period で指定した値の1/2の値をここに指定することになる。

timer_channel_output_mode_configはchannel output compare modeを指定する。タイマー4のチャンネル3を、PWMモード0に指定する。モード0 とモード1では波形が逆になる。サウンドで使う分にはどちらでも同じ。ここには、PWM以外様々なモードが指定できるが、よくわからない。

timer_channel_output_shadow_configについてはよくわからない。

timer_auto_reload_shadow_enable(TIMER4);で、TIMER4の自動リロードの時のシャドウレジスタを有効にする。自動リロードは、タイマーのカウンタがオーバーフローして割り込みがかかり、その後0にリセットされるときに自動的に一定の値を足す機能。これによりタイマーの時間を調整できる。
(推測)
オートリロードレジスタは、カウンタが動作中でも読み書き出るが、これを実現するために値はプリロードされており、読み書きの際は本当のレジスタではなく、プリロードされたシャドウレジスタを読み書きしている。この機能をTIMER4で有効にする。

timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0);    // デューティ比を指定する。後でどうせ指定するので、ここでは0を指定している。意味がないが説明のため残す。
timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0);     // TIMER4のチャンネル3をPWMモード0として使用することを宣言、
timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);

/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER4);

/* start the timer */
timer_enable(TIMER4);

サウンドの出力

初期化が終わったら、実際に音を出すための処理を記述する。音を出すためには、音の周波数とデューティ比(50%)を指定し、タイマー4を作動させればよい。

音の周波数とデューティ比をセットするのは、次のような処理になる。周波数について、APIではtimer_parameter_struct構造体の .periodメンバーに値を指定(timer_initpara.period)し、timer_initを呼び出せば指定できるが、たった一つのパラメータを変えるために、ほかのメンバーも正しく設定する必要があり面倒。そのため、今回はAPIが用意しているマクロを使って、直接書き換えることにする。(デューティ比についても、timer_channel_output_pulse_value_config関数で書き換えることができるが、同じようにマクロを使用する)

マクロは、gd32vf103_timer.h に含まれているが、ドキュメンテーションを見つけることができない。SDKのソースコードから自力で探すか、ユーザーマニュアルから当たりをつけて見つけるしかないのかもしれない。

例えば、ユーザーマニュアルで次のようになっていたとする。
PWM4.png

APIの中の、タイマー関連のヘッダをファイル名(gd32vf103_timer.h)で見つけ、その中を見る。 マニュアルで、Address offset : 0x40となっているので、0x40を探すと、

#define TIMER_CH3CV(timerx)              REG32((timerx) + 0x40U)

という行が見つかる。マニュアルでTIMERx_CH3CVとなっているところから、CH3CVなどをキーワードにして探すなどの方法。

あるいは、timer_channel_output_pulse_value_config関数の中を見て、次のコードからTIMER_CH3CVというマクロを探しても良い。

こうして、周波数とデューティ比を指定するための2つのマクロ、TIMER_CH3CV (デューティ比)、TIMER_CAR(周波数)を求め、値を指定する。

例えば、次のように指定すると、ベースの周波数、200Khzを800(799+1、ゼロバウンダリーなので)で割った、250Hz、デューティ比約50%の矩形波が得られることになる。

    TIMER_CH3CV(TIMER4) = 800 / 2;	    
    TIMER_CAR(TIMER4) = 799;	 	

周波数の指定が終わったら、タイマー4を有効にすることで音が出力され、無効にすることで音が止まる。

    timer_enable(TIMER4);		// 音を出す
    timer_disable(TIMER4);		// 音を止める

サウンド関連コードの追加

圧電サウンダの接続と、PWMの使用方法が確認出来たら、プロジェクトに、sound.cとsound.hを追加し、サウンド関連の初期化コードを記述する。

初期化、音程の設定、サウンドのスタート、ストップをそれぞれ別の関数にして、音程は引数で指定できるようにしておく。

sound.h

#ifndef __sound_h__
#define __sound_h__

void sound_pwm_init();
void StartSound(void);
void StopSound(void);
void SoundSet(int Period);

#endif

sound.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "sound.h"

void sound_pwm_init()
{
  	timer_oc_parameter_struct timer_ocinitpara; 
	timer_parameter_struct timer_initpara;           // タイマーパラメータ構造体の宣言
	timer_deinit(TIMER4);
    timer_struct_para_init(&timer_initpara);


   	// rcu_periph_clock_enable(RCU_AF);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3);
    rcu_periph_clock_enable(RCU_TIMER4);

    timer_initpara.prescaler         = 539;                 // 108MHz/540 で、およそ200KHzが得られるハズ。
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;  // count alignment edge = 0,1,2,3,0,1,2,3... center align = 0,1,2,3,2,1,0
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;    // Counter direction
    timer_initpara.period            = 3;		            // 200KHz/4 で、おおよそ50KHzが得られるハズ。ただ、これはSoundSetで動的に書き換えられちゃうけどね。
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;    // This is used by deadtime, and digital filtering (not used here though)
    timer_initpara.repetitioncounter = 0;                   // Runs continiously	
    timer_init(TIMER4, &timer_initpara);  					// Apply settings to timer   

 	timer_channel_output_struct_para_init(&timer_ocinitpara);
    timer_ocinitpara.outputstate  = TIMER_CCX_ENABLE;        // Channel enable
    timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;      // Disable complementary channel
    timer_ocinitpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;  // Active state is high
    timer_ocinitpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;  
    timer_ocinitpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW; // Idle state is low
    timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara);   // Apply settings to channel

    timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0);                   // Set pulse width
    timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0);         // Set pwm-mode
    timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);

    /* auto-reload preload enable */
    timer_auto_reload_shadow_enable(TIMER4);

    /* start the timer */
    timer_enable(TIMER4);
    
}

bool 	isSound = TRUE;
void StartSound(void) {
    if (isSound == FALSE) return;
    timer_enable(TIMER4);		
}
void StopSound(void) {
    if (isSound == FALSE) return;
    timer_disable(TIMER4);		
}
void SoundSet(int Period)
{
    TIMER_CH3CV(TIMER4) = Period / 2;	   
    TIMER_CAR(TIMER4) = Period;	 	
}

ボールミス時のサウンド

ゲームのメインプログラムから、サウンドを鳴らして実際に動作するかをテストする。
最も簡単なケースとして、ボールをミスしたとき、「ブー」と鳴らすことにする。音を鳴らす処理は実際には、音を鳴らす、止めるのペアの処理となる。そして、その間にはしばらく時間が必要になるが、その間、プログラムの状態ループを止めることができないということに注意が必要になる。

現在、game.c内のボールをミスしたときは、STATE_BALLLOSS状態として定義されている。ボールロスが発生するとSTATE_BALLLOSSに遷移し、画面"MISS"と表示してカウンタを増やし、再度STATE_BALLLOSS状態に遷移する。カウンタが一定値になるまでこのSTATE_BALLLOSS状態に入ったり出たりを繰り返す。

つまり、STATE_BALLLOSSに入った最初の回で音を出し、STATE_BALLLOSSから抜けたときに音を止めればよい。
STATE_BALLLOSSに入った最初の回、というのはwaitCnt が 0 の回になる。したがって、次のように処理を加えることで、ボールロスが起こった時にブーという音を出すことができる。

game.c

#include "lcd/lcd.h"
   :
#include "button.h"
#include "sound.h"    // 追加
          :
          :
            case STATE_BALLLOSS:{               // ボールロス
                static u16 waitCnt = 0;

                // ライフがないならゲームオーバーに遷移
                if (LifeCnt == 0) {
                    gameState = STATE_GAMEOVER;
                    break;
                }
                // 画面に"-- MISS! --"と表示させる
                if (waitCnt == 0) {                     // 変更
                    LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
                    SoundSet(800);                      // 音程を決めて
                    StartSound();                       // 音を出す
                }
                // 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
                waitCnt++;
                if (waitCnt == 0x100 || CheckP1Button()) {
                    LCD_Clear(BLACK);
                    waitCnt = 0;
                    StopSound();                      // 追加。状態が遷移するときに音を止める
                    gameState = STATE_STARTGAME;
                }
               break;
            }
                 :
                 :

呼び出している SoundSet(800);というのは、200KHzに対する分周なので200KHz / 800 で、250Hzの音が出力されることになる。

効果音の出力

音は出るようになったが、まだ一つだけの音を、ゲームのプレーが中断している間に鳴らしただけで、ゲーム中の音というわけではない。

今回は、プレイ中(ボールが動いている間)にボールが反射したときに、音を出すような処理を加えていく。
また、「ブ」だけだと貧弱なので、ブロックに当たると「ピポ」、パドルに当たると「ポピ」と2音を鳴らすようにする。

サウンドを管理するためのデータ

音が鳴り始める瞬間は、ボールがブロックやパドルに当たった瞬間、音が鳴り終わるのは、そこから一定の時間経過後になる。

そのため、次のようなデータを持ち、管理させる。


static bool bSound[2];          // ボールの音が出てるかのフラグ。[0]がポピ、[1]がピポ。
static int iSoundIdx=0;         // ボールの音がどこまで進んでいるか。
                                // 例えば「ポピ」を出しているなら、0でポ、1がピ。
static int iSoundStep[2][2];    // 今出している音の進行状況。例えば、「ポピ」の時であれば、
                                // [0][0]が「ポ」の間増やされていき、一定の値になったら
                                //「ピ」に変えて[0][1]の値を増やしていく
static int iSoundTim[2][2] = {{400,800},{1000,600}};		//200Hzに対して、これで分週する。400なら、500Hzになる。


サウンド処理

  1. 初期化処理では、iSoundStep とiSoundStepは0に初期化される。音階のインデックスであるiSoundIdxもゼロになる。
  2. ボールがパドルに衝突(サウンド番号1)したとき、bSound[1]をTRUEにして、サウンド番号1が鳴っている状態にする。
  3. ボールを鳴らし続ける処理は、一定間隔で常に呼び出される。この中では、bSound[1]がTRUEであればサウンドが鳴っている状態と判断する。サウンドが鳴っていて、iSoundStep[1…サウンド番号][0…音階のインデックス]が0 (初期化処理で0になっている)なら初期化直後なので「音の鳴り始め」として、PWMのタイマーを有効にする。この時点で音が鳴り始める。
    iSoundStep[1][0]が0より大きければ、音が鳴り続けるということなので、iSoundStep[1][0]の値を増やす。この値が一定以上になったら、次の音階に移るため、iSoundIdxを増やす。音階が終わりなら、サウンドを止める
  4. サウンドを止める処理は、初期化処理と同じでよい。

sound.h

void BallSoundInit(void);
void BallSoundStart(int sndNo);
void BallSoundStop(void);
void BallSoundTick(void);
#endif

sound.c

   :
void BallSoundInit(void)
{
    // 変数を初期化して
	for (u8 i = 0;i<2;i++) {
		iSoundIdx = 0;
		bSound[i] = FALSE;
		iSoundStep[i][0] = iSoundStep[i][1] = 0;
	}
	timer_disable(TIMER4);		//音を止める
}
// 音を出し始める。引数に音のインデックス「ピポ」「ポピ」を指定
void BallSoundStart(int sndNo)
{
	BallSoundStop();
	bSound[sndNo] = TRUE;

}
// 今出ている音をすべて止める。初期化と同じ処理でよい
void BallSoundStop(void)
{
    BallSoundInit();
}
// 音を出す主処理。タイマーで一定間隔に呼び出される必要がある
void BallSoundTick(void)
{
	for (u8 i = 0 ; i < 2;i++){
		if (bSound[i]) {
			if (iSoundStep[i][iSoundIdx] == 0) {		// 音のなり始め
				SoundSet(iSoundTim[i][iSoundIdx]);
				timer_enable(TIMER4);
				iSoundStep[i][iSoundIdx]++;					
			} else if (iSoundStep[i][iSoundIdx]<3) {
				iSoundStep[i][iSoundIdx]++;
			} else if (iSoundStep[i][iSoundIdx] == 3) {	// 次の音へ
				if (iSoundIdx == 1) {			// 全部の音を出し終わったら終了
					BallSoundStop();
				} else {
					iSoundStep[i][iSoundIdx] = INT16_MAX;
					iSoundIdx++;
				}
			}
		}
	}
}

音を出す処理を作成したら、次に、ボールが壁やパドルに当たった時に音を出し始める処理を入れる。ball.cでは、パドルの衝突が記述されている。ボールは、パドルやブロックの象限を判定するGetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);で5(内側にボールが入っている)が返された場合に衝突と判断しているので、この処理で音を出し始めればよい。

ball.c

#include "lcd/lcd.h"
#include "led.h"
   :
#include "paddle.h"
#include "sound.h"     // 追加
   :
void CheckPaddle(struct BALLINFO *bi)
{
	int chkx = CVT_AXIS(bi->x);
	int chky = CVT_AXIS(bi->y);
	for (int j=0 ; j<2;j++) {
		struct PADDLEINFO* pi = GetPaddleInfo(j);
		if (pi == NULL) continue;
		u8 pos = GetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
		if (pos == 5) {
            :
            :
				bi->dxBase = -bi->dxBase;						
			} 
			// ポピ音を出す
			BallSoundStart(1);   // 追加
		}
	}
}

block.c

#include "lcd/lcd.h"
   :
#include "paddle.h"
#include "sound.h"// 追加
   :
   :
void blockCheck(struct BALLINFO* bi)
{
    :
    :
	// 現在のボールの位置が、ブロックの中にある場合、衝突処理を行う
	if (blockmtx[xidx][yidx].item != 0 && GetOrthant(xNow,yNow, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2) == 5) {
         :
         :
			Score = Score + 1;
		}
		// ピポ音を出す
		BallSoundStart(0);    // 追加
	}
}

音を出し始めたら、これを出し続け、音程を変えたり時間が来たら止める必要がある。そのため、メインループ内で一定間隔で音を鳴らし続ける処理を呼び出す。

game.c

#include "lcd/lcd.h"
   :
#include "button.h"
#include "sound.h"    // 追加
   :
   :
void Game(bool isDemo) 
{
    rcu_periph_clock_enable(RCU_GPIOB);
     :
     :
	Adc_init();
    sound_pwm_init();
    BallSoundInit();                      // 追加 ボール音の初期化
     :
    while (TRUE) { 
		timer_enable(TIMER5);               // タイマーを有効にする
         :
         :

        heartBeat = (heartBeat+1) & 0x7FFF;
        
        BallSoundTick();                // 追加 サウンド処理を実行
        switch (gameState) {
            case STATE_IDLE:{

ここまででできたこと

PWMを使用して音を出せるようにした。ボールロスの際に、ブーという音を出した。ボールが反射するときに音を出し、ゲームを続けながら音を止めることができた

次の記事に進む

Longan Nanoを使ってみる 14 ~音楽を鳴らす~

今回の追加を反映したソースコード

sound.h

#ifndef __sound_h__
#define __sound_h__

void sound_pwm_init();
void StartSound(void);
void StopSound(void);
void SoundSet(int Period);

void BallSoundInit(void);
void BallSoundStart(int sndNo);
void BallSoundStop(void);
void BallSoundTick(void);
#endif

sound.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "sound.h"

void sound_pwm_init()
{
  	timer_oc_parameter_struct timer_ocinitpara; 
	timer_parameter_struct timer_initpara;           // タイマーパラメータ構造体の宣言
	timer_deinit(TIMER4);
    timer_struct_para_init(&timer_initpara);


   	// rcu_periph_clock_enable(RCU_AF);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3);
    rcu_periph_clock_enable(RCU_TIMER4);

    timer_initpara.prescaler         = 539;                 // タイマーの16ビットプリスケーラには54MHzが入力されるので、54Mhz/540・・・
                                                            // のはずだが、108MHzが入力されるように振舞っている。
                                                            // なので108MHz/540 で、およそ200KHzが得られるハズ。
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;  // count alignment edge = 0,1,2,3,0,1,2,3... center align = 0,1,2,3,2,1,0
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;    // Counter direction
    timer_initpara.period            = 3;		            // 200KHz/4 で、おおよそ50KHzが得られるハズ。ただ、これはSoundSetで動的に書き換えられちゃうけどね。
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;    // This is used by deadtime, and digital filtering (not used here though)
    timer_initpara.repetitioncounter = 0;                   // Runs continiously	
    timer_init(TIMER4, &timer_initpara);  					// Apply settings to timer   

 	timer_channel_output_struct_para_init(&timer_ocinitpara);
    timer_ocinitpara.outputstate  = TIMER_CCX_ENABLE;        // Channel enable
    timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;      // Disable complementary channel
    timer_ocinitpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;  // Active state is high
    timer_ocinitpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;  
    timer_ocinitpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW; // Idle state is low
    timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(TIMER4, TIMER_CH_3,&timer_ocinitpara);   // Apply settings to channel

    timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0);                   // Set pulse width
    timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0);         // Set pwm-mode
    timer_channel_output_shadow_config(TIMER4,TIMER_CH_3,TIMER_OC_SHADOW_DISABLE);

    /* auto-reload preload enable */
    timer_auto_reload_shadow_enable(TIMER4);

    /* start the timer */
    timer_enable(TIMER4);
    
}

bool 	isSound = TRUE;
void StartSound(void) {
    if (isSound == FALSE) return;
    timer_enable(TIMER4);		
}
void StopSound(void) {
    if (isSound == FALSE) return;
    timer_disable(TIMER4);		
}
void SoundSet(int Period)
{
    TIMER_CH3CV(TIMER4) = Period/2;	   
    TIMER_CAR(TIMER4) =  Period;	 	
}


static bool bSound[2];          // ボールの音が出てるかのフラグ。[0]がポピ、[1]がピポ。
static int iSoundIdx=0;         // ボールの音がどこまで進んでいるか。
                                // 例えば「ポピ」を出しているなら、0でポ、1がピ。
static int iSoundStep[2][2];    // 今出している音の進行状況。例えば、「ポピ」の時であれば、
                                // [0][0]が「ポ」の間増やされていき、一定の値になったら
                                //「ピ」に変えて[0][1]の値を増やしていく
static int iSoundTim[2][2] = {{400,800},{1000,600}};		//200Hzに対して、これで分週する。400なら、500Hzになる。


void BallSoundInit(void)
{
    // 変数を初期化して
	for (u8 i = 0;i<2;i++) {
		iSoundIdx = 0;
		bSound[i] = FALSE;
		iSoundStep[i][0] = iSoundStep[i][1] = 0;
	}
	timer_disable(TIMER4);		//音を止める
}
// 音を出し始める。引数に音のインデックス「ピポ」「ポピ」を指定
void BallSoundStart(int sndNo)
{
	BallSoundStop();
	bSound[sndNo] = TRUE;

}
// 今出ている音をすべて止める。初期化と同じ処理でよい
void BallSoundStop(void)
{
    BallSoundInit();
}
// 音を出す主処理。タイマーで一定間隔に呼び出される必要がある
void BallSoundTick(void)
{
	for (u8 i = 0 ; i < 2;i++){
		if (bSound[i]) {
			if (iSoundStep[i][iSoundIdx] == 0) {		// 音のなり始め
				SoundSet(iSoundTim[i][iSoundIdx]);
				timer_enable(TIMER4);
				iSoundStep[i][iSoundIdx]++;					
			} else if (iSoundStep[i][iSoundIdx]<3) {
				iSoundStep[i][iSoundIdx]++;
			} else if (iSoundStep[i][iSoundIdx] == 3) {	// 次の音へ
				if (iSoundIdx == 1) {			// 全部の音を出し終わったら終了
					BallSoundStop();
				} else {
					iSoundStep[i][iSoundIdx] = INT16_MAX;
					iSoundIdx++;
				}
			}
		}
	}

}

game.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "block.h"
#include "paddle.h"
#include "button.h"
#include "sound.h"

enum GAMESTATE gameState;	    // ゲームの状態
volatile u8 WakeFlag = 0;       // このフラグが1になると、処理が開始される

int LifeCnt = 0;
int Score = 0;
int Stage= 1;                      // ステージ

//
// 割り込みハンドラ。タイマーにより指定した周期で非同期に呼び出される
//
void TIMER5_IRQHandler(void)
{
    if(SET == timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP)){
        timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP);
    	WakeFlag = 1;
	    
        static u8 oeFlag;
        if (oeFlag == 0) {
            gpio_bit_reset(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 1;
        } else {
            gpio_bit_set(GPIOB, GPIO_PIN_8); //OE#
            oeFlag = 0;
        }
    }
}

//
// タイマーの初期化
//
void timer5_config(int Cnt)
{
    // タイマーのパラメータを設定する。
	// タイマーの16ビットプリスケーラには54MHzが入力される・・・はずだが、108MHzが入力されるように振舞っている。
	// それを、10000分周(timer_initpara.prescaler = 10000-1)すると、おおよそ108,000,000/10000= 10.8Khz
    // それを、引数の cnt回数えて(timer_initpara.period = Cnt;)タイマーの周期を決める。
	// 例えば、cntが30の時は、10,800 / 30 =360で、360Hzとなる。
    timer_parameter_struct timer_initpara;
    rcu_periph_clock_enable(RCU_TIMER5);
    timer_deinit(TIMER5);
    timer_struct_para_init(&timer_initpara);
    timer_initpara.prescaler         = 10000 - 1;
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.period            = Cnt;
    timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
    timer_init(TIMER5, &timer_initpara);
    timer_auto_reload_shadow_enable(TIMER5);
    timer_interrupt_enable(TIMER5, TIMER_INT_UP);

    // 割り込みを有効にして、タイマー5を設定する
    eclic_global_interrupt_enable();
	eclic_set_nlbits(ECLIC_GROUP_LEVEL3_PRIO1);
	eclic_irq_enable(TIMER5_IRQn,1,0);
    
    // タイマーを開始する
    timer_enable(TIMER5);
}



// 外枠とスコア、ライフ残を表示する
void DrawBORDER()
{
	LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y1,GAMEAREA_X0,GAMEAREA_Y0,WHITE);
	LCD_DrawLine(GAMEAREA_X0,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y0,WHITE);
	LCD_DrawLine(GAMEAREA_X1,GAMEAREA_Y0,GAMEAREA_X1,GAMEAREA_Y1,WHITE);

	LCD_ShowString(0,0,(const u8 *)"SCORE:",WHITE);
	LCD_ShowString(10,160-12,(const u8 *)"LIFE:",WHITE);
	char life[3];
	sprintf(life,"%1d",LifeCnt);
	LCD_ShowString(55,160-12,(u8 *)life,WHITE);
	u8 scr[12];
	sprintf((char *)scr,"%5d0",Score);
	LCD_ShowString(38,0,scr,WHITE);	
}


// 座標が、矩形の外側に対して、どの象限にいるのかを返す関数
//    1         2        3
//        +---------+
//   4    |    5    |    6
//        +---------+
//   7         8         9    
unsigned char GetOrthant(int x , int y , int x1, int y1 , int x2 , int y2)
{
	bool bLowerX1 =  (x < x1);
	bool bUpperX2 =  (x > x2);
	bool bLowerY1 =  (y < y1);
	bool bUpperY2 =  (y > y2);

    
	if (bLowerX1) {
        return bLowerY1 ? 1: (bUpperY2 ? 7:4);
	} else if (bUpperX2) {
        return bLowerY1 ? 3: (bUpperY2 ? 9:6);
	} else {
        return bLowerY1 ? 2: (bUpperY2 ? 8:5);
	}
}


//
// メイン処理
//
void Game(bool isDemo) 
{
    rcu_periph_clock_enable(RCU_GPIOB);
    gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);	// B8をデバッグに使う

    // 初期化処理
    gameState = STATE_INIT;             // ステータスを初期化にする
    timer5_config(100);                  // タイマーの初期化を行う
    LCD_Init();
    LCD_Clear(BLACK);
	Adc_init();
    sound_pwm_init();

    BallSoundInit();                    // ボール音の初期化


    u16 tick = 0;                       // LEDを点滅させるためのカウンターを初期化
    gameState = STATE_IDLE;             //初期化処理が終了したのでゲーム開始処理を行う
    static u16 heartBeat = 0;            // 状態遷移ループのカウンタ
    
    while (TRUE) { 
		timer_enable(TIMER5);               // タイマーを有効にする
        // タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
		
        while(WakeFlag == 0) {
            delay_1ms(10);
            break;
        }

        tick = (tick + 1) & 0x8FFF;     // LEDの点滅用カウンタのインクリメント
		WakeFlag = 0;                   // タイマーのウエイトフラグを初期化する

        heartBeat = (heartBeat+1) & 0x7FFF;
        
        BallSoundTick();                // サウンド処理を実行
        switch (gameState) {
            case STATE_IDLE:{
                u16 cnt = heartBeat & 0xFF;
                if (cnt == 0x00) {
                    LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",WHITE);
                    LCD_ShowString(5,60,(const unsigned char *)" TO START  ",WHITE);
                } else if (cnt == 0x80) {
                    LCD_ShowString(5,40,(const unsigned char *)"PUSH BUTTON",RED);
                    LCD_ShowString(5,60,(const unsigned char *)" TO START  ",RED);
                }
                if (CheckP1Button()) {
                    LCD_Clear(BLACK);
                    Score = 0;
                    LifeCnt = 3;
                    Stage = 1;
                    InitBlock(Stage);
                    gameState = STATE_STARTGAME;
                }
                break;
            }
            case STATE_STARTGAME:{
                /*ゲームの開始処理 */

                DrawBORDER();               //外枠とライフ残、スコアを画面に表示させる
                DrawBlock();
                InitBallPos(0,NULL);
				InitPaddle();
                gameState = STATE_INGAME;
                break;
            }
            case STATE_INGAME:{
        		drawDeleteBall(FALSE);			// ひとつ前のボールを消す
	            u8 ret= moveBall();
	            if (ret == 0) {                 // すべてのボールがなくなったら
                    LifeCnt--;
                    gameState = STATE_BALLLOSS; // ボールロスの状態に遷移させる
                    break;
                } else if (ret == 2) {          // ブロックがすべてなくなったら
                    gameState = STATE_NEXTSTAGE;
                    break;
                }
        		drawDeleteBall(TRUE);			// ひとつ前のボールを消す
				breakout_PaddleCtrl(isDemo);			// パドルを動かす
                break;                
            }
            case STATE_NEXTSTAGE:{
                static u16 waitNextCnt = 0;

                // 画面に"WELL DONE!"と表示させる
                LCD_ShowString(5,80,(const unsigned char *)"WELL DONE!",WHITE);
                // 一定時間が経過するか、ボタンが押されたらブロックを初期化してゲームの再開に遷移する
                waitNextCnt++;
                if (waitNextCnt == 0x100 || CheckP1Button()) {
                    LCD_Clear(BLACK);
                    waitNextCnt = 0;
                    Stage++;
                    InitBlock(Stage);                    
                    gameState = STATE_STARTGAME;
                }
               break;                
            }
            case STATE_BALLLOSS:{               // ボールロス
                static u16 waitCnt = 0;

                // ライフがないならゲームオーバーに遷移
                if (LifeCnt == 0) {
                    gameState = STATE_GAMEOVER;
                    break;
                }
                // 画面に"-- MISS! --"と表示させる
                if (waitCnt == 0) {
                    LCD_ShowString(5,80,(const unsigned char *)"-- MISS! --",WHITE);
                    SoundSet(800);
                    StartSound();
                }
                // 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
                waitCnt++;
                if (waitCnt == 0x100 || CheckP1Button()) {
                    LCD_Clear(BLACK);
                    waitCnt = 0;
                    StopSound();
                    gameState = STATE_STARTGAME;
                }
               break;
            }
            case STATE_GAMEOVER:{
                /*ゲーム-オーバー処理*/
                static u16 waitGameOverCnt = 0;
                // 画面に"-- MISS! --"と表示させる
                LCD_ShowString(5,80,(const unsigned char *)"-GAMEOVER-",WHITE);
                // 一定時間が経過するか、ボタンが押されたらゲームの再開に遷移する
                waitGameOverCnt++;
                if (waitGameOverCnt == 0x100 || CheckP1Button()) {
                    LCD_Clear(BLACK);
                    waitGameOverCnt = 0;
                    gameState = STATE_IDLE;
                }                
                break;
            }
        }

    }
}

ball.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "wall.h"
#include "block.h"
#include "paddle.h"
#include "sound.h"



// ボールの角度を示すデフォルト値と、角度の最大・最小値
#define BALL_DX_DEFAULT 1*2
#define BALL_DY_DEFAULT 2*2
#define BALL_DX_MIN  (1*2)
#define BALL_DX_MAX  (4*2)
#define BALL_DY_MIN  (1*2)
#define BALL_DY_MAX  (4*2)

//ボールの座標
struct BALLINFO ball[5];             // 最大5個のボールが出現するので配列にしておく

// ボールの情報にアクセスするための関数
struct BALLINFO* GetBallInfo(unsigned int idx)
{
    if (ball[idx].x == 0 && ball[idx].y == 0) return NULL;
    return &ball[idx];
}


u8 ballLive = 0;       // ボールの数




//ボールを1つ進める
void BallStep(struct BALLINFO *bi) 
{
	bi->oldx = bi->x;
	bi->oldy = bi->y;
	bi->x += bi->dx;
	bi->y += bi->dy;
}
//ボールを1つ前の位置に戻す
void BallBack(struct BALLINFO *bi) 
{
	bi->x = bi->oldx;
	bi->y = bi->oldy;
}
// ボールのdxを逆にする
void BallSwapX(struct BALLINFO *bi) 
{
    bi->dx = -bi->dx;
    bi->dxBase = -bi->dxBase;
}
// ボールのdyを逆にする
void BallSwapY(struct BALLINFO *bi) 
{
    bi->dy = -bi->dy;
    bi->dyBase = -bi->dyBase;
}
// ボールが失われた時の処理
void BallDead(struct BALLINFO *bi) 
{
	if (bi->x != 0) {
		ballLive--;
	}
	bi->oldx = 0;
	bi->oldy = 0;
	bi->x = 0;
	bi->y = 0;
}
// 生きているボールの数を返す
int GetBallCount()
{
	int ballCount = 0;
	for (u8 i = 0 ; i < 5;i++) {
		if (ball[i].x != 0 && ball[i].y != 0) {
			ballCount++;
		}
	}
	return ballCount;
}

// ボールの速度を調整する

void updateBallSpeed(struct BALLINFO *bi)
{
	int speedlvl = 1;
	/*
	if (bi->SpeedMask & SPDMSK_BACKWALL) speedlvl += 1; 
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_1) speedlvl += 1;
	if (bi->SpeedMask & SPDMSK_BLOCKCNT_2) speedlvl += 1;
	*/
	bi->dx = bi->dxBase * speedlvl;
	bi->dy = bi->dyBase * speedlvl;
}


// ボールを初期化する
void InitBallPos(u8 mode, struct BALLINFO *bi)
{
	if (mode ==  0) {		// 完全初期化して最初のボールを生きにする。 ballIdxは使われない
		memset(ball,0,sizeof(ball));
		ball[0].x = (GAMEAREA_X0 + GAMEAREA_X1/2)*8;
		ball[0].y = (GAMEAREA_Y0 + (GAMEAREA_Y1-GAMEAREA_Y0)/2)*8;
		ball[0].dxBase = BALL_DX_DEFAULT;
		ball[0].dyBase = BALL_DY_DEFAULT;
		updateBallSpeed(&ball[0]);
		ballLive=1;	
	} else if (mode == 1)  { // ballIdxの位置を元にしてボールを1つ追加する。 ボールの速度・角度は元のボールと変える。
		for (int j = 1; j < 5;j++ ) {
			if (ball[j].x == 0) {			// このボールで行こう
				ball[j].x = bi->x;
				ball[j].y = bi->y;
				ball[j].dyBase = BALL_DY_DEFAULT;
				ball[j].dx = -bi->dx;
				ball[j].dxBase = -bi->dxBase;
				ball[j].SpeedMask = 0;
				updateBallSpeed(&ball[j]);
				ballLive++;
				break;
			}
		}
	}
}

//
// ボールを消す、または表示する
//  true... 表示する、false ...消す
void drawDeleteBall(bool isDraw)
{
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];
		if(bi->x !=0) {
			u16 c;
			if (isDraw == FALSE) {
                c = BLACK;
			} else {
                c = WHITE;
			}
			LCD_Fill(CVT_AXIS(bi->x)-1 ,CVT_AXIS(bi->y)-1 ,CVT_AXIS(bi->x)+1,CVT_AXIS(bi->y)+1,c);

		}
	}
}


void CheckPaddle(struct BALLINFO *bi)
{
	int chkx = CVT_AXIS(bi->x);
	int chky = CVT_AXIS(bi->y);
	for (int j=0 ; j<2;j++) {
		struct PADDLEINFO* pi = GetPaddleInfo(j);
		if (pi == NULL) continue;
		u8 pos = GetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
		if (pos == 5) {
			//ひとつ前のボール座標が、パドルのどこにあったかを求める。
			chkx = CVT_AXIS(bi->oldx);
			chky = CVT_AXIS(bi->oldy);
			u8 prevpos = GetOrthant(chkx,chky,pi->x1,pi->y1,pi->x2,pi->y2);
			// ボールの新しい位置は、パドルの内側なので、ボールの座標をもとに戻さないといけない
			BallBack(bi);
			
			// パドルにボールが反射する処理
			if (prevpos == 2 || prevpos == 8) {     // パドルの長径に当たった場合、y座標を反転
				bi->dyBase = -bi->dyBase;
				u8 pdlcx = pi->x1  + pi->Width/2;
				u8 ballpdldif = abs(chkx - pdlcx);
				
				if (ballpdldif > ( pi->Width/2) / 3) {	// 端だったら角度を増やす
					int cx = pi->x1 + pi->Width / 2;	// パドルの中央位置
					int ballX = CVT_AXIS(bi->x);		// ボールの位置


					if (bi->dxBase > 0) {					// ボールは右に移動中
						bi->dxBase += (ballX > cx) ? 1 : -1;
					} else {							// ボールは左に移動中
						bi->dxBase += (ballX > cx) ? -1 : 1;
					}

					if (abs(bi->dxBase) < BALL_DX_MIN) {
						bi->dxBase = bi->dxBase > 0 ? BALL_DX_MIN : -BALL_DX_MIN;
					} else if (abs(bi->dxBase) > BALL_DX_MAX) {
						bi->dxBase = bi->dxBase > 0 ? BALL_DX_MAX : -BALL_DX_MAX;
					}
				} 
			} else if (prevpos == 4 || prevpos == 6) {		// パドルの横に当たった時
				// x反転
				bi->dxBase = -bi->dxBase;

			} else  {										// それ以外。パドルの角に当たった時
				bi->dyBase = -bi->dyBase;
				bi->dxBase = -bi->dxBase;						
			} 
			// ポピ音を出す
			BallSoundStart(1);
		}
	}
}

//  ボールを動かす。
// 0... ミス
// 1... 継続
// 2... クリア
unsigned char moveBall()
{
	
	for (u8 i = 0 ; i < 5;i++) {
		struct BALLINFO *bi = &ball[i];		
		if (bi->x == 0) continue;

		//ボールを動かす
        BallStep(bi);
		// 壁反射チェック
		{
			u8 ret = checkWall(bi);
			if (ret == 0) {
				return 0;
			} else if (ret == 2) {
				continue;
			}
		}


		//ブロック反射チェック
		// ボールの進行方向の隅がブロックに接しているかを調べる
		blockCheck(bi);
		if (blkBrk == 0) {
			return 2;
		}
		CheckPaddle(bi);
    	updateBallSpeed(bi);
    }
	return 1;
}

block.c

#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
#include "gd32vf103.h"
#include "game.h"
#include "ball.h"
#include "wall.h"
#include "block.h"
#include "paddle.h"
#include "sound.h"


// ブロックのテーブル
// 画面上はBLOCK_CNT_H x BLOCK_CNT_Vのマス目に分類され、そこにあるブロックの
// 種類が item に入っている。
// テーブルには、事前にこのブロックが存在する矩形の情報が入っており、ボールとの
// 衝突判定ではこの座標が使われる。
struct BLOCKINFO blockmtx[BLOCK_CNT_H][BLOCK_CNT_V];

//全体のブロック数と、残りのブロック数。ブロックが少なくなったらボールの速度を上げる、などに使用する。
int blkCnt = 0;			// 総ブロック数
int blkBrk = 0;			// 残りブロック数


// ブロックのテーブルを初期化する
//
void InitBlock(int Stage)
{
	// ステージごとのブロック位置
	static int BlockStart[] = {3,4,5,2,2,1,0,0};
	static int BlockEnd[] =   {6,7,8,7,8,9,8,9};

	int stageWork = (Stage-1) & 0x7; // ステージは0~7を繰り返す
    // ブロックテーブルを初期化する。
	memset((void *)blockmtx,0,sizeof(blockmtx));
    blkCnt = 0;

    for (int i = 0;i<BLOCK_CNT_H;i++) {
        for (int j = BlockStart[stageWork];j<BlockEnd[stageWork];j++) {
            blockmtx[i][j].item = 1;
	        blockmtx[i][j].x1 = i * BLOCK_SIZE_W + (GAMEAREA_X0 + 2);
	        blockmtx[i][j].y1 = j * BLOCK_SIZE_H + (GAMEAREA_Y0 + 2);
	        blockmtx[i][j].x2 = blockmtx[i][j].x1 + BLOCK_SIZE_W  - 2 ;
	        blockmtx[i][j].y2 = blockmtx[i][j].y1 + BLOCK_SIZE_H  - 3 ;            
            blkCnt++;
        }
    }
    blkBrk = blkCnt;
}

// ブロックすべてを描画する
// ブロック崩しでは、ブロックが1つだけ表示される、ということはない(消えていくだけ)ので、
// 表示は無条件で全ブロックを表示させれば良いことになる。
void DrawBlock()
{
	static u16 colTbl[]  = {RED,  BLUE,     GREEN     ,MAGENTA,CYAN,     YELLOW};

	for (u8 i = 0 ; i< BLOCK_CNT_H;i++) {
		for (u8 j = 0; j<BLOCK_CNT_V;j++) {
			u16 col = colTbl[j % 6];
			if (blockmtx[i][j].item == 1) {
				LCD_Fill(blockmtx[i][j].x1,blockmtx[i][j].y1,blockmtx[i][j].x2,blockmtx[i][j].y2,col);
			}
		}
	}
	

}

//
// ブロック反射チェック
// この時点では、BALLINFOの座標は移動済みの座標になっている。
//
void blockCheck(struct BALLINFO* bi)
{
	// 一つ前の座標
	int chkx = CVT_AXIS(bi->oldx);
	int chky = CVT_AXIS(bi->oldy);
	// 座標がある位置のブロック番号を求める
	int xNow = CVT_AXIS(bi->x);
	int yNow = CVT_AXIS(bi->y);
	int xidx = (xNow - (GAMEAREA_X0+2)) /BLOCK_SIZE_W;
	int yidx = (yNow - (GAMEAREA_Y0+2)) / BLOCK_SIZE_H;

	// 現在のボールの位置が、ブロックの中にある場合、衝突処理を行う
	if (blockmtx[xidx][yidx].item != 0 && GetOrthant(xNow,yNow, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2) == 5) {
		blkBrk--;							// 残ブロック数を1つ減らす
		// ボールの新しい位置は、ブロックの内側なので、座標はひとつ前の位置に戻さないといけない。
		BallBack(bi);

		// 難易度調整
		if (blkBrk <= (blkCnt / 2)) {				// 残ブロックスが全ブロック数の半分以下になったら
			bi->SpeedMask  |= SPDMSK_BLOCKCNT_1;	// スピードレベル1
		} 
		if (blkBrk <= (blkCnt / 4)) {				// 残ブロックスが全ブロック数の1/4分以下になったら
			bi->SpeedMask |= SPDMSK_BLOCKCNT_2;		// スピードレベル2
		}

		// ボールを跳ね返す。
		u8 pos = GetOrthant(chkx,chky, blockmtx[xidx][yidx].x1 ,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2);
		if (pos == 2 || pos== 8) {
            BallSwapY(bi);
		} else if (pos == 4 || pos == 6) {
            BallSwapX(bi);
		} else {
            BallSwapX(bi);
            BallSwapY(bi);
		}

		// ブロックは消す
		blockmtx[xidx][yidx].item = 0;	 
		LCD_Fill(blockmtx[xidx][yidx].x1,blockmtx[xidx][yidx].y1,blockmtx[xidx][yidx].x2,blockmtx[xidx][yidx].y2,BLACK);

		// スコアの処理
		if (bi->SpeedMask & SPDMSK_BACKWALL) {		// 裏に入っていたらスコアは増量
			Score = Score + 2;
		} else {
			Score = Score + 1;
		}
		// ピポ音を出す
		BallSoundStart(0);

	}
}

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?