マイコン入門 PWMによる圧電スピーカーからの音の出し方

マイコン
この記事は約20分で読めます。

こんにちは、ナナです。

ビュートローバーには圧電スピーカーが搭載されています。本章ではスピーカーから特定の音を出力する方法を学びます。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • 音っていったい何なの?
  • スピーカーから音ってどうして聞こえるの?
  • 圧電スピーカーで音を作るってどうやってやるの?
  • PWMって何?
  • タイマ機能とPWMの関係性とは?
  • PWMってどうやって使うの?

では、スピーカーによる音の制御の仕組みを学んでいきましょう。

スポンサー

スピーカーから音が聞こえるのはなぜ

音というものがそもそも何なのか考えたことありますか?音を知らずして音は制御できません。まずは音が何なのかを知っていきましょう。

音が聞こえるとは

音が聞こえるとは、音の振動が空気を通じて皆さんの耳に届いているということです。

音が聞こえる

つまり、スピーカーから音を出すとは音の振動を如何に作り出すかということになります。

音の違い

ドレミといった音はなぜ異なる音として聞こえるかというと空気振動の周波数が異なるのです。

音の違い

この周波数の異なる空気振動をスピーカーから発生させることができれば、ドレミの音を出すことが可能です。

スポンサー

圧電スピーカーによる音の作り方

圧電スピーカーで音を出力することが本章の目的です。では、圧電スピーカーから音を出力するとはいったいどのような制御をすればよいのでしょうか。

圧電スピーカーの回路図

回路図を見てみましょう。圧電スピーカーは回路図においてやはりI/Oポートの「P76」に接続されています。これはポート制御により音を出すことができるということを意味しています。

スピーカー回路図

LED制御の章を思い出してください。I/Oポートといえば電圧をHighとLowに切り替えができるのでした。圧電スピーカーで音を作るとは、音の波形をポート電圧のHigh/Lowで再現するということなのです。

ポート制御による音の作成

皆さんはI/Oポートの電圧をHigh/Lowに切り替える術をすでに学んでいますね。圧電スピーカーにつながるポート電圧の変化は、そのまま圧電スピーカーに入力され電圧波形が空気振動に変換されることになります。

圧電スピーカーから音を出すとは

つまり、ドレミの音を圧電スピーカーから出すためには、ドレミの空気振動の波形をポート電圧で再現すればよいということになります。

スピーカーからドレミ
スポンサー

PWM機能を使ったスピーカー制御

I/Oポートの電圧を一定リズムでHigh/Lowすれば音がでることはわかりました。しかし、ドの周波数に従い皆さんがポートの電圧をタイミングに合わせてHigh/Lowし続けるプログラムを作るって面倒ですよね。

そんな時に利用できる機能がPWM機能です。

I/OポートとTMOV

圧電スピーカーが接続されている「P76」をデータシートで確認するとP76/TMOVと並列表記がされています。このTMOVこそがタイマVのPWM機能につながっているのです。

ポート7

タイマVのPWM機能を利用するとポートの出力電圧を周期的に自動制御することができます。

PWM機能とは

PWM機能とはパルス幅変調(Pulse Width Modulation)という機能です。名前が何を意味しているか分かりづらいですよね。最初にこの言葉を聞いた人は何の機能なのかわからないのが普通ですので安心してください。

PWM機能を使うと何ができるかというと、依頼内容に従ってポートから一定リズムのHigh/Low電圧波形を自動で出力できるようになります。今回は音という周波数の電圧波形を周期的にポートから出力する必要があるので、うってつけの機能ですね。

PWM機能の動作イメージ

PWM機能は周期的に電圧を変化させるという特徴を持つことから、周期という時間を制御できるタイマ機能に付属することがほとんどです。PWMに対応したタイマ機能かどうかはデータシートを確認すればわかります。

圧電スピーカーをPWM機能で制御することはハード設計者も理解してるため、PWM機能が利用可能なポートに接続する回路設計を行ってくれる!

スポンサー

スピーカーから音を出すためのPWM機能の使い方

タイマVはデータシート「11章 タイマV」に記載されています。全10ページ程度でありタイマB1機能よりは高機能なタイマ機能です。

特徴

「11.1章 特徴」にタイマV機能の特徴が書かれています。

  • 7種類のクロックを選択可能
  • カウンタのクリア指定が可能
  • 2つのコンペアマッチ信号の組合せでタイマ出力を制御
  • 3種類の割り込み要因
  • トリガ入力によるカウント開始機能

