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

# 前の記事
[Longan Nanoを使ってみる 13 ～ステージの遷移とゲームオーバー](https://qiita.com/BUBUBB/items/f1ae81b835f55abe319e)

# 全体の目次
[Longan Nanoを使ってみる 1 ～ビルド環境の構築～](https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d)
[Longan Nanoを使ってみる 2 ～デバッガの環境設定～](https://qiita.com/BUBUBB/items/864b2ef9404453810d5f)
　[Sipeed RISC-V Debugger](https://qiita.com/BUBUBB/items/61316a4df1d9f81e2864)
[Longan Nanoを使ってみる 3 ～デバッガの使用方法～](https://qiita.com/BUBUBB/items/8efa5989e7f16f67672e)
[Longan Nanoを使ってみる 4 ～printfを使ったデバッグ～](https://qiita.com/BUBUBB/items/b607a3eedc4b66e7a74f)
[Longan Nanoを使ってみる 5 ～ゲームのプロジェクトを作成～](https://qiita.com/BUBUBB/items/98d9c25120c0955d5200)
[Longan Nanoを使ってみる 6 ～文字出力～](https://qiita.com/BUBUBB/items/8dee2eabe78f38364674)
　[Longan Nanoを使ってみる ～FONTX2ファイルを作る～](https://qiita.com/BUBUBB/items/8760ac886423bf06be68)
[Longan Nanoを使ってみる 7 ～外枠とブロックを書く～](https://qiita.com/BUBUBB/items/96abb1158b413c9efd27)
　[Longan Nanoを使ってみる ～謎の画像表示関数～](https://qiita.com/BUBUBB/items/40b99468839e1daeebfb)
[Longan Nanoを使ってみる 8 ～ボールを動かす～](https://qiita.com/BUBUBB/items/92f4816af3c41ea13b9e)
[Longan Nanoを使ってみる 9 ～A/Dコンバータから入力～](https://qiita.com/BUBUBB/items/311c179f7d3a0e30495d)
[Longan Nanoを使ってみる 10 ～パドルを動かす～](https://qiita.com/BUBUBB/items/eb8149c68f6e5456e7ca)
[Longan Nanoを使ってみる 11 ～ボタンの入力](https://qiita.com/BUBUBB/items/2445940a6b0d2110ee04)
[Longan Nanoを使ってみる 12 ～ボールのロス～](https://qiita.com/BUBUBB/items/f8c6ff860de127c93cb9)
[Longan Nanoを使ってみる 13 ～ステージの遷移とゲームオーバー～](https://qiita.com/BUBUBB/items/f1ae81b835f55abe319e)
[Longan Nanoを使ってみる 14 ～PWMとサウンド～](https://qiita.com/BUBUBB/items/3b592078d30cfed0ec53)
[Longan Nanoを使ってみる 15 ～音楽を鳴らす～](https://qiita.com/BUBUBB/items/adb35e02209ad78dfa9b)
[Longan Nanoを使ってみる 16 ～とりあえずのまとめ～](https://qiita.com/BUBUBB/items/ba782ccc80f50b7ab8c3)

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

今回は、圧電スピーカー（圧電サウンダ）を、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Ω程度。家にあるので良い。
- 半田ごて、線材など

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

- 圧電スピーカー[こんなの](https://akizukidenshi.com/catalog/g/gP-04118/)や、[こんなの](https://www.aitendo.com/product/12588)。

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


# 接続回路

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

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

![sch3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2096509/999c96ac-54f1-5da4-bf57-7e05db796e79.png)


# PWMの使い方
## 概要

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

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

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

<img width=60% alt="PWM1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2096509/4ae2f1ad-7e71-0318-617d-36a5fe54edb8.png">

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

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

<img width=60% alt="PWM3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2096509/11189eb2-c011-c714-3f6a-1f560aefeb4e.png">



具体的な動作として、PWMは、プリスケーラ（[タイマーの使い方](https://qiita.com/BUBUBB/items/98d9c25120c0955d5200#%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)でも設定した)で指定した分周比で動くカウンターを使用し、次のような動作を繰り返す。

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

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

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

<img width=75% alt="pwm2.png" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2096509/8946a9e6-29fc-358d-64ce-f08c41f421ea.png">

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

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



## 初期化

PWMを使うには、まずタイマーを用意し、一定間隔でカウンタを回す必要がある。これは、[タイマーの使い方](https://qiita.com/BUBUBB/items/98d9c25120c0955d5200#%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)と同じ処理となる。

- メインの処理を回すのにタイマー割込みで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;                // 繰り返しカウンタ。毎回イベントが発生するよう０を指定	
timer_init(TIMER4, &timer_initpara);  			　　　// Apply settings to timer   

```
この例では、プリスケーラによって200KHzごとにカウンタが＋１されていき、４まで上がったら０に戻る。つまり、200KHzの４回に１回の割合、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のチャンネル３に適用する
```

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

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

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

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

```
timer_channel_output_pulse_value_config(TIMER4,TIMER_CH_3,0);    // デューティ比を指定する。後でどうせ指定するので、ここでは０を指定している。意味がないが説明のため残す。
timer_channel_output_mode_config(TIMER4,TIMER_CH_3,TIMER_OC_MODE_PWM0);     // TIMER4のチャンネル３をPWMモード０として使用することを宣言、
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%)を指定し、タイマー４を作動させればよい。

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

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

例えば、ユーザーマニュアルで次のようになっていたとする。
![PWM4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2096509/5dcd3f52-7b94-bdc2-fd2a-da5e3eae5ea5.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;	 	
```

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

```
    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の音が出力されることになる。


# 効果音の出力

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

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


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

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

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

```

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



#### 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 ～音楽を鳴らす～](https://qiita.com/BUBUBB/items/adb35e02209ad78dfa9b)

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

#### 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;       // このフラグが１になると、処理が開始される

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);

    // 割り込みを有効にして、タイマー５を設定する
    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が割り込みルーチン内で１になるまで無限ループする
		
        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の位置を元にしてボールを１つ追加する。　ボールの速度・角度は元のボールと変える。
		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;
}

// ブロックすべてを描画する
// ブロック崩しでは、ブロックが１つだけ表示される、ということはない（消えていくだけ）ので、
// 表示は無条件で全ブロックを表示させれば良いことになる。
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--;							// 残ブロック数を１つ減らす
		// ボールの新しい位置は、ブロックの内側なので、座標はひとつ前の位置に戻さないといけない。
		BallBack(bi);

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

	}
}

```