Pythonで正弦波モデルを実装

Pythonを使って正弦波モデルを実装しました。ソースコードがネット上にあまりないため、頑張って自力で書きました。 今回は、M&Qアルゴリズムとよばれる方法で実装を行っています。

参考文献は以下の2つです。

[1] 小坂 直敏、「サウンドエフェクトのプログラミング―Cによる音の加工と音源合成」、オーム社、2012.

[2] R.J.McAulay and T.F.Quatieri , “Speech Analysis/Synthesis Based on a Sinusoidal Representation”, IEEE Trans. on Acoust., Speech, and Singnal Processing, vol. ASSP-34, No. 4, Aug. 1986.

正弦波モデルとは

正弦波モデルとは、正弦波に似ている信号を足し合わせて、音声や楽音などを表現する方法です。

音声のスペクトログラムの例を図1に示します。

図1:音声のスペクトログラムの例
図1:音声のスペクトログラムの例

音声のスペクトログラムでは、周波数方向に間隔をあけてエネルギーが多い箇所があり、こういう特徴を調波構造と呼んだりします。正弦波モデルでは、こういうエネルギーが多い箇所を振幅と周波数が滑らかに時間変化する正弦波で近似します。

正弦波モデルの用途としては、元信号よりも少ないデータで信号を近似するので、音声圧縮で使われたりします(MP3とかはこの方法を使いませんが...)。また、正弦波モデルによって調波成分ごとに操作が可能となるため、信号処理の前段階としても使われている印象です。

プログラム

記述したプログラムについて説明します。正直いって、論文の内容で見逃している点やバグがあるかもしれないので、参考程度にしていただければ幸いです。

プログラムは一つのファイルに記述しています。また、モノラル音源のみに対応しており、ステレオには対応していません。

メイン処理

メインの処理プログラム箇所は以下です。

import soundfile as sf
import numpy as np
import scipy.signal as sg
import cmath
import datetime
from librosa import resample, stft
import matplotlib.pyplot as plt

PEAK        = 1
CONNECT     = 2
DEATH       = 3
BIRTH       = 4
DEATH_BIRTH = 5

# フレームごとの極大値を検知
def DetectPeak(X):
        ~ 略 ~

# フレーム間ピークマッチング
def PeakMatching(peak_plane):
        ~ 略 ~

# 音声合成(位相補間と振幅補間)
def SynthesizeSpeech(X, peak_plane, track_plane):
        ~ 略 ~

# パラメータ
path = "voice.wav"
sr_o = 10000
n_fft = 512
hop_length = 256
delta_freq = 50
window = sg.windows.get_window('hamming', n_fft)
window = window / np.sum(window)
    
# WAVファイル読み込みとリサンプリング
x, sr = sf.read(path)
x = resample(x, sr, sr_o)
sf.write("sample.wav", x, sr_o, subtype='PCM_16')

# 必要な値をあらかじめ計算
delta_bin  = int(delta_freq / (sr_o/n_fft))
T = hop_length/sr_o
omega_b = 2.0 * np.pi *(sr_o/n_fft)
x_shape = x.shape

# 計測スタート
start_time = datetime.datetime.now()

# 短時間フーリエ変換
X = stft(x, n_fft, hop_length, None, window)

# フレームごとの極大値を検知
peak_plane = DetectPeak(X)

# フレーム間ピークマッチング
track_plane = PeakMatching(peak_plane)

# 音声合成(位相補間と振幅補間)
y = SynthesizeSpeech(X, peak_plane, track_plane)

# 処理時間の出力
elapsed_time = datetime.datetime.now() - start_time
print("time: %s" % (elapsed_time))

# WAVファイル出力
sf.write("soundout.wav", y, sr_o, subtype='PCM_16')

メインの処理では、以下の手順で音声を再合成しています。

  1. 音声を読み込む
  2. リサンプリングする
  3. 短時間フーリエ変換をする
  4. フレームごとに極大値(ピーク)を検知
  5. フレームどうしのピークをつなげる
  6. フレーム間の位相補間と振幅補間
  7. 音声を出力する

注意:窓関数  w[n] については以下の式を満たすように補正しています。

\displaystyle{
\hspace{3em} \sum_{n=-N/2}^{N/2} w[n] = 1 
}

ここで、 N は窓関数の幅です。

なぜ、このようなことをするかというと、補正していない窓関数を使うと短時間フーリエ変換後の各要素のパワーと元信号のパワーを単純比較できなくなるからです。

補正に関しては以下の方のブログがわかりやすいです。

フレームごとに極大値の検知

フレームごとに極大値の検知をする DetectPeak の関数定義は以下になります。

# フレームごとの極大値を検知
def DetectPeak(X):

    # 全てゼロのnumpy配列の作成
    peak_plane = np.zeros(X.shape, dtype=int)
    # 極大値の箇所を検知
    index = sg.argrelextrema(X, np.greater, axis=0, order=1)
    # 極大値の箇所に1を代入
    peak_plane[index] = PEAK

    return peak_plane

極大値がある箇所にPEAK(1)を代入して、極大値ではない箇所は0とする2次元配列 peak_plane を作成します。

DetectPeak の出力は図2のようになります。

図2:DetectPeak の出力
図2:DetectPeak の出力

補足Pythonでfor文を使うと、基本的に処理が遅いので、scipy.signal にある argrelextrema を使用して、極大値を検知しました。

フレーム間ピークマッチング

フレーム間ピークマッチングする PeakMatching の関数定義は以下になります。

# フレーム間ピークマッチング
def PeakMatching(peak_plane):

    n_bin = peak_plane.shape[0]    # ビン数
    n_frame = peak_plane.shape[1]  # フレーム数
    link_flag = False              # 接続先があるかのフラグ 
    candidate_match = n_bin        # 接続先の候補1
    next_candidate_match = n_bin   # 接続先の候補2

    # 全てゼロのnumpy配列の作成
    track_plane = np.ones(peak_plane.shape, dtype=int) * (-1)

    for i in range(n_frame-1):
        for k in range(n_bin):

            # 極大値の箇所を検知
            if peak_plane[k,i]==PEAK or peak_plane[k,i]==CONNECT:

                # 前の極大値で接続先があったとき
                if link_flag == True:
                    # step2(c)
                    if abs(pre_peak-candidate_match) < abs(k-candidate_match):
                        track_plane[pre_peak,i] = candidate_match
                        peak_plane[candidate_match, i+1] = CONNECT
                    else:
                        # step2(d)
                        if next_candidate_match > candidate_match:
                            track_plane[pre_peak,i] = pre_peak     
                            peak_plane[pre_peak,i+1] = DEATH
                        # step2(e)
                        else:
                            track_plane[pre_peak,i] = next_candidate_match
                            peak_plane[next_candidate_match, i+1] = CONNECT

                # 初期化
                link_flag = False
                candidate_match = n_bin
                next_candidate_match = n_bin

                # 接続先候補を探す
                if k < delta_bin:
                    for j in range(-k,delta_bin+1):
                        if peak_plane[k+j,i+1]==PEAK:
                            link_flag = True
                            if abs(j) < abs(candidate_match):
                                next_candidate_match = candidate_match 
                                candidate_match = k+j  #step1(b)
                elif k >= n_bin-delta_bin: 
                    for j in range(-delta_bin,n_bin-k):
                        if peak_plane[k+j,i+1]==PEAK:
                            link_flag = True
                            if abs(j) < abs(candidate_match):
                                next_candidate_match = candidate_match 
                                candidate_match = k+j  #step1(b)
                else:
                    for j in range(-delta_bin,delta_bin+1):
                        if peak_plane[k+j,i+1]==PEAK:
                            link_flag = True  
                            if abs(j) < abs(candidate_match):
                                next_candidate_match = candidate_match 
                                candidate_match = k+j  #step1(b)
                
                # 接続先がなかった場合
                if link_flag==False:
                    track_plane[k,i]  = k 
                    peak_plane[k,i+1] = DEATH

                # 極大値の位置の保存
                pre_peak = k
    
            # trackがDEATHのとき
            elif peak_plane[k,i]==DEATH:
                track_plane[k,i] = (-2)  # step1(a) DEATHとする

        # 一番高い周波数ビンのピークを接続させる
        if link_flag == True:
            track_plane[pre_peak,i] = candidate_match
            peak_plane[candidate_match, i+1] = CONNECT

        # i+1フレームに接続されてない極大値がないか確認
        for k in range(n_bin):
            # step3(f) BIRTHとする
            if peak_plane[k,i+1]==PEAK:
                track_plane[k,i] = k
                peak_plane[k,i+1] = CONNECT      
                if peak_plane[k,i]==DEATH:
                    peak_plane[k,i] = DEATH_BIRTH
                else:
                    peak_plane[k,i] = BIRTH

    return track_plane

PeakMatching は図3のようにピークの接続先のビン番号を記録した2次元配列 track_planeを出力します。ピークではない箇所は (-1) となっており、接続先がないときは (-2) とします。

図3:PeakMatching の出力の模式図
図3:PeakMatching の出力の模式図

ピークの接続は以下の手順で決めます。

STEP1

図4:STEP1のピークマッチングの模式図
図4:STEP1のピークマッチングの模式図

まず、パラメータとして Δ(delta_bin) を決めておきます。フレーム i、ビン番号 kにピークがある場合、次のフレームの [k-Δ, k+Δ] の範囲にピークがなければ、接続先はないものとします。接続先がないときは、図4(a) のように track_plane[k,i]=ktrack_plane[k,i+1]=(-2) を代入します。