特殊な用語が出てきますので解説しましょう。PWMに関する機能も併せて解説します。

カウンタのクリア指定

タイマVにはタイマカウンタがとあるレジスタの値と一致するとタイマカウンタを自動的に0に初期化する機能が付いています。それがクリア指定という用語の意味です。PWM機能において欠かせない機能です。

コンペアマッチ

コンペア=比較、マッチ=一致の意味であり、比較して一致したかを判定する機能をコンペアマッチと呼びます。

何と比較するのかというとタイマカウンタ用のレジスタと、クリア指定用のレジスタです。この機能を利用することでPWM波形のHigh/Lowのタイミングを調整します。

タイマ出力

マイコンのピンであるTMOV端子から出力される電圧のことです。タイマVではPWM機能を利用してポート電圧を出力することができます。

デューティ比

特徴の中には出てこない用語ですが、PWM機能といえばデューティ比です。PWM機能では自由な矩形型の波形を作り出すことができますが、周期に対する電圧Highの割合のことをデューティ比と呼び、%で表現します。

デューティ比  : 電圧High幅 ÷ 周期幅

例えば次のような周期の波形の場合は50%と20%がデューティ比となります。

デューティ比

音の場合はデューティ比を50%に設定します。しかし、波形によってはHighとLowの幅が異なる波形も存在するということです。

レジスタ構成

データシート「11.3章 レジスタの説明」にタイマVのレジスタ一覧があります。

  • タイマカウンタV(TCNTV)
  • タイムコンスタントレジスタA(TCORA)
  • タイムコンスタントレジスタB(TCORB)
  • タイマコントロールレジスタV0(TCRV0)
  • タイマコントロール/ステータスレジスタV(TCSRV)
  • タイマコントロールレジスタV1(TCRV1)

タイマB1機能と比べると多くのレジスタがあります。今回の課題ではタイマVをPWM機能として使うためのレジスタを解説します。

タイマカウンタV(TCNTV)とは

クロックをカウントするレジスタ。コンペアマッチはこのレジスタとの比較にて実施される。リードもライトも可能なレジスタのためプログラムから値を変更することも可能です。

タイムコンスタントレジスタA、B(TCORA、TCORB)とは

PWMのHigh/Lowを切り替えるタイミングを決定するレジスタ。この2つのレジスタ値にHigh/Lowを切り替えたいタイマカウント値を設定することで任意のデューティー比の波形を作り出すことができます。

タイマコントロール/ステータスレジスタV(TCSRV)とは

このレジスタで大事なビットはOS0~OS3のアウトプットセレクタになります。タイムコンスタントレジスタのコンペアマッチが成立したときに電圧をHighにするかLowにするかを決定する役割があります。

2ビット単位でTCORAとTCORBに関連づいており、PWMの波形を出力するためには0出力と1出力のどちらかを異なるように設定をします。

タイマコントロールレジスタV0、V1(TCRV0、TCRV1)とは

CCLR0~1をまず決める必要があります。このビットはTCNTVがTCORAもしくはTCORBのどちらと一致したときにカウンタをクリアするかを決定するレジスタです。

CKS0~2とICKS0の4つのビットを設定することで分周を決定します。CKS0~2の値をB’000にすることでタイマカウントアップを停止させる機能も持っています。音の出力を止めるためにはカウントアップを停止させる機能を使います。

PWM機能と出力波形の関係性

レジスタの関係が複雑なためまとめておきましょう。一例ですが次のように設定した場合の出力波形を図で示します。

設定例

  • TCORA:周期を示すタイマカウンタ値設定
  • TCORB:デューティ比を決定するタイマカウンタ値設定
  • TCRV0.CCLR0~1:B’01(TCORAと一致でTCNTVクリア)
  • TCSRV.OS0~1:B’01(TCORAと一致で出力電圧Low)
  • TCSRV.OS2~3:B’10(TCORBと一致で出力電圧High)
レジスタ設定例による波形出力

レジスタの設定次第で様々な周期的な波形を出力することができます。それがPWM機能なのです。

スポンサー

PWMで音を生成するSpkモジュールの作成

PWMの制御方法がわかりましたのでプログラムの作成を行います。いつもの手順でspk.cとspk.hをプロジェクトに登録してください。

Spkモジュールの役割検討

スピーカーモジュールでは次の機能を実装したいと思います。

  • 音としてドレミファソラシドの8つの音の種類を出力できるようにする。
  • 初期化処理ではPWM機能を動作させるのに必要な処理を行う。
  • 分周はφ/128を選択する。
  • 音のデューティー比は50%でよいものとする。
  • 他のモジュールから音階を指定し音を出力できるインターフェースを用意する。音は停止要求があるまで出力し続ける
  • 他のモジュールから音の出力を停止するインターフェースを用意する。

出力するドレミファソラシドの音階は次の周波数とし、128分周を選択したときのタイマカウンタ値は次の値に従うものとします。

音階のタイマカウント値

では、これらを満たすインターフェース仕様を検討します。

Spkモジュールのインターフェース仕様書

1.提供ヘッダファイル名

#include “spk.h”

2.定数定義

音階定数定義仕様

3.インターフェース定義

3.1 初期化

Spk_init関数仕様

3.2 音の出力開始

Spk_start関数仕様

3.3 音の出力停止

Spk_stop
スポンサー

課題:Spkモジュールを使って音を出してみよう

課題1

課題内容

インターフェース仕様に従いスピーカーモジュールを作成せよ。spk.cとspk.hは次のプログラムをベースにして修正を加えよ。

spk.h

#ifndef SPK_H
#define SPK_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------


//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------


//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------


//------------------------------------------------
#endif  // SPK_H

spk.hに追記すべきこと

  • 列挙型enumを使用し音階定数を定義せよ。
  • インターフェースのプロトタイプ宣言を追加せよ

spk.c

#include "iodefine.h"
#include "spk.h"

//------------------------------------------------
//  概 要:初期化
//  引  数:なし
//  戻り値:なし
//------------------------------------------------
void Spk_init(void)
{

}

//------------------------------------------------
//  概 要:音の出力開始
//  引  数:scale	音階の指定
//  戻り値:0	正常
//			-1	異常
//------------------------------------------------
int Spk_start(E_SPK_SCALE scale)
{

}

//------------------------------------------------
//  概 要:音の出力停止
//  引  数:なし
//  戻り値:0	正常
//			-1	異常
//------------------------------------------------
int Spk_stop(void)
{

}

spk.cに追記すべきこと

  • Spk_init関数ではPWM機能を動かすのに必要な処理を実施。ただしタイマカウントはまだ開始しない。
  • Spk_start関数では引数で指定された音階に従いタイムコンスタントレジスタA/Bの設定を行う。タイマカウントを開始する。
  • Spk_stop関数ではタイマカウントを停止する。

spk.h

#ifndef SPK_H
#define SPK_H
//------------------------------------------------

//------------------------------------------------
//  マクロ定義(Macro definition)
//------------------------------------------------


//------------------------------------------------
//  型定義(Type definition)
//------------------------------------------------
typedef enum {
    E_SPK_SCALE_DO  = 0, //  ド
    E_SPK_SCALE_RE,      //  レ
    E_SPK_SCALE_MI,      //  ミ
    E_SPK_SCALE_FA,      //  ファ
    E_SPK_SCALE_SO,      //  ソ
    E_SPK_SCALE_RA,      //  ラ
    E_SPK_SCALE_SI,      //  シ
    E_SPK_SCALE_DO2,     //  ド

    //----------------
    E_SPK_SCALE_END,     //  終端判別用の番兵
} E_SPK_SCALE;

//------------------------------------------------
//  プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void Spk_init(void);
int Spk_start(E_SPK_SCALE scale);
int Spk_stop(void);

//------------------------------------------------
#endif  // SPK_H
  • 列挙型を使った定数定義を行う。また、インターフェース仕様書に従いプロトタイプ宣言を追加する。

spk.c

#include "iodefine.h"
#include "spk.h"

//------------------------------------------------
//  グローバル変数
//------------------------------------------------
//  音階のタイマカウンタ値(128分周時)
const static unsigned char gScaleTable[] =
{
    179,    //  ド
    160,    //  レ
    142,    //  ミ
    134,    //  ファ
    120,    //  ソ
    107,    //  ラ
    95,     //  シ
    90,     //  ド
};

//------------------------------------------------
//  概 要:初期化
//  引  数:なし
//  戻り値:なし
//------------------------------------------------
void Spk_init(void)
{
    TV.TCRV1.BIT.ICKS   = 1;    //  128分周設定予定
    TV.TCRV0.BIT.CKS    = 0;    //  タイマカウント停止
    TV.TCRV0.BIT.CCLR   = 1;    //  コンペマッチAでカウンタクリア

    TV.TCSRV.BIT.OS     = 9;    //  出力形式
    //    コンペアマッチBでポートHigh
    //    コンペアマッチAでポートLow
}