一方、 [k-Δ, k+Δ] の範囲にピークがあれば、接続先の第一候補のビン番号(candidate_match)と第二候補のビン番号(next_candidate_match)を保存しておいて、STEP2 に移行します。

ピークの接続先がなくなった箇所(track_plane[k,i+1]=(-2)を代入した箇所)については、peak_plane[k,i+1]=DEATH(3)を代入します。

STEP2

図5:STEP2のピークマッチングの模式図
図5:STEP2のピークマッチングの模式図

接続先候補のピークがある場合、次のピークとkビンのピークのどちらが接続先第一候補(candidate_match)と近いか比較します。

kビンの方が近い場合(σ1>σ2)、track_plane[k,i]=candidate_match を代入します(図5(c))。

次のビンの方が近くて(σ1<σ2)、candidate_match<next_candidate_matchの場合、図5(d)のように track_plane[k,i]=ktrack_plane[k,i+1]=(-2) を代入します。

次のビンの方が近くて(σ1<σ2)、candidate_match>next_candidate_matchの場合、図5(e)のように track_plane[k,i]=next_candidate_match を代入します。

前のフレームのピークと接続されたピークの場所については、
peak_plane[k,i]=CONNECT(2) を代入します。
また、STEP1と同様にtrack_plane[k,i+1]=(-2)を代入した箇所については、
peak_plane[k,i+1]=DEATH(3)を代入します。

STEP3

図6:STEP3のピークマッチングの模式図
図6:STEP3のピークマッチングの模式図

フレーム i の全ピークの接続先が決まったら、フレーム(i+1) で接続されていないピークがあれば、track_plane[k,i]=kpeak_plane[k,i]=BIRTH(4) を代入します(図6(f))。

また、既にpeak_plane[k,i]=DEATH(2)となっていた場合、peak_plane[k,i]=DEATH_BIRTH(5)とします。

位相補間と振幅補間

位相補間と振幅補間をする SynthesizeSpeech の関数定義は以下になります。

# 音声合成(位相補間と振幅補間)
def SynthesizeSpeech(X, peak_plane, track_plane):

    n_bin = X.shape[0]                # ビン数
    n_frame = X.shape[1]              # フレーム数
    y = np.zeros(x_shape)             # 出力信号
    n = np.arange(hop_length)         # サンプル番号
    t = np.arange(hop_length)/sr_o    # 時間
    r_arr = np.zeros(hop_length)      # 振幅の一時保存
    theta_arr = np.zeros(hop_length)  # 位相の一時保存

    for i in range(n_frame-1):
        for k in range(n_bin):
            
            # 極大値、DEATH、BIRTHを判定
            if peak_plane[k,i] == CONNECT or \
               peak_plane[k,i] == BIRTH or \
               peak_plane[k,i] == DEATH_BIRTH:
                
                # 振幅と位相と角周波数
                if peak_plane[k,i] == BIRTH or peak_plane[k,i] == DEATH_BIRTH:
                    r1 = 0
                else:
                    if k==0 or k==n_bin-1:
                        r1 = abs(X[k,i])
                    else:
                        r1 = 2.0 * abs(X[k,i])
                theta1 = cmath.phase(X[k,i])
                omega1 = k * omega_b
                if peak_plane[k,i+1] == DEATH or peak_plane[k,i+1] == DEATH_BIRTH:
                    r2 = 0
                else:
                    if track_plane[k,i]==0 or track_plane[k,i]==n_bin-1:
                        r2 = abs(X[track_plane[k,i],i+1])
                    else:
                        r2 = 2.0 * abs(X[track_plane[k,i],i+1])
                theta2 = cmath.phase(X[track_plane[k,i],i+1])
                omega2 = track_plane[k,i] * omega_b
                
                # Mの計算
                M = ((theta1+omega1*T-theta2)+(omega2-omega1)*T/2.0)/(2.0*np.pi)
                M = round(M)

                # alphaとbeta
                matrixup = theta2-theta1-omega1*T+M*2.0*np.pi
                matrixdw = omega2-omega1
                alpha = matrixup*(3.0/(T**2))-matrixdw/T
                beta  = matrixup*(-2.0/(T**3))+matrixdw/(T**2)

                # r, theta計算とyに足し合わせる
                r_arr = r1+(r2-r1)*n/(hop_length)
                theta_arr = theta1+omega1*t+alpha*(t**2)+beta*(t**3)
                y[i*hop_length:(i+1)*hop_length] += r_arr*np.cos(theta_arr)

フレーム間の位相補間と振幅補間を行い、音声を再合成します。

フレーム i、ビン番号 k にピークがあるとき、接続先のビン番号を k' とすると、フレームごとの振幅と位相と周波数を以下のように計算します。