//-------------------------------------------------
//  概 要:音の出力開始
//  引  数:scale   音階の指定
//  戻り値:0   正常
//          -1  異常
//-------------------------------------------------
int Spk_start(E_SPK_SCALE scale)
{
    //  指定音階の範囲異常チェック
    if (scale < 0 || scale >= E_SPK_SCALE_END)
    {
        return -1;
    }

    //  デューティ比50%のクロック生成;
    TV.TCORA = gScaleTable[scale];
    TV.TCORB = TV.TCORA / 2;

    //  128分周でタイマカウント開始
    TV.TCRV0.BIT.CKS = 3;

    return 0;
}

//------------------------------------------------
//  概 要:音の出力停止
//  引  数:なし
//  戻り値:0   正常
//          -1  異常
//------------------------------------------------
int Spk_stop(void)
{
    TV.TCRV0.BIT.CKS = 0; //  タイマカウント停止
    TV.TCNTV         = 0; //  タイマカウント値の初期化

    return 0;
}
  • 音階のタイマカウントを簡単に設定できるようにグローバル変数の定数テーブルを定義し、列挙型と対応させている。
  • Spk_init関数ではPWM機能で値が確定している項目を設定しておく。
  • Spk_start関数では定数テーブルの範囲外アクセスを防ぐためにインデックス範囲のチェックを行う。列挙型から定数テーブルを参照しTCORAとTCORBの波形タイミング設定を行う。最後に分周CKSを設定することでタイマカウントを開始する。
  • Spk_stop関数ではCKSを0クリアすることでタイマカウントを停止する。

課題2

課題内容

MAINモジュールでスピーカーモジュールを使えるようにせよ。また、main関数を変更し次の処理を繰り返すようにせよ。

  • 1秒毎にド・レ・ミ・ファ・ソ・ラ・シ・ドを出力する。Timerモジュールを利用してよいものとする。
  • 5秒間、音の出力を停止する。

main.c

//------------------------------------------------
//  概  要:エントリーポイント
//------------------------------------------------
void main(void)
{
    //  システム初期化処理
    main_init();

    while(1)
    {

    }
}

課題が完成したらビルドを行いビュートローバー上で動作させ、期待動作通りに動くことを確認せよ。


期待動作

  1. システム起動直後に1秒毎にド・レ・ミ・ファ・ソ・ラ・シ・ドが出力される。
  2. 音の出力が停止され5秒間無音になる。
  3. 再度、1.から繰り返す。

main.c

#include <stdio.h>
#include "iodefine.h"
#include "led.h"
#include "sw.h"
#include "timer.h"
#include "lsen.h"
#include "sci.h"
//  スピーカーモジュールを使用するためインクルード
#include "spk.h"

//------------------------------------------------
//  内部プロトタイプ宣言
//------------------------------------------------
void main_init(void);

//------------------------------------------------
//  概  要:エントリーポイント
//------------------------------------------------
void main(void)
{
    //  システム初期化処理
    main_init();

    while(1)
    {
        E_SPK_SCALE scale;

        //  1秒毎に音階を順番に出力
        for (scale = E_SPK_SCALE_DO ; scale < E_SPK_SCALE_END ; scale++)
        {
            Spk_start(scale);
            Timer_waitTime(1000);
        }

        //  音を止めて5秒待つ
        Spk_stop();
        Timer_waitTime(5000);
    }
}

//------------------------------------------------
//  概  要:システム初期化
//------------------------------------------------
void main_init(void)
{
    //---------------------------------------------------
    //  ウォッチドッグタイマの停止(消さないこと)
    //---------------------------------------------------
    WDT.TCSRWD.BYTE = 0x92;
    WDT.TCSRWD.BYTE = 0x92;


    //---------------------------------------------------
    //  PCRの設定(PCRは書き込み専用レジスタ)
    //---------------------------------------------------
    IO.PCR6         = 0x11;     //  P60,P64を出力
    IO.PCR7         = 0x00;     //  P74を入力


    //---------------------------------------------------
    //  モジュールの初期化(各モジュールの初期化を実施)
    //---------------------------------------------------
    Led_init();
    Sw_init();
    Timer_init();
    Lsen_init();
    Sci_init();
    Spk_init();

}
  • spk.hをインクルードする。
  • main関数ではループ処理を使い列挙型を順番に切り替えながらSpk_start関数を呼び出す。
  • ループ終了後は音の出力を停止させ、無音用の5秒待ちを行う。
  • main_init関数ではSpk_init関数を呼び出す。