\displaystyle{
 r_1 = 2 |X[k,i]| \\
 r_2 = 2 |X[k',i]| \\
 \theta_1 = {\rm arg}(X[k,i])\\
 \theta_2 =  {\rm arg}(X[k',i])\\
 \omega_1 = 2 \pi k \frac{f_s}{N_F}\\
 \omega_2 = 2 \pi k' \frac{f_s}{N_F}
}

ここで、 f_s はサンプリング周波数、 N_FFFT点数です。

ただし、k=0 または k=n_bin-1(一番大きい周波数ビン番号) のときは、

\displaystyle{
 r_1 = |X[k,i]|
}

k'=0 または k'=n_bin-1 のときは、

\displaystyle{
 r_2 = |X[k',i]|
}

また、peak_plane[k,i] == BIRTH または peak_plane[k,i] == DEATH_BIRTH のときは、

\displaystyle{
 r_1 = 0
}

peak_plane[k',i] == DEATH または peak_plane[k',i] == DEATH_BIRTH のときは、

\displaystyle{
 r_2 = 0
}

とします。

その後、ピークごとにフレーム間の信号を再合成します。

ピークのインデックスを p とすると、プレーム間の各信号の振幅  A_p [n] は以下のように線形補間で求めます。

\displaystyle{
 A_p[n] = r_1 + \frac{r_2 - r_1}{S} n \hspace{1em}(n=0,1,\cdots, S-1)  
}

ここで、 S はフレームシフト点数です。

フレーム間の各信号の位相  \theta_p [n] は以下のように3次補間で求めます。

\displaystyle{
 \theta_p[n] = \theta_1 + \omega_1 \frac{n}{f_s} + \alpha (M) \left(\frac{n}{f_s}\right)^2 + \beta (M) \left(\frac{n}{f_s}\right)^3 \hspace{1em}(n=0,1,\cdots, S-1) 
}

 \alpha (M)、\beta (M) は以下のように求めます。

\displaystyle{
 \begin{bmatrix}
\alpha(M)\\
\beta(M)
\end{bmatrix}
=
 \begin{bmatrix}
\frac{3}{T^2}&\frac{-1}{T}\\
\frac{-2}{T^3}&\frac{1}{T^2}
\end{bmatrix}
 \begin{bmatrix}
\theta_2 -\theta_1-\omega_1 T + 2\pi M\\
\omega_2-\omega_1
\end{bmatrix}
}

 M については以下のように求めます。

\displaystyle{
 M = {\rm round} \left( \frac{1}{2\pi} \left[(\theta_1+\omega_1 T - \theta_2) + (\omega_2 - \omega_1)\frac{T}{2}\right] \right) 
}

 A_p[n] と \theta_p[n] を求めたら、再合成信号  y [n] を以下の式で求めます。

\displaystyle{
 y[n] = \sum_{p} A_p[n] \cos{(\theta_p[n])}
}

プログラムの実行

プログラム中のパラメータを表のように設定して、プログラムを実行しました。

パラメータ名 記号 変数名 設定値
サンプリング周波数 f_s sr_o 10 kHz
FFT点数 N_F n_fft 512
フレームシフト点数  S hop_length 256
接続先の周波数範囲 \Delta f delta_freq 50 Hz
窓関数 w window ハミング窓

プログラムの実行は記述したスクリプトmain.pyと音声ファイルvoice.wavを同じフォルダに置き、以下のようにして実行できます。

python main.py

歌声を再合成した結果は以下のようになります。

10kHz にリサンプリング後の歌声

正弦波モデルで再合成した歌声

リサンプリング後の歌声と再合成後の歌声の波形とスペクトログラムは図7です。

図7:歌声の波形とスペクトログラム(上:リサンプリング後、下:再合成後)
図7:歌声の波形とスペクトログラム(上:リサンプリング後、下:再合成後)

少しノイズがありますが、歌声も聴きとれるので多分上手くできてるのかな?

おわりに

今回は、正弦波モデルを実装しました。論文では、窓関数の幅を基本周波数に応じて変えたり、1フレームの最大ピーク数を決めていたりしているので、それらを実装することでさらに良質な歌声を再合成できるかもしれません。

使用した楽曲について

この記事で信号処理した楽曲は Cambridge Music Technology で提供されている Actions の Devil's Words を使用させていただきました。

【楽曲を提供している Cambridge Music Technology のページ】
https://www.cambridge-mt.com/ms/mtk

【ActionsのFacebook
https://www.facebook.com/actionsuk

RXマイコンのソフト開発(8)磁気式エンコーダーAS5047

RXマイコン(RX631)のソフト開発の8回目です。今回は、磁気式エンコーダーのAS5047を動作させたいと思います。AS5047によって、タイヤの回転角度を求めます。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

AS5047の周辺回路

作成したAS5047の周辺回路図は図1です。xxxxxx_Hxxxxxx_HRはコネクタでつながっています。

図1:AS5047の周辺回路図
図1:AS5047の周辺回路図

今回使用するネットの RX631 への接続先は以下です。今回は右側のエンコーダーしか使いません。

  • CS1 → PB5
  • RSPCKA → PC5/RSPCKA
  • MOSIA → PC6/MOSIA
  • MISOA → PC7/MISOA

はじめ、SPI通信で初期設定をして、R_ENC_AR_ENC_BからA相とB相の矩形波を受け取ろうと思ったのですが、SPI通信だけでも角度情報を受け取れるようなので、R_ENC_AR_ENC_Bは使いません。

補足:初期状態でA相とB相の矩形波が出力されるように設定されている(オシロスコープで出力を確認)ので、SPIの通信線がなくても角度情報は受け取れるそうです。このため、レジスタ設定をしない人は、SPIの通信線を省略してみてもいいかもしれません。

作成した機体の足回り

足回りの構造は図2のようになっています。

図2:足回りの構造
図2:足回りの構造

ギアホイールにシャフトを圧入して、カラーにシャフトを圧入して、カラーの中に磁石を入れることで、ギアホイールと磁石が同じように回転します。磁気式エンコーダーで磁石の角度がわかることで、ギアホイールの回転角度もわかります。

プログラムフロー

プログラムの処理の流れは図3です。

図3:プログラムのフロー
図3:プログラムのフロー

クロックの初期化とCMT0、SCI1の初期化を行って、RSPI0を初期化します。その後、メインルーチンに入り、1ms経過したら、磁石の角度を取得して、UARTで送信します。

プログラム

磁気式エンコーダーに関係するプログラム部分だけ示します。sci.csci.h を除いた全てのソースコードGitHubの「8_as5047」フォルダの中にあります。

メイン関数

main.c:メイン関数のソースコード

#include "sci.h"
#include "spi.h"
#include "init_rx631.h"
#include "define_wakaba.h"

extern volatile unsigned char g_flag;

void main(void){
    unsigned short rd_data;
    unsigned short data;

    init_rx631();       // Overall Initialization

    while(1){
        if(g_flag){
            g_flag = 0;  // Clear flag

            // Read angular velocity
            rd_data = SPI_ReadAS5047(AS5047_ANGLECOM);
            data = (rd_data & 0x3FFF) * 360 / 16384;
            sci_printf("%u\r\n", data);
        }
    }
}

メイン関数では、RX631の初期化を行い、メインルーチンに入ります。CMT0の割り込み関数によって1ms 経過フラグ(g_flag)が立ったら、フラグを下げて、角度を読み、その値をsci_printfでUART送信します。

補足:角度データが入っているレジスタANGLEANGLECOMがありますが、今回はエラー補正したANGLECOMから角度データを取り出します。

RSPI0の初期化

初期化関数のソースコードが以下です。RSPI に関してはユーザーズマニュアルの p.1619「38. シリアルペリフェラルインタフェース(RSPI)」に記述があります。ほとんどRXマイコンのソフト開発(6)RSPIでMPU6000から角速度取得の初期化と同じです。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"
#include "spi.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_sci1(void);
static void init_rspi0(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 Initialization
    init_cmt0();

    // SCI1 Initialization
    init_sci1();

    // RSPI0 Initialization
    init_rspi0();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

/*---- RSPI0 Initialization ----*/
static void init_rspi0(void){

    MSTP(RSPI0) = 0;               //  RSPI0 Module Stop Release

    RSPI0.SPBR = 5;                //  521 kbps when BRDV=3

    RSPI0.SPSCR.BIT.SPSLN = 0;     //  Sequence length = 1
    RSPI0.SPDCR.BIT.SPFC  = 0;     //  Frame Num = 1
    RSPI0.SPDCR.BIT.SPLW  = 1;     //  Long Word Access

    /* RSPI Command Register0 */
    // CPHA = 1  : Data change at odd edges, data sample at even edges
    // CPOL = 0  : RSPCK at idle is LOW
    // BRDV = 3  : 8 divisions of the base bit rate
    // LSBF = 0  : MSB First
    // SPB  = 15 : Data length = 16bit
    RSPI0.SPCMD0.WORD = 0x0F0D;

    MPC.PWPR.BIT.B0WI     = 0;     // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE    = 1;     // PmnPFS Register write enable
    MPC.PC7PFS.BIT.PSEL   = 13;    // Set to MISOA
    MPC.PC6PFS.BIT.PSEL   = 13;    // Set to MOSIA
    MPC.PC5PFS.BIT.PSEL   = 13;    // Set to RSPCKA
    MPC.PWPR.BYTE         = 0x80;  // Reprotect
    PORTC.PMR.BIT.B7      = 1;     // set to Peripheral
    PORTC.PMR.BIT.B6      = 1;     // set to Peripheral
    PORTC.PMR.BIT.B5      = 1;     // set to Peripheral
    PORTB.PDR.BIT.B5      = 1;     // CS1: OUT
    PORTB.PODR.BIT.B5     = 1;     // CS1: HIGH

    RSPI0.SPCR.BIT.SPMS = 1;       // Clock synchronous operation
    RSPI0.SPCR.BIT.MSTR = 1;       // Master Mode
}

MPU6000のときと変わっている点は、SPCMD0レジスタのCPOLビットを"0"にしている点だけです。これで、アイドル時にRSPCKAが"L"になります。

補足:今回は、ANGLECOMレジスタから角度データを取り出すので、AS5047の初期設定はしませんでした。ANGLEレジスタから角度データを取り出す場合は、SETTINGS1レジスタDAECDESビットやDataselectビットを設定したほうがいいかもしれません。

SPIの送受信

SPIで読み書きするソースコードとヘッダーファイルが以下です。こっちはMPU6000のときと少し変わっています。

spi.c:SPI関連のソースコード

#include "define_wakaba.h"
#include "spi.h"
#include "sci.h"

static u16 SPI_SendRecvAS5047(u32 packet);

/* Write to AS5047 register */
void SPI_WriteAS5047(u16 address, u16 data){
    u16 packet;
    u8  i;
    u8  parity=0;

    CS_AS5047 = ASSERT;  // Assert AS5047

    /* make packet */
    packet = address;
    for(i=0;i<15;i++)
        parity += (packet >> i) & 1;
    packet += (parity % 2) << 15;

    SPI_SendRecvAS5047(packet);  // Send Address

    CS_AS5047 = NEGATE;  // Negate AS5047

    for(i=0;i<10;i++)   // Wait 700ns
        __nop();

    CS_AS5047 = ASSERT;  // Assert AS5047

    /* make packet */
    packet = data;
    parity = 0;
    for(i=0;i<15;i++)
        parity += (packet >> i) & 1;
    packet += (parity % 2) << 15;

    SPI_SendRecvAS5047(packet);  // Send Data

    CS_AS5047 = NEGATE;  // Negate AS5047
}

/* Read AS5047 register */
u16 SPI_ReadAS5047(u16 address){
    u16 packet=0;
    u8  i;
    u8  parity=0;
    u16 data=0;

    CS_AS5047 = ASSERT;  // Assert AS5047

    /* make packet */
    packet = 0x4000 + address;
    for(i=0;i<15;i++)
        parity += (packet >> i) & 1;
    packet += (parity % 2) << 15;

    SPI_SendRecvAS5047(packet);  // Send Address

    CS_AS5047 = NEGATE;  // Negate AS5047

    for(i=0;i<10;i++)   // Wait 700ns
        __nop();

    CS_AS5047 = ASSERT;  // Assert AS5047

    /* make packet */
    packet = 0xC000;

    data = SPI_SendRecvAS5047(packet);  // Receive Data

    CS_AS5047 = NEGATE;  // Negate AS5047

    return data;
}

/* Sending/receiving data */
static u16 SPI_SendRecvAS5047(u32 packet){
    u16 data;

    RSPI0.SPCR.BIT.SPTIE  = 1;  // Enable transmission IRQ
    RSPI0.SPCR.BIT.SPE    = 1;  // Enable RSPI

    /* Wait for the send buffer to be empty */
    while(IR(RSPI0, SPTI0)==0);
    IR(RSPI0, SPTI0)=0;         // Clear Flag

    RSPI0.SPDR.LONG = packet;   // Send data

    RSPI0.SPCR.BIT.SPTIE  = 0;  // Disable transmission IRQ
    RSPI0.SPCR2.BIT.SPIIE = 1;  // Enable idle IRQ
    RSPI0.SPCR.BIT.SPRIE  = 1;  // Enable receive IRQ

    /* Wait for RSPI to idle */
    while(IR(RSPI0, SPII0)==0);
    IR(RSPI0, SPII0)=0;         // Clear Flag

    /* Wait for the receive buffer to be written */
    while(IR(RSPI0, SPRI0)==0);
    IR(RSPI0, SPRI0)=0;         // Clear Flag

    data = RSPI0.SPDR.LONG & 0xFFFF;

    RSPI0.SPCR2.BIT.SPIIE = 0;  // Enable idle IRQ
    RSPI0.SPCR.BIT.SPRIE  = 0;  // Enable receive IRQ
    RSPI0.SPCR.BIT.SPE    = 0;  // Disable RSPI

    return data;
}

spi.h:SPI関連のヘッダーファイル

#ifndef SPI_H_
#define SPI_H_

#include "iodefine.h"
#include "define_wakaba.h"

#define LENGTH_16BIT  15
#define LENGTH_24BIT   1

#define AS5047_ERRFL     0x0001
#define AS5047_ZPOSM     0x0016
#define AS5047_ZPOSL     0x0017
#define AS5047_SETTINGS1 0x0018
#define AS5047_SETTINGS2 0x0019
#define AS5047_ANGLE     0x3FFE
#define AS5047_ANGLECOM  0x3FFF

#define ASSERT 0
#define NEGATE 1

#define CS_AS5047  PORTB.PODR.BIT.B5

void SPI_WriteAS5047(u16 address, u16 data);
u16 SPI_ReadAS5047(u16 address);

#endif /* SPI_H_ */

メイン関数ではSPI_WriteAS5047は使っていないので、SPI_ReadAS5047だけ説明したいと思います。SPI_SendRecvAS5047についてはSPI_SendRecvMPU6000と変わっていません。

SPI通信でAS5047のレジスタを読む方法は図4のようになります。

図4:SPI通信によるレジスタの読みかた(AS5047Pのデータシートから引用)
図4:SPI通信によるレジスタの読みかた(AS5047Pのデータシートから引用)

コマンドフレーム(Command)の構造は以下の表のようになります。

ビット 名前 説明
15 PARC 14:0 ビットで計算された偶数パリティビット
14 R/W 0:書き込み、1:読み込み
13:0 ADDR 読み書きするアドレス

パリティビットを作る必要があるので、SPI_ReadAS5047関数の中でパリティビットを作成しています。

データフレーム(Data)の構造は以下の表のようになります。

ビット 名前 説明
15 PARD 14:0 ビットで計算された偶数パリティビット
14 EF 0:コマンドフレームでエラーなし、1:エラーあり
13:0 DATA データ

15:14 ビットについてはデータではないので、メイン関数でマスクをして、データだけ取り出しています。

また、アドレス送信とデータ受信の間に350ns以上、CSを"H"にする必要があるので、NOPを10回実行しています。オシロスコープで測定したところ、NOPを10回実行することで、700nsの間、CSが”H”になっていました。

データを受信するときは、NOPレジスタ(Address=0x0000)を読み込むためのコマンドフレーム(0xC000)を同時送信しています。

プログラムの実行

プログラムを実行して、タイヤを左右に動かすと図5のようになりました(取得したデータから初期値を引いています)。

図5:取得した回転角度
図5:取得した回転角度

図5を見たところ、回転角度は問題なく取得できてそうです。

おわりに

今回で磁気式エンコーダーのAS5047からタイヤの回転角度を取得できました。これまでで、ほとんどのデバイスを動作させることができましたが、赤外線センサーだけ上手く動作できなかったので、もう一度基板を作りなおそうかと思います。

参考文献

RXマイコンのソフト開発(7)モータドライバーDRV8836の動作

RXマイコン(RX631)のソフト開発の7回目です。今回は、モータドライバーのDRV8836を動作させたいと思います。制御はしないです。ただ動かすだけです。ギアが噛み合うかも確かめたいと思います。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

DRV8836の周辺回路

作成したDRV8836の周辺回路図は図1です。

図1:DRV8836の周辺回路図
図1:DRV8836の周辺回路図

各ネットの RX631 への接続先は以下です。

  • NSLEEP → P15
  • M_PHASE_L → PE4
  • M_ENBL_L → PB1/MTIOC4C
  • M_PHASE_R → PB0
  • M_ENBL_R → PB3/MTIOC4A

DRV8836は制御の仕方に IN/IN モードと PHASE/ENABLE モードがありますが、作成した回路はPHASE/ENABLE モードで使う回路となっています。xIN1(xENABLE)にPWM波形を入れて、DUTY比を変えることで速度を変更します。また、xIN2(xPHASE)にHIGH、LOWを入れて、回転方向を変更します。

プログラムフロー

プログラムの処理の流れは図2です。

図2:プログラムのフローチャート
図2:プログラムのフローチャート

クロックの初期化とCMT0の初期化を行って、MTU4を初期化します。その後、メインルーチンに入り、スイッチが押されたら、片方のモータをDUTY比50%で回転させるか、逆回転させるか、停止させるかします。その後、status変数を変更します。

プログラム

重要なプログラム部分だけ示します。全てのソースコードGitHubの「7_drv8836」フォルダの中にあります。

メイン関数

main.c:メイン関数のソースコード

#include "define_wakaba.h"
#include "interface.h"
#include "iodefine.h"

#define NSLEEP       PORT1.PODR.BIT.B5
#define M_PHASE_R    PORTB.PODR.BIT.B0
#define M_PHASE_L    PORTE.PODR.BIT.B4
#define MOTOR_START  MTU.TSTR.BIT.CST4

void main(void){
    unsigned char status = 0;

    init_rx631(); // Overall Initialization
    NSLEEP = 1;   // Sleep mode stop

    while(1){
        if(g_sw_chg){   // Enter when SW is pressed
            g_sw_chg = 0;
            if(status==0){  // Rotate motor
                M_PHASE_R = 0;
                MTU4.TGRA = 249;   // Duty M_ENBL_R
                MTU4.TGRC = 249;   // Duty M_ENBL_L
                MOTOR_START = 1;
                status = 1;
            }
            else if(status==1){  // Reverse rotation of motor
                M_PHASE_R = 1;
                status = 2;
            }
            else if(status==2){  // Motor Stop
                MTU4.TGRA = 500;   // Duty M_ENBL_R
                MTU4.TGRC = 500;   // Duty M_ENBL_L
                status = 0;
            }
        }
    }
}

メイン関数では、RX631の初期化を行い、P15(NSLEEP)をHIGHにすることで、DRV8836のスリープモードを解除します。スイッチが押されてフラグ(g_sw_flag)が立ったら、フラグを下げて、status に応じて、モーターを回転させたり、逆回転させたり、停止させたりします。回転させるときは、M_PHASE_R で回転方向を決めて、MOTOR_START (MTU.TSTR.BIT.CST4) を1に設定して、タイマーを動作させて、PWM波形を出力させます。

補足:モーターを停止させるとき、MTU4.TGRX=500 とさせることで、コンペアマッチさせず、HIGHにならないようにしています。

MTU4の初期化

初期化関数のソースコードが以下です。MTU に関してはユーザーズマニュアルの p.765「23. マルチファンクションタイマパルスユニット 2(MTU2a)」に記述があります。初期化の方法は RXマイコンのソフト開発(3)スピーカーから音を出す とほとんど同じです。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_cmt1(void);
static void init_mtu4(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 and CMT1 Initialization
    init_cmt0();

    // MTU4 Initialization
    init_mtu4();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

/*---- MTU4 Initialization ----*/
void init_mtu4(void){

    MSTP(MTU) = 0;              // MTU Module Stop Release

    MPC.PWPR.BIT.B0WI   = 0;    // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE  = 1;    // PmnPFS Register write enable
    MPC.PB3PFS.BIT.PSEL = 0x02; // set to MTIOC4A
    MPC.PB1PFS.BIT.PSEL = 0x02; // set to MTIOC4C
    MPC.PWPR.BYTE       = 0x80; // Reprotect
    PORTB.PMR.BIT.B3    = 1;    // set to Peripheral
    PORTB.PMR.BIT.B1    = 1;    // set to Peripheral
    PORTB.PDR.BIT.B0    = 1;    // M_PHASE_R: OUT
    PORTE.PDR.BIT.B4    = 1;    // M_PHASE_L: OUT
    PORT1.PDR.BIT.B5    = 1;    // NSLEEP:    OUT

    MTU4.TMDR.BIT.MD   = 0x2;   // PWM mode1
    MTU.TOER.BIT.OE4A  = 1;     // MTIOC4A Output Enable
    MTU.TOER.BIT.OE4C  = 1;     // MTIOC4C Output Enable
    MTU4.TIORH.BIT.IOA = 0x2;   // Initial output LOW, Compare match HIGH
    MTU4.TIORH.BIT.IOB = 0x1;   // Initial output LOW, Compare match LOW
    MTU4.TIORL.BIT.IOC = 0x2;   // Initial output LOW, Compare match HIGH
    MTU4.TIORL.BIT.IOD = 0x1;   // Initial output LOW, Compare match LOW
    MTU4.TCR.BIT.TPSC  = 0x0;   // PCLK/1
    MTU4.TCR.BIT.CKEG  = 0x0;   // count by riging edge
    MTU4.TCR.BIT.CCLR  = 0x2;   // Clear by compare match TGRB
    MTU4.TGRA          = 249;   // Duty M_ENBL_R
    MTU4.TGRB          = 499;   // PWM period
    MTU4.TGRC          = 249;   // Duty M_ENBL_L
    MTU4.TGRD          = 499;   // PWM period
}

モジュールストップを解除して、端子機能を変更して、MTU4の設定をしています。

スピーカーのときと異なる点は主に以下です。

  • MTIOC4A、MTIOC4Cの出力許可を出す
  • コンペアマッチしたときの出力の仕方を変える
  • PWM波形の周波数とDUTY比の設定をする

MTIOC3Aに関しては出力許可をするためのレジスタはありませんでしたが、MTIOC4xに関してはTOERレジスタを設定しないと出力許可されないので設定します。

また、初期設定のときに端子がLOWになってほしいので、TIORHレジスタTIORLレジスタの設定を変更しています。

最後に、TGRXレジスタを設定して、PWM波形の周波数を100kHz、DUTY比を50%にしています。

プログラムの実行

プログラムを実行させて、スイッチを押したところ、図3のようにモーターを回転させることができました(写真ではわかりにくいですが...)。

図3:モーターが回転している様子
図3:モーターが回転している様子

おわりに

今回でDRV8836を動作させることができました。次回は磁気式エンコーダーを動かしたいと思います。

参考文献

RXマイコンのソフト開発(6)RSPIでMPU6000から角速度取得

RXマイコン(RX631)のソフト開発の6回目です。今回は、RSPIを使ってジャイロ(MPU6000)から角速度を取得して、計算した角度をTeraTermで確認したいと思います。情報が少なかったので、実装するのに苦労しました。ユーザーズマニュアルや本、他の人のブログ、MPU6000のデータシートを参考に実装を行いました。

以下の本を少しだけ参考にしました。ただ、基本的にはユーザーズマニュアルや他の人のブログを参考にしています。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

ジャイロの回路

作成したMPU6000の周辺回路図は図1です。

図1:MPU6000の周辺回路図
図1:MPU6000の周辺回路図

各ネットの RX631 への接続先は以下です。

  • MOSIA → PC6/MOSIA
  • RSPCKA → PC5/RSPCKA
  • CS2 → P16
  • MISOA → PC7/MISOA

MPU6000とはSPIとI2Cの通信方法でデータをやり取りできますが、作成した回路はSPIで通信する回路となっています。また、RX631の端子機能であるSSLAxを使わず、汎用入出力でCSを制御しています。

MPU6000の読み書き方法

MPU6000のデータシートを読んでも、読み書きの仕方がいまいちわからなくて苦労しました。そのため、読み書きの仕方を説明します。

1バイトのデータを読む

1バイトのデータを読むときは図2のようにデータをやり取りします。

図2:1バイトの読み方
図2:1バイトの読み方

CS2を”L”にして、最初に送信する1ビットを"1"とすることで、読み込み動作となります。読み込むレジスタは次に送信する7ビットのアドレス [A6]-[A0] で指定できます。アドレス送信後、 指定したアドレスのデータが MPU6000 から送信されますので、これを受信することで1バイト読むことができます。

アドレス送信後に送信するデータ [D7]-[D0] はダミーデータで、RSPCKAを動作させるためのデータです。RSPIの仕様上必要となります。ダミーデータの値はなんでもいいですが、私は 0x00 を送信しています。

1バイトのデータを書く

1バイトのデータを書くときは図3のようにデータを送信します。

図3:1バイトの書き方
図3:1バイトの書き方

CS2を”L”にして、最初に送信する1ビットを"0"とすることで、書き込み動作となります。書き込むレジスタは次に送信する7ビットのアドレス [A6]-[A0] で指定できます。アドレス送信後、 書き込むデータ [D7]-[D0] を送信することで、指定したアドレスのレジスタにその値が書き込まれます。

2バイトのデータを読む

角速度のデータは2バイトの符号付整数のため、2バイトを一気に読み込む必要があります。1バイトずつ読み込んだ場合、上位8ビットと下位8ビットが異なる時間の値となってしまいます。

2バイトのデータを読むときは図4のようにデータをやり取りします。

図4:2バイトの読み方
図4:2バイトの読み方

1バイトのデータを読み込んだあとに、再びダミーデータを送信することで、指定したアドレスの次のアドレスのデータがMPU6000から送信されます。

3バイト以上を読むときも、ダミーデータを何度も送信することでさらに次のアドレスのデータが送信されて、読むことができます。

注意:SPIを使用するデバイスがMPU6000だけの場合、CSをずっと”L”にしておけばいいと思っていましたが、上記のバースト転送の仕様のため、SPI送信後にCSを"H"にしないといけないみたいです。ずっと”L”の場合、時間を空けたとしても、前回受信した次のアドレスのデータがMPU6000から送信されてしまいます。CSを”H”から"L"にして送信した最初のデータがR/Wとアドレスとなるようです。これに気づかなくて、最初に読みだした1バイトだけ正しい現象が起きて、数時間悩みました。

プログラムフロー

プログラムの処理の流れは図5です。

図5:プログラムのフローチャート
図5:プログラムのフローチャート

クロックの初期化とCMT0、SCI1の初期化を行って、RSPIとMPU6000を初期化します。その後、メインルーチンに入り、1ms 経過するたびに角速度をMPU6000から取得して、足し合わせていきます。また、100ms経過するたびに足し合わせた値を角度(deg)に変換して、UART 送信します。

プログラム

MPU6000に関係するプログラム部分だけ示します。sci.csci.h を除いた全てのソースコードGitHubの「6_mpu6000」フォルダの中にあります。

メイン関数

main.c:メイン関数のソースコード

#include "sci.h"
#include "spi.h"
#include "init_rx631.h"
#include "define_wakaba.h"

extern volatile unsigned char g_flag;

void main(void){
    int angle_Z=0;
    u16 cnt=0;
    int send_data;
    short rd_data;

    init_rx631();   // Overall Initialization

    while(1){
        if(g_flag){
            g_flag = 0;  // Clear flag

            // Read angular velocity
            rd_data = SPI_ReadMPU6000(MPU6000_GYRO_ZOUT_H);
            angle_Z += rd_data;

            cnt++;
            if(cnt==100){
                cnt=0;
                send_data = angle_Z / 16384;  // (angle_Z*2000) / (32768*1000)
                sci_printf("angle=%l\r\n", send_data);
            }
        }
    }
}

メイン関数では、RX631の初期化を行い、メインルーチンに入ります。CMT0の割り込み関数によって1ms 経過フラグ(g_flag)が立ったら、フラグを下げて、角速度を読み、いままでの値に足し合わせ、カウント(cnt)をインクリメントします。100msが経過(cnt==100)したら、カウントをクリアして、角度を計算して、その値をsci_printfでUART送信します。

角度(ANGLE)については、長方形近似による数値積分で以下のように求めています。

\displaystyle{
ANGLE=\frac{angle\_Z \times {\rm FS} }{ 32768 \times  1000 } 
}

angle_Z は読み取った値の総和、FS はフルスケールの値に対応する角速度です。MPU6000から読み取るデータは2バイトの符号付整数なので、フルスケールの値は32768となります。また、1ms ごとにサンプリングしているため、1000で割っています。

MPU6000の初期化で、{\rm FS}=2000\ {\rm dps} としているため、約分してソースコードの計算式になります。

RSPI0の初期化

初期化関数のソースコードが以下です。RSPI に関してはユーザーズマニュアルの p.1619「38. シリアルペリフェラルインタフェース(RSPI)」に記述があります。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"
#include "spi.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_sci1(void);
static void init_rspi0(void);
static void init_mpu6000(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 Initialization
    init_cmt0();

    // SCI1 Initialization
    init_sci1();

    // RSPI0 Initialization
    init_rspi0();

    // MPU6000 Initialization
    init_mpu6000();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

/*---- RSPI0 Initialization ----*/
static void init_rspi0(void){

    MSTP(RSPI0) = 0;               //  RSPI0 Module Stop Release

    RSPI0.SPBR = 5;                //  521kbps when BRDV=3

    RSPI0.SPSCR.BIT.SPSLN = 0;     //  Sequence length = 1
    RSPI0.SPDCR.BIT.SPFC  = 0;     //  Frame Num = 1
    RSPI0.SPDCR.BIT.SPLW  = 1;     //  Long Word Access

    /* RSPI Command Register0 */
    // CPHA = 1  : Data change at odd edges, data sample at even edges
    // CPOL = 1  : RSPCK at idle is High
    // BRDV = 3  : 8 divisions of the base bit rate
    // LSBF = 0  : MSB First
    // SPB  = 15 : Data length = 16bit
    RSPI0.SPCMD0.WORD = 0x0F0F;

    MPC.PWPR.BIT.B0WI     = 0;     // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE    = 1;     // PmnPFS Register write enable
    MPC.PC7PFS.BIT.PSEL   = 13;    // Set to MISOA
    MPC.PC6PFS.BIT.PSEL   = 13;    // Set to MOSIA
    MPC.PC5PFS.BIT.PSEL   = 13;    // Set to RSPCKA
    MPC.PWPR.BYTE         = 0x80;  // Reprotect
    PORTC.PMR.BIT.B7      = 1;     // set to Peripheral
    PORTC.PMR.BIT.B6      = 1;     // set to Peripheral
    PORTC.PMR.BIT.B5      = 1;     // set to Peripheral
    PORT1.PDR.BIT.B6      = 1;     // CS2: OUT
    PORT1.PODR.BIT.B6     = 1;     // CS2: HIGH

    RSPI0.SPCR.BIT.SPMS = 1;       //  Clock synchronous operation
    RSPI0.SPCR.BIT.MSTR = 1;       //  Master Mode
}

/*---- MPU6000 Initialization ----*/
static void init_mpu6000(void){

    // Sleep mode Release
    SPI_WriteMPU6000(MPU6000_PWR_MGMT_1, 0x00);

    // Disable I2C interface
    SPI_WriteMPU6000(MPU6000_USER_CTRL, 0x10);

    // The full scale range of the gyroscope = +-2000 dps
    SPI_WriteMPU6000(MPU6000_GYRO_CONFIG, 0x18);
}

RSPI0の初期化では、モジュールストップ状態の解除と端子機能の変更を行います。

その後、以下のRSPI0の設定を行います。

  • RSPI0.SPBR=5 で、ベースビットレートを4.16Mbpsに設定
  • RSPI0.SPSCR.BIT.SPSLN=0 で、シーケンス長を1に設定
  • RSPI0.SPDCR.BIT.SPFC=0 で、フレーム数を1に設定
  • RSPI0.SPDCR.BIT.SPLW=1 で、SPDR レジスタへのアクセス幅を32ビットに設定
  • RSPI0.SPCMD0.WORD = 0x0F0F で、送信の設定をいろいろする
  • RSPI0.SPCR.BIT.SPMS = 1 で、クロック同期式動作に設定
  • RSPI0.SPCR.BIT.MSTR = 1 で、マスタモードに設定

シーケンス長を1にすることで、SPCMD0で設定した送信の方法でしか、送信しないようにします。SPIで送信するデバイスが複数ある場合は、デバイスの数だけシーケンス長を設定するようです。

SPCMD0を設定することで、送信の仕方を以下のように変更します。

  • CPOLビットを”1”にして、アイドル時のRSPCKをHIGHにする
  • CPHAビットを”1”にして、CPOL=1 のときRSPCKの立ち上がりでサンプリング(図2のような波形にするために、ユーザーズマニュアルのp.1660「38.3.5.2 CPHA ビット= 1 の場合」の図 38.24 にあるように、CPOL=1、CPHA=1とします)
  • BRDVビットを3にして、ベースビットレートを8分周する(4.16Mbpsを8分周することで、512kbpsにします。MPU6000の絶対定格ではSCLKの周波数が1MHzなので、1MHz以下になるように512kbpsにしています。)
  • LSBFビットを”0”にして、MSBファーストで送信
  • SPBビットを15にして、16ビット送信する(送信するたびに変更しますが、初期設定は16ビットで送信するようにします。)

SPCMD0の設定が終わった後は、SSLは使わないので、クロック同期式動作にします。また、マスタモードに設定することで、RXマイコンから送信開始するようにします。

以上でRSPI0の設定は終わりです。

つづいて、MPU6000の初期化を行います。初期設定で以下のことをします。

  • PWR_MGMT_1(アドレス107)のSLEEPビットを"0"にして、スリープモードを解除
  • USER_CTRL(アドレス106)の I2C_IF_DIS ビットを"1"にして、I2Cを無効にしてSPIを有効にする(設定しなくてもSPI通信はできるので、設定する必要はないかもしれない)
  • GYRO_CONFIG(アドレス27)の FS_SELビットを3にして、フルスケールの値に対応する角速度を2000dpsにする

SPIの送受信

SPIで読み書きするソースコードとヘッダーファイルが以下です。ソースコードは主にユーザーズマニュアルのp.1676「(a) 送信処理フロー」とp.1677「(b) 受信処理フロー」を参考に書きました。

spi.c:SPI 関係のソースコード

#include "define_wakaba.h"
#include "spi.h"
#include "sci.h"

static u16 SPI_SendRecvMPU6000(u32 packet);

/* Write to MPU6000 register */
void SPI_WriteMPU6000(u8 address, u8 data){
    u16 packet;

    /* make packet */
    packet = address << 8;
    packet |= data;

    /* Number of data to send = 16bit */
    RSPI0.SPCMD0.BIT.SPB = LENGTH_16BIT;

    SPI_SendRecvMPU6000(packet);  // Write
}

/* Read MPU6000 register */
u16 SPI_ReadMPU6000(u8 address){
    u16 data;
    u32 packet;

    /* make packet */
    packet = 0x800000;
    packet |= address << 16;

    /* Number of data to send = 24bit */
    RSPI0.SPCMD0.BIT.SPB = LENGTH_24BIT;

    data = SPI_SendRecvMPU6000(packet);  // Read

    return data;
}

/* Sending/receiving data to/from MPU6000 */
static u16 SPI_SendRecvMPU6000(u32 packet){
    u16 data;

    CS_MPU6000 = ASSERT;   // Assert MPU6000 CS

    RSPI0.SPCR.BIT.SPTIE  = 1;  // Enable transmission IRQ
    RSPI0.SPCR.BIT.SPE    = 1;  // Enable RSPI

    /* Wait for the send buffer to be empty */
    while(IR(RSPI0, SPTI0)==0);
    IR(RSPI0, SPTI0)=0;         // Clear Flag

    RSPI0.SPDR.LONG = packet;   // Send data

    RSPI0.SPCR.BIT.SPTIE  = 0;  // Disable transmission IRQ
    RSPI0.SPCR2.BIT.SPIIE = 1;  // Enable idle IRQ
    RSPI0.SPCR.BIT.SPRIE  = 1;  // Enable receive IRQ

    /* Wait for RSPI to idle */
    while(IR(RSPI0, SPII0)==0);
    IR(RSPI0, SPII0)=0;         // Clear Flag

    /* Wait for the receive buffer to be written */
    while(IR(RSPI0, SPRI0)==0);
    IR(RSPI0, SPRI0)=0;         // Clear Flag

    data = RSPI0.SPDR.LONG & 0xFFFF;

    RSPI0.SPCR2.BIT.SPIIE = 0;  // Enable idle IRQ
    RSPI0.SPCR.BIT.SPRIE  = 0;  // Enable receive IRQ
    RSPI0.SPCR.BIT.SPE    = 0;  // Disable RSPI

    CS_MPU6000 = NEGATE;  // Negate MPU6000 CS

    return data;
}

spi.h:SPI 関係のヘッダーファイル

#ifndef SPI_H_
#define SPI_H_

#include "iodefine.h"
#include "define_wakaba.h"

#define MPU6000_GYRO_CONFIG  27
#define MPU6000_GYRO_ZOUT_H  71
#define MPU6000_GYRO_ZOUT_L  72
#define MPU6000_USER_CTRL   106
#define MPU6000_PWR_MGMT_1  107
#define MPU6000_WHO_AM_I    117

#define LENGTH_16BIT  15
#define LENGTH_24BIT   1

#define ASSERT 0
#define NEGATE 1

#define CS_MPU6000  PORT1.PODR.BIT.B6

void SPI_WriteMPU6000(u8 address, u8 data);
u16 SPI_ReadMPU6000(u8 address);

#endif /* SPI_H_ */

書き込み関数 SPI_WriteMPU6000 と読み込み関数 SPI_WriteMPU6000 はほとんど違いはなく、以下の手順でプログラムが実行されます。

  1. 送信するパケットの作成
  2. 送信するデータ数を設定
  3. SPIで送受信を行う

読み書きで異なる点は以下です。

  • 送信するパケットのMSBが、読みでは"1"、書きでは"0"
  • 読みではダミーデータを送信、書きでは書き込みデータを送信
  • 送信データ数が、読みでは24ビット、書きでは16ビット

SPIの送受信関数SPI_SendRecvMPU6000では以下の手順でプログラムが実行されます。

  1. MPU6000のCSをアサート("L"にする)
  2. 送信割り込み要求の許可とRSPI機能を有効化する
  3. 送信バッファが空になったら、フラグを下げる
  4. 送受信バッファSPDRにパケットを書き、SPI送信する
  5. 送信割り込み要求を禁止して、アイドル割り込み要求と受信割り込み要求を許可する
  6. アイドル状態になったら、フラグを下げる
  7. 受信バッファに書き込まれたら、フラグを下げる
  8. 送受信バッファSPDRから、受信データを取り出す
  9. 全ての割り込み要求を禁止して、RSPI機能を無効化する
  10. MPU6000のCSをネゲート("H"にする)

プログラムの実行

プログラムを実行して、基板の角度をだいたい-45 度から45 度に変えたところ、図6のようにデータを取得できました。

図6:取得した角度データ
図6:取得した角度データ

上手くできてそうです。

おわりに

今回でMPU6000のデータを取得できました。今回の記事は赤外線LEDと赤外線センサーについて書こうと思ったのですが、所望の値が得られなかったため記事が書けませんでした。ハードウェアの問題の可能性が高いので、もう一度基板を作りなおすかもしれません。次回はモータドライバー(DRV8836)の記事を書こうと思います。

参考文献

RXマイコンのソフト開発(5)ADCでバッテリーの電圧測定

RXマイコン(RX631)のソフト開発の5回目です。今回は、ADCでバッテリーの電圧を測定して、TeraTermで値を確認したいと思います。

図1:AD変換の模式図
図1:AD変換の模式図

今回の記事は、以下の資料を主に参考にしました。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

バッテリーチェックの回路

作成したバッテリーチェックの回路図は図2です。

図2:バッテリーチェックの回路
図2:バッテリーチェックの回路

SEN_BAT のポートは PE1/AN009 です。Lipoバッテリーの電圧を抵抗によって半分に分圧しています。その半分になった電圧をADCで測定します。

プログラムフロー

プログラムの処理の流れは図3です。

図3:プログラムのフローチャート
図3:プログラムのフローチャート

クロックの初期化とCMT0とSCI1の初期化を行って、ADCを初期化します。その後、メインルーチンに入り、スイッチの押し込みを検知したら バッテリーの電圧値 を UART 送信します。

プログラム

重要なプログラム部分だけ示します。sci.csci.h を除いた全てのソースコードGitHubの「5_adc」フォルダの中にソースコードがありますので、詳細を知りたい場合はご覧ください。

メイン関数

main.c:メイン関数のソースコード

#include "interface.h"
#include "sci.h"
#include "init_rx631.h"
#include "iodefine.h"

void main(void){
    unsigned short voltage;

    init_rx631();       // Overall Initialization

    while(1){
        if(g_sw_chg){   // Enter when SW is pressed
            g_sw_chg = 0;
            S12AD.ADCSR.BIT.ADST = 1;         // ADC start
            while(S12AD.ADCSR.BIT.ADST==1);   // wait for ADC completion
            voltage = S12AD.ADDR9*330*2/4095; // Convert to voltage
            sci_printf("voltage = %u.%u\n\r",voltage/100,voltage%100);
        }
    }
}

メイン関数では、RX631の初期化を行い、割り込み関数によってスイッチの押し込みフラグが立ったら、フラグを下げて、AD変換を行います。AD変換を開始したら、AD変換が完了するまで待ち、AD変換が完了したら、AD変換値から電圧値に変換します。その電圧値をsci_printfでUART送信します。

ADCの初期化

初期化関数のソースコードが以下です。ADC に関してはユーザーズマニュアルの p.1769「42. 12 ビット A/D コンバータ(S12ADa)」に記述があります。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_sci1(void);
static void init_adc(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 Initialization
    init_cmt0();

    // SCI1 Initialization
    init_sci1();

    // ADC Initialization
    init_adc();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

static void init_adc(void){

    MSTP(S12AD) = 0;               // S12AD Module Stop Release

    MPC.PWPR.BIT.B0WI     = 0;     // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE    = 1;     // PmnPFS Register write enable
    MPC.PE1PFS.BIT.ASEL   = 1;     // Used as an analog port
    MPC.PWPR.BYTE         = 0x80;  // Reprotect
    //PORTE.PMR.BIT.B1    = 1;     // set to Peripheral (2021/9/15 comment out)

    S12AD.ADCSR.BIT.CKS   = 3;     // PCLK/1
    S12AD.ADANS0.WORD     = 0x200; // conversion target:AN009
    S12AD.ADCSR.BIT.ADCS  = 0;     // single scan mode
}

ADCの初期化では、UARTの初期化と同様にモジュールストップ状態の解除と端子機能の変更を行います。端子機能の変更でUARTのときと少し違う点はアナログ端子として使用するので、PmnPFSレジスタのPSELではなく、ASELを変更している点です。

その後、以下のADCの設定を行います。

  • ADCSRレジスタのCKSビットを3にして、分周比を1とする。
  • ADANS0レジスタを0x200にして、AN009を変換対象とする。
  • ADCSRレジスタのADCSビットを0にして、シングルスキャンモードにする。

連続スキャンモードというのもありますが、連続でAD変換する必要はないので、シングルスキャンモードにします。

また、ソフトウェアトリガでAD変換を開始するので、ADCSRレジスタのEXTRGビットやTRGEビットは”0”、”1”どちらでも構いません。

補足:はじめ、 ADCSRレジスタのCKSビットを0(分周比:8)にしていたのですが、そうするとAD変換結果が必ず4095になってしまいました。CKSビットを3にすることでこの現象が治りました。原因は不明ですが、基板のほうに問題があるのかもしれません。

追記(2021/9/15):マニュアルp.764「22.4.3 アナログ機能を使う場合の注意事項」に「アナログ機能を使用するときは、ポートモードレジスタ(PMR)の当該ビットを “0”、ポート方向レジスタ(PDR)の当該ビットを “0” にし、当該端子を汎用入力ポートにしてから、Pmn 端子機能制御レジスタの端子機能選択ビット(PmnPFS.ASEL[1:0])を “1” にしてください。」とありますので、PMRレジスタの設定はコメントアウトしました

プログラムの実行

スイッチを押すたびに、図4のようにバッテリーの電圧値が表示されるようになりました。

図4:プログラムの実行結果
図4:プログラムの実行結果

おわりに

今回でAD変換ができました。次の記事では、赤外線LEDと赤外線センサーを使ってみたいと思います。

参考文献

RXマイコンのソフト開発(4)UART送信でHelloWorld

RXマイコン(RX631)のソフト開発の4回目です。今回は、UARTでHello Worldを出力したいと思います。

今回の記事に関しては、ほとんど以下のマウス本を参考にしています。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

UARTの回路

図1:UARTの回路
図1:UARTの回路

作成したUART部分の回路図は図1です。TXDのポートは P26/TXD1 です。TXD1を使って、RXマイコンからPCにデータを送信したいと思います。

プログラムフロー

プログラムの処理の流れは図2です。

図2:プログラムのフローチャート
図2:プログラムのフローチャート

いつものようにクロックの初期化とコンペアマッチタイマー0(CMT0)の初期化を行っています。次に、シリアルコミュニケーションインターフェース1(SCI1)を初期化します。その後、メインルーチンに入り、スイッチの押し込みを検知したら HelloWorld を UART 送信します。

プログラム

重要なプログラム部分だけ示します。sci.csci.h を除いた全てのソースコードGitHubの「4_uart」フォルダの中にソースコードがありますので、詳細を知りたい場合はご覧ください。

メイン関数

main.c:メイン関数のソースコード

#include "interface.h"
#include "sci.h"
#include "init_rx631.h"

void main(void){

    init_rx631();       // Overall Initialization

    while(1){
        if(g_sw_chg){   // Enter when SW is pressed
            g_sw_chg = 0;
            sci_printf("Hello World\n\r");
        }
    }
}

メイン関数では、RX631の初期化を行い、割り込み関数によってスイッチの押し込みフラグが立ったら、フラグを下げて、sci_printf でHelloWorldをUART送信します。

SCI1の初期化

初期化関数のソースコードが以下です。SCI に関してはユーザーズマニュアルの p.1357「35. シリアルコミュニケーションインターフェース(SCIc、SCId)」に記述があります。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_sci1(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 Initialization
    init_cmt0();

    // SCI1 Initialization
    init_sci1();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

static void init_sci1(void){

    MSTP(SCI1) = 0;             // SCI1 Module Stop Release

    MPC.PWPR.BIT.B0WI   = 0;    // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE  = 1;    // PmnPFS Register write enable
    MPC.P26PFS.BIT.PSEL = 0x0A; // set to TXD1
    MPC.PWPR.BYTE       = 0x80; // Reprotect
    PORT2.PMR.BIT.B6    = 1;    // set to Peripheral

    SCI1.SMR.BYTE       = 0x00; // PCLK/1, asynchronous,
                                // 1 stop bit, no parity bit, data length=8bit
    SCI1.SEMR.BIT.ABCS  = 1;    // 1bit transfer in 8 cycles
    SCI1.BRR            = 80;   // 38400 bps, error=0.47%

    SCI1.SCR.BIT.TE     = 1;    // Enable serial transmission
}

SCI1の初期化では、MTU3の初期化と同様にモジュールストップ状態の解除と端子機能の変更を行います。

その後、SCI1の設定を行います。SCI1.SMR.BYTE=0x00 とすることで以下のように設定されます。

  • クロックの分周比  a:1
  • ストップビットの数:1
  • パリティビットなし
  • データ長:8ビット
  • 動作モード:調歩同期式

ビットレート B については、BRRレジスタを設定することで決定します。SCI1.SEMR.BIT.ABCS=1のとき、BRRの値は以下のように計算されます。

\displaystyle{
BRR=\frac{\rm PCLK}{16 \times a \times B } - 1
}

 B=38400 bpsとしたい場合は、BRRの値は以下のようになります。

\displaystyle{
BRR=\frac{50\times 10^6}{16 \times 1 \times 38400 } - 1 =80.4
}

プログラム中では、小数点以下を四捨五入して、 BRR=80 としています。

最後に、SCI1.SCR.BIT.TE=1 で、シリアル送信を許可しています。

UART送信する関数

UART送信する関数については、マウス本のHPで公開している sci.csci_printf 関数を使わせていただきました。

マウス本のHP:https://takeyuta.wixsite.com/mouse

プログラムの実行

PCとマウスを接続したら、TeraTerm を開いて、図3のように「設定」→「シリアルポート」を実行します。

図3:TeraTermの設定1
図3:TeraTermの設定1

シリアルポートの設定では、ボーレートを38400に設定して、「OK」を左クリックします(図4)。

図4:TeraTermの設定2
図4:TeraTermの設定2

マウスのプッシュスイッチを押すと、図5のように TeraTermHello World が表示されます。

図5:プログラムの実行結果
図5:プログラムの実行結果

おわりに

今回でUART送信ができました。次の記事では、ADCを使ってバッテリーの電圧を測定したいと思います。

参考文献

RXマイコンのソフト開発(3)スピーカーから音を出す

RXマイコン(RX631)のソフト開発の3回目です。今回は、スピーカーから音を出したいと思います。MTUという機能を用いて、端子から矩形波を出力することで音を出します。この機能によって、いちいちプログラムで端子をLOW、HIGHに設定せずとも、自動で矩形波が出力されます。

環境

  • パソコン: Windows10 64 bit
    • 統合開発環境: e2studio 2021-01をインストール
    • コンパイラ: Renesas CCRX v3.03.00をインストール
    • 書き込みソフト: Renesas Flash Programmer V3.08.01(無償版) をインストール
  • ターゲットデバイス: R5F5631PDDFL(RX631 48ピン)

スピーカーの回路

図1:スピーカーの回路図
図1:スピーカーの回路図

作成したスピーカー部分の回路図は図1です。SPEAKERのポートは P14/MTIOC3A です。MTIOC3Aの機能を持った端子からは任意の周波数の矩形波を出力できるので、その機能を使って音を出力したいと思います。

プログラムフロー

プログラムの処理の流れは図2です。レジスタプロテクトの設定は省略しています。

図2:プログラムのフローチャート
図2:プログラムのフローチャート

まず、前回の記事と同様にクロックの初期化とI/Oポートの設定、コンペアマッチタイマー0(CMT0)の初期化を行っています。次に、CMT1の初期化とマルチファンクションパルスユニット2 のチャネル3 (MTU3)を初期化します。その後、メインルーチンに入り、スイッチの押し込みを検知したら音を出力します。音は0.5秒間だけ出力して、音階名でC5からC6を出力するようにします。

プログラム

重要なプログラム部分だけ示します。全てのソースコードGitHubの「3_speaker」フォルダの中にソースコードがありますので、詳細を知りたい場合はご覧ください。

メイン関数

main.c:メイン関数のソースコード

#include "define_wakaba.h"
#include "interface.h"

void main(void){
    unsigned char spk_note_num = 0;

    init_rx631();  // Overall Initialization

    while(1){ 
        if(g_sw_chg){   // Enter when SW is pressed
            g_sw_chg = 0;
            switch(spk_note_num){
            case 0:
                SPK_Soundout(C5, 50);
                break;
            case 1:
                SPK_Soundout(D5, 50);
                break;

            ~略~       

            case 7:
                SPK_Soundout(C6, 50);
                break;
            }
            spk_note_num++;
            spk_note_num = spk_note_num % 8;
        }
    }
}

メイン関数では、RX631の初期化を行い、割り込み関数によってスイッチの押し込みフラグ(g_sw_chg)が立ったら、フラグを下げて、SPK_Soundout で音を出力するようにしています。

CMT1とMTU3の初期化

初期化関数のソースコードが以下です。ここでは MTU3 の初期化について主に説明します。MTU に関してはユーザーズマニュアルの p.765「23. マルチファンクションタイマパルスユニット2(MTU2a)」に記述があります。

init_rx631.c:初期化関数のソースコード

#include "init_rx631.h"
#include "iodefine.h"

/*** Function Declaration ***/
static void init_clock(void);
static void init_cmt0(void);
static void init_cmt1(void);
static void init_mtu3(void);

/*---- RX631 Initialization ----*/
void init_rx631(void){
    SYSTEM.PRCR.WORD = 0xA503;  // Unprotect

    // MainCLK, SUBCLK and RTC Initialization
    init_clock();

    // CMT0 and CMT1 Initialization
    init_cmt0();
    init_cmt1();

    // MTU3 Initialization
    init_mtu3();

    SYSTEM.PRCR.WORD = 0xA500;  // Reprotect
}

~略~

/*---- CMT1 Initialization ----*/
void init_cmt1(void){

    MSTP(CMT1) = 0;          // CMT1 Module Stop Release

    CMT.CMSTR0.BIT.STR1 = 0; // CMT1 Stop
    CMT1.CMCR.WORD = 0x00C1; // interrupt enabled and PCLK/32
    CMT1.CMCOR = 15624;      // Set interrupt cycle to 10ms

    IPR(CMT1,CMI1) = 9;      // Priority Level 9
    IEN(CMT1,CMI1) = 1;      // Interrupt enabled
}

/*---- MTU3 Initialization ----*/
void init_mtu3(void){

    MSTP(MTU) = 0;              // MTU Module Stop Release

    MPC.PWPR.BIT.B0WI   = 0;    // PFSWE bit write enable
    MPC.PWPR.BIT.PFSWE  = 1;    // PmnPFS Register write enable
    MPC.P14PFS.BIT.PSEL = 0x01; // set to MTIOC3A
    MPC.PWPR.BYTE       = 0x80; // Reprotect
    PORT1.PMR.BIT.B4    = 1;    // set to Peripheral
    
    MTU3.TMDR.BIT.MD   = 0x2;   // PWM mode1
    MTU3.TIORH.BIT.IOA = 0x5;   // Initial output HIGH, Compare match LOW
    MTU3.TIORH.BIT.IOB = 0x6;   // Initial output HIGH, Compare match HIGH
    MTU3.TCR.BIT.TPSC  = 0x1;   // PCLK/4
    MTU3.TCR.BIT.CKEG  = 0x0;   // count by riging edge
    MTU3.TCR.BIT.CCLR  = 0x2;   // Clear by compare match TGRB
}

CMT1の初期化はCMT0の初期化とほとんど変わらないです。変化している点は割り込み周期が10msである点とカウンタを動作させていない点です。

MTU3の初期化では、まず、モジュールストップ機能によって MTU が停止しているため、モジュールストップ状態を解除します。

端子機能の変更

MPC.PWPR.BIT.B0WI   = 0;    // PFSWE bit write enable
MPC.PWPR.BIT.PFSWE  = 1;    // PmnPFS Register write enable
MPC.P14PFS.BIT.PSEL = 0x01; // set to MTIOC3A
MPC.PWPR.BYTE       = 0x80; // Reprotect
PORT1.PMR.BIT.B4    = 1;    // set to Peripheral

端子の機能を変更します。MTU3のPWMモード1で矩形波を出力したいので、P14端子の機能をMTIOC3Aに変更します。ただ、端子の機能を変更するレジスタがプロテクトされているので解除します。その解除は少し面倒で、MPC.PWPR.BIT.B0WI=0 で書き込みを可能にするビットの書き込みを可能にして、MPC.PWPR.BIT.PFSWE=1 で書き込みを可能にします。

P14 端子の選択できる機能は p.723「22.2.3 P1n 端子機能制御レジスタ(P1nPFS)(n=0 ~ 7)」に記述してあります。機能がMTIOC3Aとなるように、MPC.P14PFS.BIT.PSEL=0x01 とします。機能を選択できたら、MPC.PWPR.BYTE=0x80 で再度プロテクトします。また、端子を周辺機能として使用できるようにPORT1.PMR.BIT.B4=1でポートモードレジスタを変更します。

MTU3の設定

MTU3.TMDR.BIT.MD   = 0x2;   // PWM mode1
MTU3.TIORH.BIT.IOA = 0x5;   // Initial output HIGH, Compare match LOW
MTU3.TIORH.BIT.IOB = 0x6;   // Initial output HIGH, Compare match HIGH
MTU3.TCR.BIT.TPSC  = 0x1;   // PCLK/4
MTU3.TCR.BIT.CKEG  = 0x0;   // count by riging edge
MTU3.TCR.BIT.CCLR  = 0x2;   // Clear by compare match TGRB

MTU3の設定を変更します。今回は、PWMモード1で端子からPWM波形を出力したいので、MTU3.TMDR.BIT.MD=0x2を実行します。

図3のようにPWM波形を出力したいので、TIORH レジスタを変更して、初期出力HIGH、TGRAとコンペアマッチでLOW出力、TGRBとコンペアマッチでHIGH出力に設定します。

また、TCR レジスタの CCLR ビットを変更することで、TGRBとコンペアマッチでカウントクリアするようにします。

図3:PWMモード1の動作例
図3:PWMモード1の動作例

あとは、MTU3.TCR.BIT.TPSC = 0x1で分周比を4に設定して、MTU3.TCR.BIT.CKEG = 0x0でクロックの立ち上がりエッジでカウントするようにします。

音を出力する関数

interface.c:インターフェース用の関数のソースコード

#include "define_wakaba.h"
#include "interface.h"
#include "iodefine.h"

volatile unsigned char g_sw_chg;
static volatile unsigned char spk_cnt;

~略~

// CMT1 CMI1 Interrupt function
// Control soundout time
void SPK_CtrlCnt(void){

    // Reduce the count
    spk_cnt--;
    // Stop square wave output
    if(spk_cnt==0){
        MTU.TSTR.BIT.CST3 = 0;
        MTU3.TCNT = 0;
        CMT.CMSTR0.BIT.STR1 = 0;
        CMT1.CMCNT = 0;
    }
}

// Speaker Sound output
void SPK_Soundout(NOTE spk_note, unsigned char l_spk_cnt){
    spk_cnt = l_spk_cnt;
    MTU3.TGRA = spk_note>>2;
    MTU3.TGRB = spk_note;
    MTU.TSTR.BIT.CST3 = 1;
    CMT.CMSTR0.BIT.STR1 = 1;
}

SPK_CtrlCnt 関数は、割り込み関数の中に書かれている関数です。そのため、実質割り込み関数です。カウンタを減らして、カウンタがゼロになれば、MTU3を停止させて、カウンタTCNTをクリアさせています。また、CMT1も停止させて、カウンタCMCNTもクリアさせています。

SPK_Soundout 関数は、カウンタの初期値をまず設定してます。l_spk_cnt=50 とすれば、音を500 ms 間出力します。それから、TGRAやTGRBの値を設定して、周波数を設定します。spk_note の変数型がNOTEになっていますが、これはinterface.hに以下のように記述されたものです。

/*** type define ***/
typedef enum{
    C5 = 23900,  // 523Hz
    D5 = 21282,  // 587Hz
    E5 = 18960,  // 659Hz
    F5 = 17896,  // 698Hz
    G5 = 15943,  // 783Hz
    A5 = 14204,  // 880Hz
    B5 = 12654,  // 987Hz
    C6 = 11944   // 1046Hz
} NOTE;

各値は以下の式で決定しています。

\displaystyle{
TGRB=\frac{\rm PCLK}{分周比}\times \frac{1}{周波数} - 1
}

最後に、MTU.TSTR.BIT.CST3=1でMTU3を動作、CMT.CMSTR0.BIT.STR1 = 1でCMT1を動作させています。

プログラムの実行

プログラムを書き込むとスイッチを押すたびに以下のように音が変化します。

おわりに

今回でスピーカーから音が無事出力できました。次の記事では、デバッグを行いやすくするために、UARTで送信を行いたいと思います。

参考文献

お問い合わせフォーム プライバシーポリシー

© 2021 Setoti All rights reserved.