目次
前
次
PSG(Programable Sound Generator)
PSG(Programable Sound Generator)を、単純に周波数を変更する
矩形波発振回路から、FM音源まで変化させてみます。
PSGは、与える数値で出力される音の周波数を可変にする電子回路です。
与える数値は、ほしい周波数になるように、元の周波数を分周するために
必要になります。この与える数値を、分周比と呼びます。
ブロック図で示すと、下記のように単純です。
元の周波数を分周するためには、少し計算が必要です。
目的とする周波数の分周比は、利用する発振器の周波数に変わります。
いつも電卓を叩くのは面倒なので、Tcl/Tkで利用周波数と
目的周波数を入力すると、分周比を計算するスクリプトを
作成しました。
#!/bin/sh
wm title . "generate divider"
# clear file names
set o_frequency 0
set t_frequency 0
set result 0
# add menu on TopLevel
. configure -menu .mnuTop
menu .mnuTop
# add sub form
.mnuTop add cascade -label File -underline 0 -menu .mnuTop.file
menu .mnuTop.file -tearoff no
# add sub menu "Quit"
.mnuTop.file add command -label "Quit" -command {exit}
# set window size
set w 1000
set h 1000
label .lblOfreq -text "origin frequency(Hz)"
label .lblTfreq -text "target frequency(Hz)"
label .lblResult -textvariable result
entry .edtOfreq -textvariable o_frequency
entry .edtTfreq -textvariable t_frequency
button .btnCalc -text "calculate" -command {Calc}
pack .lblOfreq
pack .edtOfreq
pack .lblTfreq
pack .edtTfreq
pack .btnCalc
pack .lblResult
#######################
# calculate
#######################
proc Calc { } {
global o_frequency t_frequency result
# flag
set flag 0
# get origin frequency
set y $o_frequency
# get target frequency
set x $t_frequency
# judge
if { $x < 19 } {
tk_messageBox -type ok -message "frequency must be greater than 19Hz"
set flag 1
}
if { $x > 20000 } {
tk_messageBox -type ok -message "frequency must be less than 20kHz"
set flag 1
}
if { $flag == 0 } {
set result [expr $y / $x]
}
}
100kHzから、800Hzを生成する計算をしてみると、次のようになります。
人間が耳で音として認識できる周波数は、20Hz〜20kHzと言われています。
この範囲の周波数を、CPLD/FPGAで生成し、スピーカに接続すると音がでます。
これから、1チャネル矩形波発振器を作り、3チャネルに拡張し
さらにFM音源へと展開してみます。
1チャネル矩形波発振
もっとも単純なブロック図から、矩形波を発振するVHDLコードを記述します。
ブロック図を作成します。
ブロック図は、次のような視点で設計しました。
4MHzのシステムクロックを使います。
分周比を16ビットの範囲にまとめるために、1MHzに落とします。
4MHz→1MHzの変換にPRESCALERを利用します。
分周比を保存するために16ビットのレジスタREGを使います。
入力シンクロナイザで、同期化してトリガーを与えます。
分周比を保存するために16ビットのレジスタREGを使います。
入力シンクロナイザで、同期化してトリガーを与えます。
1MHzを入力クロックとするカウンタとREGの値を比較して
一致で1を出力し、それ以外は0を出力します。
ここが、矩形波発振回路の要になります。
音が常に出ていると、騒々しいので、イネーブル端子を用意し
音の停止ができるようにします。
各ブロックをまとめと、以下となります。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity stst is
Port (
-- system
nRESET : in std_logic ; -- system reset
CLOCK : in std_logic ; -- system clock (4MHz)
-- fdiv
TRGH : in std_logic ; -- upper byte
TRGL : in std_logic ; -- lower byte
DIN : in std_logic_vector(7 downto 0) ;
-- I/O
ENA : in std_logic ;
SOUT : out std_logic --;
);
end stst;
architecture Behavioral of stst is
-- sound clock
signal iSCNT : std_logic_vector(1 downto 0) ;
signal iSCLK : std_logic ;
-- input synchronizer
signal iTRGHS : std_logic_vector(3 downto 0) ;
signal iTRGH : std_logic ;
signal iTRGLS : std_logic_vector(3 downto 0) ;
signal iTRGL : std_logic ;
-- frequency divider
signal iFDIV : std_logic_vector(15 downto 0) ;
-- frequency counter
signal iFCNT : std_logic_vector(15 downto 0) ;
signal iSOUT : std_logic ;
begin
-- prescaler (generate 1MHz)
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSCNT <= "00" ;
elsif rising_edge( CLOCK ) then
-- increment
iSCNT <= iSCNT + '1' ;
end if ;
end process ;
iSCLK <= '1' when ( iSCNT = "00" ) else '0' ;
-- input synchronizer
process ( nRESET , iSCLK )
begin
if ( nRESET = '0' ) then
iTRGHS <= "1111" ;
iTRGLS <= "1111" ;
elsif rising_edge( iSCLK ) then
-- shift in
iTRGHS <= iTRGHS(2 downto 0) & TRGH ;
iTRGLS <= iTRGLS(2 downto 0) & TRGL ;
end if ;
end process ;
iTRGH <= '1' when ( iTRGHS = "0000" ) else '0' ;
iTRGL <= '1' when ( iTRGLS = "0000" ) else '0' ;
-- frequency divider latch
process ( nRESET , iSCLK )
begin
if ( nRESET = '0' ) then
iFDIV <= (others => '0') ;
elsif rising_edge( iSCLK ) then
-- latch
if ( iTRGH = '1' ) then
iFDIV(15 downto 8) <= DIN ;
end if ;
if ( iTRGL = '1' ) then
iFDIV( 7 downto 0) <= DIN ;
end if ;
end if ;
end process ;
-- frequency divider
process ( nRESET , iSCLK )
begin
if ( nRESET = '0' ) then
iFCNT <= (others => '0') ;
elsif rising_edge( iSCLK ) then
-- increment
iFCNT <= iFCNT + '1' ;
-- judge
if ( iFCNT = iFDIV ) then
iFCNT <= (others => '0') ;
end if ;
end if ;
end process ;
iSOUT <= '1' when ( conv_integer(iFCNT) = 0 ) else '0' ;
-- sound out
SOUT <= iSOUT when ( ENA = '1' ) else '0' ;
end Behavioral;
このVHDLコードは、CoolRunnerIIのボードにスピーカを
接続してテストしました。
分周比は、8ビットのDIPスイッチを1個利用して設定します。
また、トリガーには、プッシュスイッチを2個使いました。
矩形波を出すので、音は綺麗ではありませんが、与える分周比で
音階を作りだすので、PSGの雛形と呼べるでしょう。
3チャネル矩形波発振
1チャネル分の矩形波発振ができたので、和音がでるように
拡張します。
往年のPSGであるAY-3-8910は、発振器を3チャネルもつので
敬意を表し、3チャネル分の矩形波発振を作成します。
音の周波数を生成器は、3チャネルでも、利用する発振器が
1個であることが重要です。
言い換えると、音の周波数を生成する発振器が1個でも
デジタル回路を並べれば、好きな数の音の周波数発振器
を作れることです。
1チャネルの矩形波発振器はあるので、分周回路をコピー
して3チャネル分用意すると、3チャネル矩形波発振器の
完成です。
VHDLでは、1つのブロックを定義しておき、使い回すときには
component記述を利用します。
component記述により、同一回路ブロックを複数用意し、利用できます。
1チャネル矩形発振器で作成した、次のブロックを複数用意します。
各ブロックを定義してみます。
分周比レジスタ
16ビットレジスタに、値を設定すればよいので
内部にsignalでレジスタを確保します。
signal iREG : std_logic_vector(15 downto 0) ;
16ビットレジスタ値の入力をDINと出力DOUTとして
TRGHが1のとき、上位8ビットにラッチします。
TRGLが1のとき、下位8ビットにラッチします。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iREG <= (others => '0') ;
elsif rising_edge( CLOCK ) then
-- latch
if ( TRGH = '1' ) then
iREG(15 downto 8) <= DIN ;
end if ;
if ( TRGL = '1' ) then
iREG( 7 downto 0) <= DIN ;
end if ;
end if ;
end process ;
DOUT <= iREG ;
コンポーネントとして定義します。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity fdlatch is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- trigger
TRGH : in std_logic ;
TRGL : in std_logic ;
-- data
DIN : in std_logic_vector(7 downto 0) ;
-- result
DOUT : out std_logic_vector(15 downto 0) --;
);
end fdlatch;
architecture Behavioral of fdlatch is
signal iREG : std_logic_vector(15 downto 0) ;
begin
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iREG <= (others => '0') ;
elsif rising_edge( CLOCK ) then
-- latch
if ( TRGH = '1' ) then
iREG(15 downto 8) <= DIN ;
end if ;
if ( TRGL = '1' ) then
iREG( 7 downto 0) <= DIN ;
end if ;
end if ;
end process ;
DOUT <= iREG ;
end Behavioral;
分周ブロック
16ビットレジスタの値と比較するので、カウンタを
16ビットバイナリとします。
signal iCNT : std_logic_vector(15 downto 0) ;
比較した結果の論理値を格納するレジスタを用意します。
signal iSOUT : std_logic ;
16ビットバイナリカウンタのインクリメントとゼロクリア
を同期して実行します。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iCNT <= (others => '0') ;
elsif rising_edge( CLOCK ) then
-- increment
iCNT <= iCNT + '1' ;
-- judge
if ( iCNT = DIN ) then
iCNT <= (others => '0') ;
end if ;
end if ;
end process ;
比較処理は、単純なデコーダにします。
iSOUT <= '1' when ( conv_integer(iCNT) = 0 ) else '0' ;
デコーダの結果をENABLEにより、出力あるいは停止します。
SOUT <= iSOUT when ( ENABLE = '1' ) else '0' ;
コンポーネントとして定義します。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity fcnt is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- data
DIN : in std_logic_vector(15 downto 0) ;
-- enable
ENABLE : in std_logic ;
-- result
SOUT : out std_logic --;
);
end fcnt;
architecture Behavioral of fcnt is
signal iCNT : std_logic_vector(15 downto 0) ;
signal iSOUT : std_logic ;
begin
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iCNT <= (others => '0') ;
elsif rising_edge( CLOCK ) then
-- increment
iCNT <= iCNT + '1' ;
-- judge
if ( iCNT = DIN ) then
iCNT <= (others => '0') ;
end if ;
end if ;
end process ;
iSOUT <= '1' when ( conv_integer(iCNT) = 0 ) else '0' ;
SOUT <= iSOUT when ( ENABLE = '1' ) else '0' ;
end Behavioral;
入力シンクロナイザ
1チャネル分のトリガーを、シフトレジスタでラッチします。
signal iTRGS : std_logic_vector(3 downto 0);
シフトレジスタに格納した内容から、出力を確定します。
signal iTRGH : std_logic;
signal iTRGL : std_logic;
クロックで、トリガーの値をラッチしていき、4ビットの
内容で、トリガー値を確定します。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iTRGHS <= "1111" ;
iTRGLS <= "1111" ;
elsif rising_edge( CLOCK ) then
-- shift in
iTRGHS <= iTRGHS(2 downto 0) & TRGH ;
iTRGLS <= iTRGLS(2 downto 0) & TRGL ;
end if ;
end process ;
iTRGH <= '1' when ( iTRGHS = "0000" ) else '0' ;
iTRGL <= '1' when ( iTRGLS = "0000" ) else '0' ;
コンポーネントとして定義します。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity isync is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- trigger
TRGH : in std_logic ;
TRGL : in std_logic ;
-- result
RTRGH : out std_logic ;
RTRGL : out std_logic --;
);
end isync;
architecture Behavioral of isync is
signal iTRGHS : std_logic_vector(3 downto 0);
signal iTRGH : std_logic;
signal iTRGLS : std_logic_vector(3 downto 0);
signal iTRGL : std_logic;
begin
-- input synchronizer
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iTRGHS <= "1111" ;
iTRGLS <= "1111" ;
elsif rising_edge( CLOCK ) then
-- shift in
iTRGHS <= iTRGHS(2 downto 0) & TRGH ;
iTRGLS <= iTRGLS(2 downto 0) & TRGL ;
end if ;
end process ;
iTRGH <= '1' when ( iTRGHS = "1000" ) else '0' ;
iTRGL <= '1' when ( iTRGLS = "1000" ) else '0' ;
RTRGH <= iTRGH ;
RTRGL <= iTRGL ;
end Behavioral;
各ブロックを定義したので、入出力信号とプリスケーラの動作を
確定します。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity sound0 is
Port (
-- system
nRESET : in std_logic ; -- system reset
CLOCK : in std_logic ; -- system clock (4MHz)
-- fdiv
TRGH : in std_logic_vector(2 downto 0) ;
TRGL : in std_logic_vector(2 downto 0) ;
DIN : in std_logic_vector(7 downto 0) ;
-- I/O
ENA : in std_logic ;
ENB : in std_logic ;
ENC : in std_logic ;
SOUT : out std_logic_vector(2 downto 0) --;
);
end sound0;
architecture Behavioral of sound0 is
-- sound clock
signal iSCNT : std_logic_vector(1 downto 0) ;
signal iSCLK : std_logic ;
-- input synchronizer
component isync
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- trigger
TRGH : in std_logic ;
TRGL : in std_logic ;
-- result
RTRGH : out std_logic ;
RTRGL : out std_logic --;
);
end component ;
signal iTRGHA : std_logic ;
signal iTRGHB : std_logic ;
signal iTRGHC : std_logic ;
signal iTRGLA : std_logic ;
signal iTRGLB : std_logic ;
signal iTRGLC : std_logic ;
-- frequency latch
component fdlatch
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- trigger
TRGH : in std_logic ;
TRGL : in std_logic ;
-- data
DIN : in std_logic_vector(7 downto 0) ;
-- result
DOUT : out std_logic_vector(15 downto 0) --;
);
end component ;
signal iFDIVA : std_logic_vector(15 downto 0) ;
signal iFDIVB : std_logic_vector(15 downto 0) ;
signal iFDIVC : std_logic_vector(15 downto 0) ;
-- frequency counter
component fcnt
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- data
DIN : in std_logic_vector(15 downto 0) ;
-- enable
ENABLE : in std_logic ;
-- result
SOUT : out std_logic --;
);
end component;
signal iSOUT : std_logic_vector(2 downto 0) ;
begin
-- generate sound clock (100kHz)
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSCNT <= "00" ;
elsif rising_edge( CLOCK ) then
-- increment
iSCNT <= iSCNT + '1' ;
end if ;
end process ;
iSCLK <= '1' when ( iSCNT = "00" ) else '0' ;
-- input synchronizer
ISA : isync port map (nRESET,iSCLK,TRGH(0),TRGL(0),iTRGHA,iTRGLA) ;
ISB : isync port map (nRESET,iSCLK,TRGH(1),TRGL(1),iTRGHB,iTRGLB) ;
ISC : isync port map (nRESET,iSCLK,TRGH(2),TRGL(2),iTRGHC,iTRGLC) ;
-- frequency divider latch
FD_A : fdlatch port map (nRESET,iSCLK,iTRGHA,iTRGLA,DIN,iFDIVA);
FD_B : fdlatch port map (nRESET,iSCLK,iTRGHB,iTRGLB,DIN,iFDIVB);
FD_C : fdlatch port map (nRESET,iSCLK,iTRGHC,iTRGLC,DIN,iFDIVC);
-- frequency counter
FCT_A : fcnt port map (nRESET,iSCLK,iFDIVA,ENA,iSOUT(0));
FCT_B : fcnt port map (nRESET,iSCLK,iFDIVB,ENB,iSOUT(1));
FCT_C : fcnt port map (nRESET,iSCLK,iFDIVC,ENC,iSOUT(2));
-- sound out
SOUT <= iSOUT(2) & iSOUT(1) & iSOUT(0) ;
end Behavioral;
4つのVHDLコードを利用して、回路情報を生成したなら、実機に
ダウンロードしてテストします。
3チャネル矩形波発振回路に、3つの分周比を設定し、音を出す止める
を信号線で設定する作業は、結構しんどいものです。
この作業をマイコンにやらせると楽です。
Music Sequencerと呼ばれる装置は、3〜6個程度の分周比の設定
と継続時間をメモリに保存します。
保存してある情報を使い、楽器の演奏をします。
シリアルインタフェース3チャネル矩形波発振
マイコンから、3チャネル矩形波発振を制御する場合
各チャネルの分周比をパラレルで入力すると、ピンが
不足します。
ピン不足を解消する目的で、各チャネルの分周比をシリアルで
転送する仕様に変更します。
3チャネルのレジスタで、クロックとLOAD信号を共通にし
データだけを別に用意して、分周比を設定します。
このブロックの動作を定義してみます。
process ( nRESET , SCLK )
begin
if ( nRESET = '0' ) then
iSA_REG <= (others => '0') ;
iSB_REG <= (others => '0') ;
iSC_REG <= (others => '0') ;
elsif rising_edge( SCLK ) then
if ( LOAD = '1' ) then
iSA_REG <= iSA_REG(14 downto 0) & SA ;
iSB_REG <= iSB_REG(14 downto 0) & SB ;
iSC_REG <= iSC_REG(14 downto 0) & SC ;
end if ;
end if ;
end process ;
LOAD信号が'1'のときに、シリアル1ビット入力の値をラッチして
同時にシフトします。
この処理を入れたトップレベルを定義します。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity sound1 is
Port (
-- system
nRESET : in std_logic ; -- system reset
CLOCK : in std_logic ; -- system clock (4MHz)
-- fdiv
SCLK : in std_logic ;
LOAD : in std_logic ;
SA : in std_logic ;
SB : in std_logic ;
SC : in std_logic ;
-- I/O
ENA : in std_logic ;
ENB : in std_logic ;
ENC : in std_logic ;
SOUT : out std_logic_vector(2 downto 0) --;
);
end sound1;
architecture Behavioral of sound1 is
-- sound clock
signal iSCNT : std_logic_vector(1 downto 0) ;
signal iSCLK : std_logic ;
-- data latch
signal iSA_REG : std_logic_vector(15 downto 0) ;
signal iSB_REG : std_logic_vector(15 downto 0) ;
signal iSC_REG : std_logic_vector(15 downto 0) ;
-- frequency counter
component fcnt
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- data
DIN : in std_logic_vector(15 downto 0) ;
-- enable
ENABLE : in std_logic ;
-- result
SOUT : out std_logic --;
);
end component;
signal iSOUT : std_logic_vector(2 downto 0) ;
begin
-- generate sound clock (100kHz)
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSCNT <= "00" ;
elsif rising_edge( CLOCK ) then
-- increment
iSCNT <= iSCNT + '1' ;
end if ;
end process ;
iSCLK <= '1' when ( iSCNT = "00" ) else '0' ;
-- fdiv
process ( nRESET , SCLK )
begin
if ( nRESET = '0' ) then
iSA_REG <= (others => '0') ;
iSB_REG <= (others => '0') ;
iSC_REG <= (others => '0') ;
elsif rising_edge( SCLK ) then
if ( LOAD = '1' ) then
iSA_REG <= iSA_REG(14 downto 0) & SA ;
iSB_REG <= iSB_REG(14 downto 0) & SB ;
iSC_REG <= iSC_REG(14 downto 0) & SC ;
end if ;
end if ;
end process ;
-- frequency counter
FCT_A : fcnt port map (nRESET,iSCLK,iSA_REG,ENA,iSOUT(0));
FCT_B : fcnt port map (nRESET,iSCLK,iSB_REG,ENB,iSOUT(1));
FCT_C : fcnt port map (nRESET,iSCLK,iSC_REG,ENC,iSOUT(2));
-- sound out
SOUT <= iSOUT(2) & iSOUT(1) & iSOUT(0) ;
end Behavioral;
分周処理は、既に定義してあるブロックを流用しました。
20ピンのマイコンATtiny2313で、動かせるかを検証してみます。
- nRESET(リセット)に、1ピンを割り当てる(PB5)
- CLOCKは、タイマー割込みの出力に接続(PB3)
- LOADに、1ピンを割り当てる(PB4)
- SA、SB、SCに、3ピンを割り当てる(PB0,PB1,PB2)
- EA、EB、ECに、3ピンを割り当てる(PD4,PD5,PD6)
合計9ピンあれば、音を出す制御はできます。
ATtiny2313のシリアル接続を利用し、パーソナルコンピュータから
このマイコンのSRAMに、分周比、音を出すチャネル指定、音を出す
時間を保存すれば、マイコンをMusic Sequencerに仕立てることも
可能です。
Music Sequencerを、CPLD/FPGAで作成してみます。
Music Sequencer
シリアルインタフェースをもつ3チャネル矩形波発振器
を制御する、Music Sequencerの仕様を考えます。
- 外付けするSRAMに、分周比、音を出すチャネル、音の継続時間を保存する
- トリガーを与えて、SRAM中の情報を取り出す
- 音に関する情報を3チャネル矩形波発振器に転送
この程度の大雑把な仕様から、ブロック図を作成し、段階的に
詳細を詰めていきます。
ブロック図から、以下のインタフェースブロックが必要だと
思いつくでしょう。
- SRAMに情報を書き込む
- SRAMから情報を取得する
SRAMに情報を書き込む処理は、Music Sequencerではなく
マイコンにやってもらうことにします。
マイコンが、制御、アドレス、データの各信号を使えるよう
Music Sequencerは、これらの信号をハイインピーダンスに
します。そのための信号が、Enableです。
Enableを'1'にした場合、Music SequencerがSRAMにアクセス
できるようにします。
Enableが'0'では、制御、アドレス、データは、ハイインピーダンス
とします。
SRAMから情報を取得して、PSGを制御するため
トリガー信号triggerを利用します。
ここまで仕様を固めると、Entity定義ができます。
entity musicseq is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- SRAM interface
ENABLE : in std_logic ;
nCS : out std_logic ;
nRD : out std_logic ;
ADR : out std_logic_vector(14 downto 0) ;
SDATA : in std_logic_vector( 7 downto 0) ;
-- trigger
TRG : in std_logic ;
-- clock
SCLOCK : out std_logic ;
-- fdiv
LOAD : out std_logic ;
SA : out std_logic ;
SB : out std_logic ;
SC : out std_logic ;
-- I/O
ENA : out std_logic ;
ENB : out std_logic ;
ENC : out std_logic --;
) ;
end musicseq;
SRAMを32kバイトとして、Music Sequencerの動作を定義します。
SRAMにいれる情報のフォーマットを、決めておきます。
+0 channel_A upper divider
+1 channel_A lower divider
+2 channel_B upper divider
+3 channel_B lower divider
+4 channel_C upper divider
+5 channel_C lower divider
+6 enable channel bit
+7 time count
8バイトで、1回の動作を指定します。
32kバイトのSRAMでは、4096通りの音の出し方を記述できます。
Music SequencerのPSG制御シーケンスを考えます。
- トリガー待ち
- SRAMから情報取得
- 分周比転送
- 動作チャネル指定
- 動作時間待ち
- 4096回処理したなら1に、それ以外では2にもどる
シーケンスをVHDLで記述します。
type stype is (S0,S1,S2,S3,S4,S5,S6,S7,S8);
signal iSTATE : stype ;
signal iCOUNT : std_logic_vector(12 downto 0);
signal iADR : std_logic_vector(14 downto 3);
signal iGET_TRG : std_logic ;
signal iGET_END : std_logic ;
signal iSTORE_TRG : std_logic ;
signal iSTORE_END : std_logic ;
signal iTIME_TRG : std_logic ;
signal iTIME_END : std_logic ;
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSTATE <= S0 ;
iCOUNT <= (others => '0');
iADR <= (others => '0');
elsif rising_edge( CLOCK ) then
case iSTATE is
-- trigger wait
when S0 =>
if ( TRG = '1' and ENABLE = '1' ) then
iCOUNT <= (others => '0') ;
iADR <= (others => '0');
iSTATE <= S1 ;
end if ;
-- judge
when S1 =>
if ( iCOUNT(12) = '1' ) then
iSTATE <= S8 ;
else
iSTATE <= S2 ;
end if ;
-- get information
when S2 =>
iSTATE <= S3 ;
-- wait
when S3 =>
if ( iGET_END = '1' ) then
iSTATE <= S4 ;
else
iSTATE <= S3 ;
end if ;
-- store frequency divider
when S4 =>
iSTATE <= S5 ;
-- wait
when S5 =>
if ( iSTORE_END = '1' ) then
iSTATE <= S6 ;
else
iSTATE <= S5 ;
end if ;
-- enable channel
when S6 =>
iSTATE <= S7 ;
-- sustine time
when S7 =>
if ( iTIME_END = '1' ) then
iSTATE <= S6 ;
iCOUNT <= iCOUNT + '1' ;
iADR <= iADR + 1 ;
else
iSTATE <= S1 ;
end if ;
-- return first state
when S8 =>
iSTATE <= S0 ;
-- default
when others =>
iSTATE <= S0 ;
end case ;
end if ;
end process ;
SRAMから情報を取得するブロックを定義します。
SRAMのリードアクセスは、次のシーケンスになっています。
- nCSをイネーブル
- アドレス出力
- nRDをイネーブル
- 出力データラッチ
- nRDをディセーブル
- nCSをディセーブル
8バイトのデータを取得するので、アドレスを出力して
nRDを制御しながら、SRAMが出力するデータをラッチします。
type atype is (AS0,AS1,AS2,AS3,AS4,AS5,AS6,AS7) ;
signal iACUR : atype ;
signal iADRX : std_logic_vector(14 downto 0);
signal iACNT : std_logic_vector( 3 downto 0);
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iACUR <= AS0 ;
iADRX <= (others => '0');
nCSi <= '1' ;
elsif rising_edge( CLOCK ) then
case iSTATE is
-- wait trigger
when AS0 =>
if ( iGET_TRG = '1' ) then
iADRX(14 downto 3) <= iADR ;
iADRX( 2 dwonto 0) <= "000" ;
iACNT <= "0000" ;
iACUR <= AS1 ;
end if ;
-- judge
when AS1 =>
if ( iACNT(3) = '1' ) then
iACUR <= AS7 ;
else
iACUR <= AS2 ;
end if ;
-- enable nCS
when AS2 =>
nCSi <= '0' ;
iACUR <= AS3 ;
-- enable nRD
when AS3 =>
iACUR <= AS4 ;
-- latch
when AS4 =>
iACUR <= AS5 ;
-- disable nRD
when AS5 =>
case iACNT is
when "0000" =>
iSA_REG(15 downto 8) <= SDATA ;
when "0001" =>
iSA_REG( 7 downto 0) <= SDATA ;
when "0010" =>
iSB_REG(15 downto 8) <= SDATA ;
when "0011" =>
iSB_REG( 7 downto 0) <= SDATA ;
when "0100" =>
iSC_REG(15 downto 8) <= SDATA ;
when "0101" =>
iSC_REG( 7 downto 0) <= SDATA ;
when "0110" =>
iCHANNEL <= SDATA(2 downto 0) ;
when "0111" =>
iTIME <= SDATA ;
when others =>
NULL ;
end case ;
iACUR <= AS6 ;
-- increment
when AS6 =>
iACNT <= iACNT + '1' ;
iADR <= iADR + '1' ;
iACUR <= AS1 ;
-- disable nCS
when AS7 =>
nCSi <= '1' ;
iACUR <= AS0 ;
-- default
when others =>
iACUR <= AS0 ;
end case ;
end if ;
end process ;
iGET_END <= '1' when ( iACUR = AS7 ) else '0' ;
nRDi <= '0' when ( iACUR = AS3 or iACUR = AS4 ) else '1' ;
nCS <= nCSi when ( ENABLE = '1' ) else 'Z' ;
nRD <= nRDi when ( ENABLE = '1' ) else 'Z' ;
ADR <= iADRX when ( ENABLE = '1' ) else (others => 'Z') ;
SRAMから、音を出すチャネルを指定されたならば、それを
PSGに転送します。
ENA <= iCHANNEL(0) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
ENB <= iCHANNEL(1) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
ENC <= iCHANNEL(2) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
音を出している時間を制御するブロックを定義します。
単純にカウンタをデクリメントして対応します。
ただし、100ms単位で考えます。
動作シーケンスを定義します。
type ctype is (CS0,CS1,CS2,CS3,CS4,CS5) ;
signal iCCUR : ctype ;
signal iCCNT : std_logic_vector(7 downto 0);
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iCCUR <= CS0 ;
iCCNT <= (others => '0');
elsif rising_edge( CLOCK ) then
case iCCUR is
-- wait trigger
when CS0 =>
if ( iTIME_TRG = '1' ) then
iCCNT <= (others => '0') ;
iCCUR <= CS1 ;
end if ;
-- judge
when CS1 =>
if ( iCCNT = iTIME ) then
iCCUR <= CS4 ;
else
iCCUR <= CS2 ;
end if ;
-- wait 10ms
when CS2 =>
if ( conv_integer(iTECNTX) = 625 ) then
iCCUR <= CS3 ;
end if ;
-- increment
when CS3 =>
iCCNT <= iCCNT + '1' ;
iCCUR <= CS1 ;
-- set end flag
when CS4 =>
iCCUR <= CS5 ;
-- return first state
when CS5 =>
iCCUR <= CS0 ;
-- default
when others =>
iCCUR <= CS0 ;
end case ;
end if ;
end process ;
100msの時間経過は、他のブロックのカウンタの値を参照して
確認します。
時間待ち処理は、2段階で実現します。
1MHzでシーケンサを動かしているとして、1段目で8分周します。
125kHzのクロックができるので、100msは100Hzであることから
2段目で1250分周します。
100msの時間経過は、分周カウンタが625になったときで判定します。
処理内容は、単純に2つのカウンタを動かすだけです。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iTECNT <= "000" ;
elsif rising_edge( CLOCK ) then
iTECNT <= iTECNT + '1' ;
end if ;
end process ;
iTCLOCK <= '1' when ( iTECNT = "000" ) else '0' ;
process ( nRESET , iTCLOCK )
begin
if ( nRESET = '0' ) then
iTECNTX <= (others => '0') ;
elsif rising_edge( iTCLOCK ) then
iTECNTX <= iTECNTX + '1' ;
if ( conv_integer(iTECNTX) = 1250 ) then
iTECNTX <= (others => '0') ;
end if ;
end if ;
end process ;
全ソースコードは、以下となります。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity musicseq is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- SRAM interface
ENABLE : in std_logic ;
nCS : out std_logic ;
nRD : out std_logic ;
ADR : out std_logic_vector(14 downto 0) ;
SDATA : in std_logic_vector( 7 downto 0) ;
-- trigger
TRG : in std_logic ;
-- clock
SCLOCK : out std_logic ;
-- fdiv
LOAD : out std_logic ;
SA : out std_logic ;
SB : out std_logic ;
SC : out std_logic ;
-- I/O
ENA : out std_logic ;
ENB : out std_logic ;
ENC : out std_logic --;
) ;
end musicseq;
architecture Behavioral of musicseq is
-- sequencer
type stype is (S0,S1,S2,S3,S4,S5,S6,S7,S8);
signal iSTATE : stype ;
signal iCOUNT : std_logic_vector(12 downto 0);
signal iADR : std_logic_vector(14 downto 3);
signal iGET_TRG : std_logic ;
signal iGET_END : std_logic ;
signal iSTORE_TRG : std_logic ;
signal iSTORE_END : std_logic ;
signal iTIME_TRG : std_logic ;
signal iTIME_END : std_logic ;
-- frequency divider registers
component store_seq
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- data
PSA : in std_logic_vector(15 downto 0) ;
PSB : in std_logic_vector(15 downto 0) ;
PSC : in std_logic_vector(15 downto 0) ;
-- trigger
TRG : in std_logic ;
-- fdiv
SCLK : out std_logic ;
LOAD : out std_logic ;
SA : out std_logic ;
SB : out std_logic ;
SC : out std_logic ;
SEND : out std_logic --;
) ;
end component ;
signal iSA_REG : std_logic_vector(15 downto 0);
signal iSB_REG : std_logic_vector(15 downto 0);
signal iSC_REG : std_logic_vector(15 downto 0);
signal iCHANNEL: std_logic_vector( 2 downto 0);
signal iTIME : std_logic_vector( 7 downto 0);
-- SRAM access
type atype is (AS0,AS1,AS2,AS3,AS4,AS5,AS6) ;
signal iACUR : atype ;
signal iADRX : std_logic_vector(14 downto 0);
signal iACNT : std_logic_vector( 3 downto 0);
signal nCSi : std_logic ;
signal nRDi : std_logic ;
-- time count
type ctype is (CS0,CS1,CS2,CS3,CS4,CS5) ;
signal iCCUR : ctype ;
signal iCCNT : std_logic_vector(7 downto 0);
-- time edge 100ms
signal iTECNT : std_logic_vector(2 downto 0);
signal iTCLOCK : std_logic ;
signal iTECNTX : std_logic_vector(10 downto 0);
begin
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSTATE <= S0 ;
iCOUNT <= (others => '0');
iADR <= (others => '0');
elsif rising_edge( CLOCK ) then
case iSTATE is
-- trigger wait
when S0 =>
if ( TRG = '1' and ENABLE = '1' ) then
iCOUNT <= (others => '0') ;
iADR <= (others => '0');
iSTATE <= S1 ;
end if ;
-- judge
when S1 =>
if ( iCOUNT(12) = '1' ) then
iSTATE <= S8 ;
else
iSTATE <= S2 ;
end if ;
-- get information
when S2 =>
iSTATE <= S3 ;
-- wait
when S3 =>
if ( iGET_END = '1' ) then
iSTATE <= S4 ;
else
iSTATE <= S3 ;
end if ;
-- store frequency divider
when S4 =>
iSTATE <= S5 ;
-- wait
when S5 =>
if ( iSTORE_END = '1' ) then
iSTATE <= S6 ;
else
iSTATE <= S5 ;
end if ;
-- enable channel
when S6 =>
iSTATE <= S7 ;
-- sustine time
when S7 =>
if ( iTIME_END = '1' ) then
iSTATE <= S6 ;
iCOUNT <= iCOUNT + '1' ;
iADR <= iADR + 1 ;
else
iSTATE <= S1 ;
end if ;
-- return first state
when S8 =>
iSTATE <= S0 ;
-- default
when others =>
iSTATE <= S0 ;
end case ;
end if ;
end process ;
-- trigger
iGET_TRG <= '1' when ( iSTATE = S2 ) else '0' ;
iSTORE_TRG <= '1' when ( iSTATE = S4 ) else '0' ;
iTIME_TRG <= '1' when ( iSTATE = S6 ) else '0' ;
-- get information
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iACUR <= AS0 ;
iADRX <= (others => '0');
iACNT <= "0000" ;
iSA_REG <= (others => '0') ;
iSB_REG <= (others => '0') ;
iSC_REG <= (others => '0') ;
iCHANNEL<= "000" ;
iTIME <= (others => '0') ;
nCSi <= '1' ;
elsif rising_edge( CLOCK ) then
case iACUR is
-- wait trigger
when AS0 =>
if ( iGET_TRG = '1' ) then
iADRX(14 downto 3) <= iADR ;
iADRX( 2 downto 0) <= "000" ;
iACNT <= "0000" ;
iACUR <= AS1 ;
end if ;
-- judge
when AS1 =>
if ( iACNT(3) = '1' ) then
iACUR <= AS6 ;
else
nCSi <= '0' ;
iACUR <= AS2 ;
end if ;
-- enable nRD
when AS2 =>
iACUR <= AS3 ;
-- latch
when AS3 =>
case iACNT is
when "0000" =>
iSA_REG(15 downto 8) <= SDATA ;
when "0001" =>
iSA_REG( 7 downto 0) <= SDATA ;
when "0010" =>
iSB_REG(15 downto 8) <= SDATA ;
when "0011" =>
iSB_REG( 7 downto 0) <= SDATA ;
when "0100" =>
iSC_REG(15 downto 8) <= SDATA ;
when "0101" =>
iSC_REG( 7 downto 0) <= SDATA ;
when "0110" =>
iCHANNEL <= SDATA(2 downto 0) ;
when "0111" =>
iTIME <= SDATA ;
when others =>
NULL ;
end case ;
iACUR <= AS4 ;
-- disable nRD
when AS4 =>
iACUR <= AS5 ;
-- increment
when AS5 =>
iACNT <= iACNT + '1' ;
iADRX <= iADRX + '1' ;
iACUR <= AS1 ;
-- disable nCS
when AS6 =>
nCSi <= '1' ;
iACUR <= AS0 ;
-- default
when others =>
iACUR <= AS0 ;
end case ;
end if ;
end process ;
iGET_END <= '1' when ( iACUR = AS6 ) else '0' ;
nRDi <= '0' when ( iACUR = AS2 or iACUR = AS3 ) else '1' ;
nCS <= nCSi when ( ENABLE = '1' ) else 'Z' ;
nRD <= nRDi when ( ENABLE = '1' ) else 'Z' ;
ADR <= iADRX when ( ENABLE = '1' ) else (others => 'Z') ;
ENA <= iCHANNEL(0) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
ENB <= iCHANNEL(1) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
ENC <= iCHANNEL(2) when ( iSTATE = S6 or iSTATE = S7 ) else '0' ;
-- frequency divider registers
STORE_SEQP : store_seq port map (nRESET,CLOCK,
iSA_REG,iSB_REG,iSC_REG,iSTORE_TRG,SCLOCK,LOAD,SA,SB,SC,iSTORE_END) ;
-- time count
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iCCUR <= CS0 ;
iCCNT <= (others => '0');
elsif rising_edge( CLOCK ) then
case iCCUR is
-- wait trigger
when CS0 =>
if ( iTIME_TRG = '1' ) then
iCCNT <= (others => '0') ;
iCCUR <= CS1 ;
end if ;
-- judge
when CS1 =>
if ( iCCNT = iTIME ) then
iCCUR <= CS4 ;
else
iCCUR <= CS2 ;
end if ;
-- wait 10ms
when CS2 =>
if ( conv_integer(iTECNTX) = 625 ) then
iCCUR <= CS3 ;
end if ;
-- increment
when CS3 =>
iCCNT <= iCCNT + '1' ;
iCCUR <= CS1 ;
-- set end flag
when CS4 =>
iCCUR <= CS5 ;
-- return first state
when CS5 =>
iCCUR <= CS0 ;
-- default
when others =>
iCCUR <= CS0 ;
end case ;
end if ;
end process ;
iTIME_END <= '1' when ( iCCUR = CS5 ) else '0' ;
-- time edge 100ms
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iTECNT <= "000" ;
elsif rising_edge( CLOCK ) then
iTECNT <= iTECNT + '1' ;
end if ;
end process ;
iTCLOCK <= '1' when ( iTECNT = "000" ) else '0' ;
process ( nRESET , iTCLOCK )
begin
if ( nRESET = '0' ) then
iTECNTX <= (others => '0') ;
elsif rising_edge( iTCLOCK ) then
iTECNTX <= iTECNTX + '1' ;
if ( conv_integer(iTECNTX) = 1250 ) then
iTECNTX <= (others => '0') ;
end if ;
end if ;
end process ;
end Behavioral;
この他に、componentで1ブロック利用しているので、その
VHDLコードも記しておきます。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity store_seq is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- data
PSA : in std_logic_vector(15 downto 0) ;
PSB : in std_logic_vector(15 downto 0) ;
PSC : in std_logic_vector(15 downto 0) ;
-- trigger
TRG : in std_logic ;
-- fdiv
SCLK : out std_logic ;
LOAD : out std_logic ;
SA : out std_logic ;
SB : out std_logic ;
SC : out std_logic ;
SEND : out std_logic --;
) ;
end store_seq;
architecture Behavioral of store_seq is
-- sequencer
type stype is (S0,S1,S2,S3,S4,S5);
signal iSTATE : stype ;
signal iCOUNT : std_logic_vector(4 downto 0);
begin
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSTATE <= S0 ;
iCOUNT <= "00000" ;
elsif rising_edge( CLOCK ) then
case iSTATE is
-- trigger wait
when S0 =>
if ( TRG = '1' ) then
iSTATE <= S1 ;
iCOUNT <= "00000" ;
end if ;
-- judge
when S1 =>
if ( iCOUNT(4) = '1' ) then
iSTATE <= S5 ;
else
iSTATE <= S2 ;
end if ;
-- impress data
when S2 =>
iSTATE <= S3 ;
-- clock H
when S3 =>
iSTATE <= S4 ;
-- clock L and count up
when S4 =>
iCOUNT <= iCOUNT + '1' ;
iSTATE <= S1 ;
-- return first state
when S5 =>
iSTATE <= S0 ;
-- default
when others =>
iSTATE <= S0 ;
end case ;
end if ;
end process ;
SCLK <= '1' when ( iSTATE = S3 ) else '0' ;
LOAD <= '1' when ( iSTATE > S0 ) else '0' ;
SA <= PSA(15 - conv_integer(iCOUNT)) when ( iSTATE > S0 ) else '0' ;
SB <= PSB(15 - conv_integer(iCOUNT)) when ( iSTATE > S0 ) else '0' ;
SC <= PSC(15 - conv_integer(iCOUNT)) when ( iSTATE > S0 ) else '0' ;
SEND <= '1' when ( iSTATE > S5 ) else '0' ;
end Behavioral;
CPLDの中に、Music sequencerを入れる場合、マクロセルが問題になります。
VHDLコードで作成した、このMusic sequencerは、179マクロセル利用して
手持ちのCoolRunnerIIでは、インプリメントできませんでした。
手持ちのFPGAの中にある、Spartan3の5万ゲートクラスでインプリメントできました。
ワンチップマイコンが¥500程度で入手できるので、Music sequencerを
これらのワンチップマイコンで実現する方が、楽ですし、安価です。
Music sequencerで、音階を構成するため、逐次電卓で周波数を計算して
打ち込みするのは面倒です。
楽器のキーボードをみるとわかりますが、五譜紙に記される音階は
周波数は固定です。
周波数が固定されていることを利用して、周波数とその周波数を出す時間を
記述できるように考案された言語があります。
MML(Music Macro Language)と呼ばれ、MSXなどでは標準で使えた言語です。
MMLは、OMRONのWorkStationであるLunaでも標準で使えました。
Lunaは、UNIXを利用したWorkStationで、コストパフォーマンスがよい
マシンでした。
自分もMMLで、高価なWorkStationで音を鳴らして遊んだものです。
MMLについて、説明します。
MML(Music Macro Language)
Wikiペディアなどで、MMLの説明があるので、簡単に説明します。
MMLは、コンピュータ上にある楽譜をイメージした簡易言語です。
元々は、音楽演奏機能を持つ8ビットパーソナルコンピュータ(以下PC)の
BASICに含まれていました。
PLAYコマンドがあり、そのコマンドのパラメータを表現していました。
BASICがPC標準の言語でなくなっても、楽譜記述用として、しぶとく生き残りました。
現在では、PCと音源や楽器を接続するためにMIDI規格を利用しますが
Standard MIDI File形式や各種ソフトのファイル形式などに変換できる
フィルタプログラムを利用して、MMLから必要な形式に変換し利用されて
います。
利用する英数字には、以下のような意味を持たせています。
(英字では、大文字、小文字の区別はありません。)
C D E F G A B
それぞれ、ドレミファソラシの音符です。
Aは、440Hzの整数倍、整数分の1に設定されます。
Aを基準として、1オクターブ(2倍の周波数に相当)は
12平均律で、1オクターブを12乗根で等比数列になるように
周波数を割当てます。
# + -
音符の後につけて半音上げ下げを表します。
#と+は同じ意味になります。
#と♭は、楽譜で利用されますが、♭をキーボードで入力
できないので、+と−で半音の上げ下げを指定します。
R
休符。
数字 .
音符や休符の後につけ、音の長さを表します。
2=2分音符。.は付点で長さを1.5倍する。4.=付点4分音符。
A4B4.C2だと、ラの4分音符、シの付点4分音符、ドの2分音符です。
&
二つの音符を連結します。
場合によっては、タイを表します。
システムにより解釈が異なります。
O
オクターブを指定します。
一度、音階の前にオクターブを指定すると、次にオクターブを
変更しない限り、そのオクターブを維持します。
L
A〜GやRの後に数字をつけない場合の音の長さを指定します。
初期値は、4になっているシステムが多いです。
V
音量(ボリューム)を指定します。
システムにより、対数指定で1〜10であったり、リニアに
0〜255のようにしたりで、違いがあります。
@
FM音源などでの音色指定に使います。
StandardMIDIでは、FM音源で出力する楽器の音が規定されています。
楽器の音を、1〜256に分類してあるので、@に続けて楽器の番号
を指定します。
T
テンポを指定。たとえば「T120」なら120BPMで演奏します。
コンピュータによっては、テンポのずれが発生します。
テンポずれは、非同期処理をしているシステムで発生します。
人間が打ち込んだMMLで記述した楽譜を、CPLD/FPGAで周波数や時間待ち
の数値を生成する機械を実現できます。しかし、ワンチップマイコンが
ワンコインで購入できる時代なので、データ変換処理はマイコンが担当
する仕事にした方が、開発期間は短くなります。
音に変化をつける方法
これまでに説明してきた音は、矩形波です。
スピーカで聞いてみると、汚い音です。
矩形波は、基本波の他に倍音を大量に含むので
汚い音にしかなりません。
澄んだ音を出す場合には、4つのフィルタを組合わせる方法があります。
フィルタは、扱う周波数により、以下の4つに分類できます。
- LPF(Low Pass Filter)
- HPF(High Pass Filter)
- BPF(Band Pass Filter)
- BEF(Band Elimination Filter)
最近は、これらのフィルタはDSP(Discrete Signal Processor)を使い
計算で実現します。
DSPを構成する方法は、別のページで紹介することにして、ここでは
アナログ回路でフィルタを作ってみます。
OPアンプを利用し、パッシブタイプのLPF、HPFを作ると以下となります。
どの周波数まで通すかを決めるには、R1とC1による時定数を計算します。
R1=1kohm、C1=0.1uFでは、R1xC1=0.1msとなり、約10kHzのLPF、HPFを
作れます。
C1を小さくすると、高い周波数のLPF、HPFを構成できます。
2段目のOPアンプでは、反転増幅しています。
1段目で逆相になった波形を、2段目で正相に戻しています。
また、RCのパッシブでは、電圧利得が6dB落ちるので、2段目で
6dB回復させます。
BPF(Band Pass Filter)は、LPFとHPFを接続すると実現できます。
ここまでは、矩形波から澄んだ音を得るための処理でした。
複数の矩形波を加算、乗算すると、面白い音を作れます。
加算処理は、OPアンプによる加算回路を構成します。
同一振幅の信号を加算すると飽和するので、一度アッティネータで
振幅を小さくします。その後、反転加算します。
乗算処理は、アナログスイッチを利用したデータセレクタを
利用します。
スイッチング処理は、乗算になります。
この回路は、振幅変調をデジタルで実現します。
スイッチングに利用する信号を、530kHz〜1600kHzの矩形波に
すると、AMワイヤレスマイクの基本回路になります。
デジタル出力を、アナログ回路で操作すると
面白い音を簡単に作れます。
CPLD/FPGAに封入したデジタル回路と外付けアナログ回路で
面白い音を作れますが、FM音源の利用で、デジタルだけで
いろいろな音を作り出せます。
FM音源に必要となる、正弦波生成をDDSで作成し
FM音源を作ってみます。
DDS(Direct Digital Synthesizer)の活用
DDSは、日本語ではディジタル直接合成発振器と呼ばれています。
加算器、ラッチ、アキュムレータを構成し、クロックに同期して
周波数に相当する値N(周波数設定値)を累積します。
累積値を、波形ROMのアドレスとして、デジタルデータを取得します。
デジタルデータをD/Aコンバータでアナログ値に変換します。
D/Aコンバータ出力は階段波なので、LPFで高調波成分を除去して
綺麗なアナログ値にします。
周波数は、加算器のビット数をnとすると、次のように計算できます。
発振周波数=周波数設定値×クロック周波数÷(2のn乗)
波形ROMの内容を、正弦波、三角波等にすれば、任意の波形を生成できます。
D/Aコンバータは、R-2Rラダー型にすると、CPLD/FPGAに抵抗を
接続して簡単に実現できます。
D/Aコンバータは、外付け抵抗で実現できるので、ROMを
どうにかしなければなりません。
FM音源を実現する場合は、正弦波を1波長分用意すれば
使い回せるので、CPLD/FPGAの中にROMを定義し、その中
に、1波長の正弦波データを入れます。
ROMは、次のように定義します。
subtype DTYPE is std_logic_vector(3 downto 0) ;
type DEC is array(0 to 15) of DTYPE ;
constant DECX : DEC := (
"0000",
"0001",
"0001",
"0010",
"0001",
"0010",
"0010",
"0011",
"0001",
"0010",
"0010",
"0011",
"0010",
"0011",
"0011",
"0100"
) ;
最初に、subtypeでデータサイズを定義します。
定義したデータサイズで、配列を作り、その中に
固定データを入れるとROMになります。
constantは、固定データを意味します。従って
この修飾子をつけないと、RAMを定義することに
なります。(RAMでは、データ定義をしません。)
正弦波の1波長データを作成します。電卓を叩くのは
面倒なので、プログラムに生成させます。
振幅が最大255、256個で正弦波の1波長になるような
データを生成するTcl/Tkコードは、以下です。
#!/usr/local/bin/wish
# procedure
proc h2b {x} {
set tmp $x
set result ""
#
set result "$result[expr $tmp / 8]"
set tmp [expr $tmp % 8]
#
set result "$result[expr $tmp / 4]"
set tmp [expr $tmp % 4]
#
set result "$result[expr $tmp / 2]"
set result "$result[expr $tmp % 2]"
return $result
}
proc hh2bb {x} {
set ud [expr $x / 16]
set ld [expr $x % 16]
set result "\"[h2b $ud][h2b $ld]\""
return $result
}
# calculate pi
set xpi [expr 4 * atan(1)]
set xpi2 [expr 2 * $xpi]
set fd_out [open "sincode.txt" "w"]
set tmp ""
# generate code
for {set i 0} {$i < 256} {incr i} {
# calculate
set val0 [expr 127*(sin(($xpi2*$i)/256)+1)]
# convert float to integer
set val [expr int($val0)]
# concatenate
set tmp "$tmp[hh2bb $val],"
# store code to file
if { [expr $i % 8] == 7 } {
puts $fd_out $tmp
set tmp ""
}
}
close $fd_out
Tcl/Tkで求めた値は、次のようになります。
"01111111","10000010","10000101","10001000","10001011","10001110","10010001","10010100",
"10010111","10011010","10011101","10100000","10100011","10100110","10101001","10101100",
"10101111","10110010","10110101","10111000","10111010","10111101","11000000","11000010",
"11000101","11001000","11001010","11001101","11001111","11010001","11010100","11010110",
"11011000","11011010","11011101","11011111","11100001","11100011","11100101","11100110",
"11101000","11101010","11101011","11101101","11101111","11110000","11110001","11110011",
"11110100","11110101","11110110","11110111","11111000","11111001","11111010","11111010",
"11111011","11111100","11111100","11111101","11111101","11111101","11111101","11111101",
"11111110","11111101","11111101","11111101","11111101","11111101","11111100","11111100",
"11111011","11111010","11111010","11111001","11111000","11110111","11110110","11110101",
"11110100","11110011","11110001","11110000","11101111","11101101","11101011","11101010",
"11101000","11100110","11100101","11100011","11100001","11011111","11011101","11011010",
"11011000","11010110","11010100","11010001","11001111","11001101","11001010","11001000",
"11000101","11000010","11000000","10111101","10111010","10111000","10110101","10110010",
"10101111","10101100","10101001","10100110","10100011","10100000","10011101","10011010",
"10010111","10010100","10010001","10001110","10001011","10001000","10000101","10000010",
"01111111","01111011","01111000","01110101","01110010","01101111","01101100","01101001",
"01100110","01100011","01100000","01011101","01011010","01010111","01010100","01010001",
"01001110","01001011","01001000","01000101","01000011","01000000","00111101","00111011",
"00111000","00110101","00110011","00110000","00101110","00101100","00101001","00100111",
"00100101","00100011","00100000","00011110","00011100","00011010","00011000","00010111",
"00010101","00010011","00010010","00010000","00001110","00001101","00001100","00001010",
"00001001","00001000","00000111","00000110","00000101","00000100","00000011","00000011",
"00000010","00000001","00000001","00000000","00000000","00000000","00000000","00000000",
"00000000","00000000","00000000","00000000","00000000","00000000","00000001","00000001",
"00000010","00000011","00000011","00000100","00000101","00000110","00000111","00001000",
"00001001","00001010","00001100","00001101","00001110","00010000","00010010","00010011",
"00010101","00010111","00011000","00011010","00011100","00011110","00100000","00100011",
"00100101","00100111","00101001","00101100","00101110","00110000","00110011","00110101",
"00111000","00111011","00111101","01000000","01000011","01000101","01001000","01001011",
"01001110","01010001","01010100","01010111","01011010","01011101","01100000","01100011",
"01100110","01101001","01101100","01101111","01110010","01110101","01111000","01111011",
求められた値を、VHDLコードでROMデータとします。
subtype DTYPE is std_logic_vector(7 downto 0) ;
type SINTABLE is array(0 to 255) of DTYPE ;
constant SINDATA : SINTABLE := (
"01111111","10000010","10000101","10001000","10001011","10001110","10010001","10010100",
"10010111","10011010","10011101","10100000","10100011","10100110","10101001","10101100",
"10101111","10110010","10110101","10111000","10111010","10111101","11000000","11000010",
"11000101","11001000","11001010","11001101","11001111","11010001","11010100","11010110",
"11011000","11011010","11011101","11011111","11100001","11100011","11100101","11100110",
"11101000","11101010","11101011","11101101","11101111","11110000","11110001","11110011",
"11110100","11110101","11110110","11110111","11111000","11111001","11111010","11111010",
"11111011","11111100","11111100","11111101","11111101","11111101","11111101","11111101",
"11111110","11111101","11111101","11111101","11111101","11111101","11111100","11111100",
"11111011","11111010","11111010","11111001","11111000","11110111","11110110","11110101",
"11110100","11110011","11110001","11110000","11101111","11101101","11101011","11101010",
"11101000","11100110","11100101","11100011","11100001","11011111","11011101","11011010",
"11011000","11010110","11010100","11010001","11001111","11001101","11001010","11001000",
"11000101","11000010","11000000","10111101","10111010","10111000","10110101","10110010",
"10101111","10101100","10101001","10100110","10100011","10100000","10011101","10011010",
"10010111","10010100","10010001","10001110","10001011","10001000","10000101","10000010",
"01111111","01111011","01111000","01110101","01110010","01101111","01101100","01101001",
"01100110","01100011","01100000","01011101","01011010","01010111","01010100","01010001",
"01001110","01001011","01001000","01000101","01000011","01000000","00111101","00111011",
"00111000","00110101","00110011","00110000","00101110","00101100","00101001","00100111",
"00100101","00100011","00100000","00011110","00011100","00011010","00011000","00010111",
"00010101","00010011","00010010","00010000","00001110","00001101","00001100","00001010",
"00001001","00001000","00000111","00000110","00000101","00000100","00000011","00000011",
"00000010","00000001","00000001","00000000","00000000","00000000","00000000","00000000",
"00000000","00000000","00000000","00000000","00000000","00000000","00000001","00000001",
"00000010","00000011","00000011","00000100","00000101","00000110","00000111","00001000",
"00001001","00001010","00001100","00001101","00001110","00010000","00010010","00010011",
"00010101","00010111","00011000","00011010","00011100","00011110","00100000","00100011",
"00100101","00100111","00101001","00101100","00101110","00110000","00110011","00110101",
"00111000","00111011","00111101","01000000","01000011","01000101","01001000","01001011",
"01001110","01010001","01010100","01010111","01011010","01011101","01100000","01100011",
"01100110","01101001","01101100","01101111","01110010","01110101","01111000","01111011"
) ;
人間の可聴帯域は、20Hz〜20kHzなので、最高周波数の20kHzを
出すためには、20kHz x 256 = 5120kHz = 5.12MHz が必要です。
デジタル回路で利用するクリスタルとして、10.24MHzを入手できる
ので、CPLD/FPGAで使う周波数として、この値を利用します。
20kHzの正弦波が得られると、その整数倍の正弦波を得ることは
DDSを使うと簡単にできます。
ROMのアドレスを1ずつ増やすと、20kHzの正弦波になります。
ROMのアドレスを2ずつ増やすと(偶数アドレスを生成すると)
40kHzの20kHzの正弦波になります。
増分を1からN(N=2,3,4,..)に変えると、MHzの正弦波から
MxNHzの正弦波を生成できます。
M、Nの組み合わせは、無限に存在しますが、M、N単独では
有限個の選択肢になります。
Nの場合は、ROMのアドレスを0〜255にしてあるので、1〜63
の自然数になります。正弦波のグラフを描くと、この範囲になる
ことがわかります。
正弦波のグラフから、1/4波長のデータがあれば、振幅は再現できると
読み取れます。
0〜1/4波長までは、振幅は増加していきます。1/4波長を超えると
振幅は減少していきます。これで、1/2波長まで振幅を再生できます。
正の1/2波長の振幅を、負の方向に折り返すと、負の1/2波長の振幅を
再生できます。
少し頭を捻ると、1/4波長までをカバーするNの値でないと、振幅の再生は
できないと理解できるでしょう。
0〜255には、256個のアドレスがあるので、アドレスにNを加算していくと
1/4波長に相当する63が最大になります。
1/4波長に相当する値を超えると、その値より小さいNの値を加算している
のと同じになります。
Mの値は、利用しているクロック周波数に依存します。
人間の可聴帯域は、20Hzから20kHzなので、5.12MHzを利用
している場合、5.12MHz/256=20kHzです。
10kHzを得たい場合には、5.12MHz/(256x2)=10kHzになります。
ほしい周波数MHzを決めて、次の式でmを計算します。
20000Hz/M = m
この式は、次の計算で算出しました。
5120000Hz/(256xm)= M Hz
20000Hz/m = M Hz
Mをmに換算してみると、20Hzから20kHzなので
mは、1〜1000になります。
人間の可聴帯域を生成する場合には、M(m)の値を変化
させ、そのN倍の周波数を得たい時は、Nの値を操作します。
Mで発振する正弦波の周波数を決定し、Nでその整数倍の
正弦波を生成すると考えればよいでしょう。
DDSを利用すると、使っているクロックの整数倍の周波数を
生成できます。
DDSを作成するため、ブロック図を描きました。
ブロック図を見ながら、必要な信号と内部ブロックを検討します。
ROM内データを出力するため、ROMにアドレスを与えます。
アドレス生成のために、Adderを使います。
必要なときに音を出せるよう、ENABLE信号を設けます。
Adderは、DREGに格納された値を、クロックに同期させて加算します。
加算結果を、ROMのアドレスとします。
DREGの値は、外部から設定します。
(DREGに設定する値が、Nに相当)
Adderに与えるクロックを作成しなければなりません。
クロックは、FDIVに設定した値とCOUNTERの値が一致した
ときに、出力されるよう構成します。
(FDIVに設定する値が、Mに相当)
ここまで検討したので、Entityを定義します。
entity dds0 is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ; -- 10.24MHz
-- fdiv
LCLK : in std_logic ;
FLOAD : in std_logic ;
DLOAD : in std_logic ;
SIN : in std_logic ;
-- dds out
ENABLE : in std_logic ;
DDSOUT : out std_logic_vector(7 downto 0) -- ;
) ;
end dds0;
FDIV、DREGに値を設定するには、シリアル転送にします。
パラレルで転送すると、CPLD/FPGAの入出力ピンを大量に
消費するので、シリアル転送で、利用ピンを減らします。
内部レジスタへの転送処理は、次のように定義しました。
process ( nRESET , LCLK )
begin
if ( nRESET = '0' ) then
iFDIV <= (others => '0') ;
iDREG <= "000001" ;
elsif rising_edge( LCLK ) then
-- divider
if ( FLOAD = '1' ) then
iFDIV <= iFDIV(14 downto 0) & SIN ;
end if ;
-- difference
if ( DLOAD = '1' ) then
iDREG <= iDREG(4 downto 0) & SIN ;
end if ;
end if ;
end process ;
FDIVに設定した値で、MHzのMに相当する周波数のクロックを生成します。
このクロックを生成するブロックを定義します。
process ( nRESET , iSCLK )
begin
if ( nRESET = '0' ) then
iCOUNT <= (others => '0') ;
elsif rising_edge( iSCLK ) then
iCOUNT <= iCOUNT + '1' ;
if ( iCOUNT = iFDIV ) then
iCOUNT <= (others => '0') ;
end if ;
end if ;
end process ;
iSSCLK <= '1' when ( conv_integer(iCOUNT) = 0 ) else '0' ;
可聴帯域の周波数を生成するため、システムクロックから
iSCLKを生成します。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSCNT <= "00" ;
elsif rising_edge( CLOCK ) then
iSCNT <= iSCNT + '1' ;
end if ;
end process ;
iSCLK <= '1' when ( iSCNT(1) = '0' ) else '0' ;
最後に、ROMのアドレスを生成するブロックを定義します。
process ( nRESET , iSSCLK )
begin
if ( nRESET = '0' ) then
iROMADR <= (others => '0') ;
elsif rising_edge( iSSCLK ) then
iROMADR <= iROMADR + ('0' & iDREG) ;
end if ;
end process ;
iDDSOUT <= SINDATA( conv_integer(iROMADR) ) ;
DDSを実現するVHDLコード全体は、以下となります。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity dds0 is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ; -- 10.24MHz
-- fdiv
LCLK : in std_logic ;
FLOAD : in std_logic ;
DLOAD : in std_logic ;
SIN : in std_logic ;
-- dds out
ENABLE : in std_logic ;
DDSOUT : out std_logic_vector(7 downto 0) -- ;
) ;
end dds0;
architecture Behavioral of dds0 is
-- sound clock
signal iSCNT : std_logic_vector(1 downto 0);
signal iSCLK : std_logic ;
-- fdiv
signal iFDIV : std_logic_vector(15 downto 0);
signal iDREG : std_logic_vector( 5 downto 0);
-- M divider
signal iCOUNT : std_logic_vector(15 downto 0);
signal iSSCLK : std_logic ;
-- ROM
subtype DTYPE is std_logic_vector(7 downto 0) ;
type SINTABLE is array(0 to 255) of DTYPE ;
constant SINDATA : SINTABLE := (
"01111111","10000010","10000101","10001000","10001011","10001110","10010001","10010100",
"10010111","10011010","10011101","10100000","10100011","10100110","10101001","10101100",
"10101111","10110010","10110101","10111000","10111010","10111101","11000000","11000010",
"11000101","11001000","11001010","11001101","11001111","11010001","11010100","11010110",
"11011000","11011010","11011101","11011111","11100001","11100011","11100101","11100110",
"11101000","11101010","11101011","11101101","11101111","11110000","11110001","11110011",
"11110100","11110101","11110110","11110111","11111000","11111001","11111010","11111010",
"11111011","11111100","11111100","11111101","11111101","11111101","11111101","11111101",
"11111110","11111101","11111101","11111101","11111101","11111101","11111100","11111100",
"11111011","11111010","11111010","11111001","11111000","11110111","11110110","11110101",
"11110100","11110011","11110001","11110000","11101111","11101101","11101011","11101010",
"11101000","11100110","11100101","11100011","11100001","11011111","11011101","11011010",
"11011000","11010110","11010100","11010001","11001111","11001101","11001010","11001000",
"11000101","11000010","11000000","10111101","10111010","10111000","10110101","10110010",
"10101111","10101100","10101001","10100110","10100011","10100000","10011101","10011010",
"10010111","10010100","10010001","10001110","10001011","10001000","10000101","10000010",
"01111111","01111011","01111000","01110101","01110010","01101111","01101100","01101001",
"01100110","01100011","01100000","01011101","01011010","01010111","01010100","01010001",
"01001110","01001011","01001000","01000101","01000011","01000000","00111101","00111011",
"00111000","00110101","00110011","00110000","00101110","00101100","00101001","00100111",
"00100101","00100011","00100000","00011110","00011100","00011010","00011000","00010111",
"00010101","00010011","00010010","00010000","00001110","00001101","00001100","00001010",
"00001001","00001000","00000111","00000110","00000101","00000100","00000011","00000011",
"00000010","00000001","00000001","00000000","00000000","00000000","00000000","00000000",
"00000000","00000000","00000000","00000000","00000000","00000000","00000001","00000001",
"00000010","00000011","00000011","00000100","00000101","00000110","00000111","00001000",
"00001001","00001010","00001100","00001101","00001110","00010000","00010010","00010011",
"00010101","00010111","00011000","00011010","00011100","00011110","00100000","00100011",
"00100101","00100111","00101001","00101100","00101110","00110000","00110011","00110101",
"00111000","00111011","00111101","01000000","01000011","01000101","01001000","01001011",
"01001110","01010001","01010100","01010111","01011010","01011101","01100000","01100011",
"01100110","01101001","01101100","01101111","01110010","01110101","01111000","01111011"
) ;
-- dds out
signal iROMADR : std_logic_vector(7 downto 0);
signal iDDSOUT : std_logic_vector(7 downto 0);
begin
-- sound clock
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSCNT <= "00" ;
elsif rising_edge( CLOCK ) then
iSCNT <= iSCNT + '1' ;
end if ;
end process ;
iSCLK <= '1' when ( iSCNT(1) = '0' ) else '0' ;
-- fdiv and difference
process ( nRESET , LCLK )
begin
if ( nRESET = '0' ) then
iFDIV <= (others => '0') ;
iDREG <= "000001" ;
elsif rising_edge( LCLK ) then
-- divider
if ( FLOAD = '1' ) then
iFDIV <= iFDIV(14 downto 0) & SIN ;
end if ;
-- difference
if ( DLOAD = '1' ) then
iDREG <= iDREG(4 downto 0) & SIN ;
end if ;
end if ;
end process ;
-- M divider
process ( nRESET , iSCLK )
begin
if ( nRESET = '0' ) then
iCOUNT <= (others => '0') ;
elsif rising_edge( iSCLK ) then
iCOUNT <= iCOUNT + '1' ;
if ( iCOUNT = iFDIV ) then
iCOUNT <= (others => '0') ;
end if ;
end if ;
end process ;
iSSCLK <= '1' when ( conv_integer(iCOUNT) = 0 ) else '0' ;
-- generate ROM address
process ( nRESET , iSSCLK )
begin
if ( nRESET = '0' ) then
iROMADR <= (others => '0') ;
elsif rising_edge( iSSCLK ) then
iROMADR <= iROMADR + ('0' & iDREG) ;
end if ;
end process ;
iDDSOUT <= SINDATA( conv_integer(iROMADR) ) ;
-- dds out
DDSOUT <= iDDSOUT when ( ENABLE = '1' ) else (others => '0') ;
end Behavioral;
テストは、CoolRunnerIIのXC2C256が載ったボードを
使いました。M、Nの設定には、CPLDボードにマイコン
を接続し、PCからシリアルで設定しました。
DDSを利用すると、PSGだけでなく、FM音源や無線機用
発振器を作成できます。
FM音源を作成してみます。
FM音源の原理
Fouier変換で有名な、Fourierはすべての波は、正弦波の
組合せでできていると証明しました。
正弦波を組合わせて、どんな波でも生成できるので
複数の正弦波を作り、合成すると音ができます。
合成をどう実現するのかが、問題です。
大学では、Fourier級数を利用した、基本波の整数倍の正弦波を
複数用意し、各正弦波の振幅を変え、加算する方法を説明され
演習で確認します。
スプレッドシート(表計算ソフト)を利用すると、一瞬で結果が
出てくるので、簡単な方法です。
スプレッドシートを利用して、基本波、3倍高調波、5倍高調波
を生成し、加算してみると、以下となります。
(振幅は、1、0.5、0.25として加算してあります。)
この方法は、簡単なのですが、問題があります。
複数の正弦波を用意し、振幅を変えるために、掛算が
必要になり計算が大変です。
掛算をしつつ加算する処理になるので、DSPを使えば、それほど
大変ではないのですが、CPLD/FPGAにDSPを入れるのは面倒です。
DSPをCPLD/FPGAの中に封入すれば、実現できますが、利用する
マクロセル数やゲート数が膨大になります。
マクロセル数やゲート数が膨大になると、CPLD/FPGAのマクロセル数や
ゲート数が大きなチップを採用することになります。
何とか、マクロセル数やゲート数を減らし、豊かな音を作り
出したいと考えた、技術者が選択した方式が、FM音源です。
豊かな音作りに挑戦した技術者は、楽器メーカのYAMAHAにいました。
FM音源の特許は、YAMAHAが所有しています。
YAMAHAは、FM音源のICを設計し、小型安価なPortaSoundという
ミュージックシンセサイザを製造しました。
ミュージックシンセサイザDX-7にも、FM音源を載せたので
DX-7は大物ミュージシャンが使う名器になりました。
ただし、DX-7に載せたFM音源ICは、市販されませんでしたが。
最近では、携帯電話で着メロにFM音源が利用されています。
FM音源は、ソフトでもハードでも実現できるので、小さな
マイコンでも1音程度は生成できます。
最近の携帯電話は、OSとしてAndroidを載せていることが多い
ですが、OSを載せられる程度のマイコンを利用しているので
FM音源をソフトウエアで実現するのは簡単です。
歴史や応用例は、このくらいにして、FM音源の原理を説明します。
FM音源の原理はとても単純で、次の式をソフトか
ハードで実現するだけです。
sin(f+Rsin(Nf))
正弦波を生成する式の中に、また正弦波の式が出てきます。
この式を少し、整理してみましょう。
sin(f+p) p=Rsin(Nf)
左は、基本波の式で、周波数はfです。
pが位相になります。
右は、基本波の整数倍で発振して、振幅に相当する値を
掛けています。
FM音源は、基本波の位相を、基本波の整数倍の正弦波で
変化させているということです。
どんな波形になるのかを、スプレッドシートで描いて
みると、次のようになります。
(sin(f+0.5sin(3f)) を生成しています。)
上には、基本波と3倍高調波を描いてあります。
合成波形が、白で描かれているのですが、見えにくい
ので、合成波形だけを取り出して、下に描きました。
たった2つの周波数を組合わせるだけで、複雑な波形を
生成できるので、基本波と高調波を加算するよりも簡便
です。
同じ式を利用して、Rに相当する部分を変化させると
次のような波形になります。
この波形をどこかで見た記憶があるかも知れません。
FM放送の周波数変調波形が、このようになります。
※FM音源は、周波数変調の親戚と言えるでしょう。
FM音源を実現するために、式を見直してみます。
sin(f+p)
p=Rsin(Nf)
基本周波数の整数倍の正弦波が必要と判断できます。
要するにDDSです。
FM音源の実現
FM音源で音色を作るためには、3つの概念を利用します。
その3つの概念は、以下です。
FM音源を実現するための式で見ると、
キャリア、モジュレータは単純です。
sin(f+p) p=Rsin(Nf)
があれば、pがモジュレータです。
キャリアは、sin(f+p)になります。
アルゴリズムは、キャリアとモジュレータの
組合せを指します。
キャリア、モジュレータを式で表すのは面倒なので
キャリア、モジュレータを箱として表現します。
最初、FM音源のシンセサイザを触ったとき、アルゴリズムの
概念を理解していなかったので、とんでもない音を出して、
合奏者に驚かれたことがありました。
アルゴリズムは、変調の掛け具合を決める
重要なパラメータです。
楽器の音をシミュレートする場合、どんなアルゴリズムを
使うのかは、だいたい決まるのですが、楽器の音には千差
万別の個性があるので、無限個の組合せが存在します。
ピアノであればYAMAHAとSTAINWAYの違いを出すことも
原理上可能ですが、グランドとアップライトのタイプ
のちがいや音響環境にも影響されます。
アルゴリズムの表現は、次のようにします。
モジュレータ、キャリアをオペレータとまとめて呼びます。
各オペレータを矢印で結いで描き、どのオペレータの出力が
どのオペレータに 入力されるのかを記します。
FM音源の基本となる式を、アルゴリズムで表現すると
次のようになります。
sin(f+p)とp=Rsin(Nf)を接続しています。
アルゴリズムは、出力信号が右になるように描きますが
キャリアとモジュレータの関係がわかるように、出力は
左に描いて、わかりやすくしておきます。
オペレータのうち、モジュールには、掛算が必要です。
デジタル回路の中に掛算をするときは、2のべき乗で
操作できるようにします。
2から10倍の生成方法を考えてみます。
- 2倍 左に1ビットシフト
- 3倍 2倍値と元の値を加算
- 4倍 左に2ビットシフト
- 5倍 4倍値と元の値を加算
- 6倍 4倍値と2倍値の加算
- 7倍 4倍値、2倍値、元の値を加算
- 8倍 左に3ビットシフト
- 9倍 8倍値、元の値を加算
- 10倍 8倍値、2倍値を加算
10倍までならば、補助レジスタを2つ用意すると対応できます。
補助レジスタが、reg0、reg1とreg2の3つあるとし
倍数処理を検討します。
2倍
左に1ビットシフトすればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に1ビットシフト
reg0 + reg1 + reg2 を計算
3倍
2倍値と元の値を加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に1ビットシフト
reg0 + reg1 + reg2 を計算
4倍
左に2ビットシフトすればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = 0 , reg2 = 0 と設定
reg0の内容を左に2ビットシフト
reg0 + reg1 + reg2 を計算
5倍
4倍値と元の値を加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に2ビットシフト
reg0 + reg1 + reg2 を計算
6倍
4倍値と2倍値の加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に2ビットシフト、reg1の内容を左に1ビットシフト
reg0 + reg1 + reg2 を計算
7倍
4倍値、2倍値、元の値を加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = x と設定
reg0の内容を左に2ビットシフト、reg1の内容を左に1ビットシフト
reg0 + reg1 + reg2 を計算
8倍
左に3ビットシフトすればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に3ビットシフト
reg0 + reg1 + reg2 を計算
9倍
8倍値、元の値を加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に3ビットシフト
reg0 + reg1 + reg2 を計算
10倍
8倍値、2倍値を加算を加算すればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を左に3ビットシフト、reg1の内容を左に1ビットシフト
reg0 + reg1 + reg2 を計算
各処理のシーケンスを眺めると、次の処理で倍数処理を
実現していることがわかります。
- reg0、reg1の値を初期化
- 左シフト処理
- 加算
2〜10までの整数倍は、上のシーケンスで実現できます。
整数で割る処理を考えてみます。
0.5倍、0.25倍、0.125倍は、シフト処理で対応できます。
この組合せで他にできる掛算は、つぎのようになります。
- 0.875倍 0.5倍値、0.25倍値、0.125倍値を加算
- 0.750倍 0.5倍値、0.25倍値を加算
- 0.625倍 0.5倍値、0.125倍値を加算
- 0.375倍 0.25倍値、0.125倍値を加算
整数倍同様に、乗算処理を考えてみます。
0.875倍
0.5倍値、0.25倍値、0.125倍値を加算すればよいので
元の値をxとして次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = x と設定
reg0の内容を右に1ビットシフト、reg1の内容を右に2ビットシフト、
reg2の内容を右に3ビットシフト
reg0 + reg1 + reg2 を計算
0.750倍
0.5倍値、0.25倍値を加算すればよいので
元の値をxとして次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を右に1ビットシフト、reg1の内容を右に2ビットシフト
reg0 + reg1 + reg2 を計算
0.625倍
0.5倍値、0.125倍値を加算すればよいので
元の値をxとして次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を右に1ビットシフト、reg1の内容を右に3ビットシフト
reg0 + reg1 + reg2 を計算
0.5倍
0.5倍値を求めればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = 0 , reg2 = 0 と設定
reg0の内容を右に1ビットシフト
reg0 + reg1 + reg2 を計算
0.375倍
0.25倍値、0.125倍値を加算すればよいので
元の値をxとして次のシーケンスを使います。
reg0 = x , reg1 = x , reg2 = 0 と設定
reg0の内容を右に2ビットシフト、reg1の内容を右に3ビットシフト
reg0 + reg1 + reg2 を計算
0.25倍
0.25倍値を求めればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = 0 , reg2 = 0 と設定
reg0の内容を右に2ビットシフト
reg0 + reg1 + reg2 を計算
0.125倍
0.125倍値を求めればよいので、元の値をxとして
次のシーケンスを使います。
reg0 = x , reg1 = 0 , reg2 = 0 と設定
reg0の内容を右に3ビットシフト
reg0 + reg1 + reg2 を計算
各処理のシーケンスを眺めると、次の処理で倍数処理を
実現していることがわかります。
- reg0、reg1、reg2の値を初期化
- 右シフト処理
- 加算
乗算処理は、整数倍に9通り、小数倍に7通りの合計16通り
考えられます。動作シーケンスは同じで、初期値とシフト
するビット数が異なるだけです。
8ビットの整数に乗算する処理をVHDLで定義します。
仕様を検討します。
8ビットの最大値は、255なので10倍すると2550となり
結果を保存するには、12ビット必要です。
乗算種別は、16通りあるので、乗算種別の設定に
4ビット必要です。
4ビットの乗算種別指定は、次のようにします。
0000 2倍
0001 3倍
0010 4倍
0011 5倍
0100 6倍
0101 7倍
0110 8倍
0111 9倍
1000 10倍
1001 0.875倍
1010 0.750倍
1011 0.625倍
1100 0.500倍
1101 0.375倍
1110 0.250倍
1111 0.125倍
乗算開始のトリガーを用意し、計算中であることを示す
フラグを用意します。
ここまでの仕様検討で、Entityを作成できます。
entity mulx0 is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- select
SEL : in std_logic_vector(3 downto 0) ;
-- trigger
TRG : in std_logic ;
-- data
ADAT : in std_logic_vector(7 downto 0) ;
-- output
BUSY : out std_logic ;
RESEULT : out std_logic_vector(11 downto 0) --;
) ;
end mulx0;
動作は、シーケンスになっています。
シーケンスをまとめて、コードを作成しやすくします。
- トリガー待ち
- reg0初期化
- 内部レジスタ初期化
- シフト処理
- 加算
ステートマシンを構成します。
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSTATE <= S0 ;
iREG0 <= (others => '0') ;
iREG1 <= (others => '0') ;
iREG2 <= (others => '0') ;
iRESULT <= (others => '0') ;
elsif rising_edge( CLOCK ) then
case iSTATE is
-- wait trigger
when S0 =>
if ( TRG = '1' ) then
iSTATE <= S1 ;
end if ;
-- initialize registers
when S1 =>
iSTATE <= S2 ;
iSEL <= SEL ;
iREG0 <= "0000" & ADAT ;
iREG1 <= (others => '0') ;
iREG2 <= (others => '0') ;
iRESULT <= (others => '0') ;
-- configure registers
when S2 =>
iSTATE <= S3 ;
case iSEL is
-- x 3 , x 5 , x 6
when "0001" | "0011" | "0100" =>
iREG1 <= iREG0 ;
-- x 7 , x 0.875
when "0101" | "1001" =>
iREG1 <= iREG0 ;
iREG2 <= iREG0(7 downto 0) ;
-- x 9 , x 10
when "0111" | "1000" =>
iREG1 <= iREG0 ;
-- x 0.750 , x 0.625 , x 0.375
when "1010" | "1011" | "1101" =>
iREG1 <= iREG0 ;
-- default
when others => NULL ;
end case ;
-- shift
when S3 =>
iSTATE <= S4 ;
-- x 2 , x 3
if ( iSEL(3 downto 1) = "000" ) then
iREG0 <= iREG0(10 downto 0) & '0' ;
end if ;
-- x 4 , x 5
if ( iSEL(3 downto 1) = "001" ) then
iREG0 <= iREG0( 9 downto 0) & "00" ;
end if ;
-- x 6 , x 7
if ( iSEL(3 downto 1) = "010" ) then
iREG0 <= iREG0( 9 downto 0) & "00" ;
iREG1 <= iREG1(10 downto 0) & '0' ;
end if ;
-- x 8 , x 9
if ( iSEL(3 downto 1) = "011" ) then
iREG0 <= iREG0( 8 downto 0) & "000" ;
end if ;
case iSEL is
-- x 10
when "1000" =>
iREG0 <= iREG0( 8 downto 0) & "000" ;
iREG1 <= iREG1(10 downto 0) & '0' ;
-- x 0.875
when "1001" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "00" & iREG1(11 downto 2) ;
iREG2 <= "000" & iREG2(7 downto 3) ;
-- x 0.750
when "1010" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "00" & iREG1(11 downto 2) ;
-- x 0.625
when "1011" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "000" & iREG1(11 downto 3) ;
-- x 0.500
when "1100" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
-- x 0.375
when "1101" =>
iREG0 <= "00" & iREG0(11 downto 2) ;
iREG1 <= "000" & iREG1(11 downto 3) ;
-- x 0.250
when "1110" =>
iREG0 <= "00" & iREG0(11 downto 2) ;
-- x 0.125
when "1111" =>
iREG0 <= "000" & iREG0(11 downto 3) ;
-- default
when others => NULL ;
end case ;
-- add
when S4 =>
iSTATE <= S5 ;
iRESULT <= iREG0 + iREG1 + ("0000" & iREG2) ;
-- return first state
when S5 =>
iSTATE <= S0 ;
-- default
when others =>
iSTATE <= S0 ;
end case ;
end if ;
end process ;
全ソースコードは、以下です。
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity mulx0 is
Port (
-- system
nRESET : in std_logic ;
CLOCK : in std_logic ;
-- select
SEL : in std_logic_vector(3 downto 0) ;
-- trigger
TRG : in std_logic ;
-- data
ADAT : in std_logic_vector(7 downto 0) ;
-- output
BUSY : out std_logic ;
RESULT : out std_logic_vector(11 downto 0) --;
) ;
end mulx0;
architecture Behavioral of mulx0 is
-- result
signal iRESULT : std_logic_vector(11 downto 0) ;
-- select
signal iSEL : std_logic_vector(3 downto 0) ;
-- internal register
signal iREG0 : std_logic_vector(11 downto 0) ;
signal iREG1 : std_logic_vector(11 downto 0) ;
signal iREG2 : std_logic_vector( 7 downto 0) ;
-- state
type stype is (S0,S1,S2,S3,S4,S5);
signal iSTATE : stype ;
begin
RESULT <= iRESULT ;
BUSY <= '1' when ( iSTATE > S0 ) else '0' ;
process ( nRESET , CLOCK )
begin
if ( nRESET = '0' ) then
iSTATE <= S0 ;
iREG0 <= (others => '0') ;
iREG1 <= (others => '0') ;
iREG2 <= (others => '0') ;
iRESULT <= (others => '0') ;
elsif rising_edge( CLOCK ) then
case iSTATE is
-- wait trigger
when S0 =>
if ( TRG = '1' ) then
iSTATE <= S1 ;
end if ;
-- initialize registers
when S1 =>
iSTATE <= S2 ;
iSEL <= SEL ;
iREG0 <= "0000" & ADAT ;
iREG1 <= (others => '0') ;
iREG2 <= (others => '0') ;
iRESULT <= (others => '0') ;
-- configure registers
when S2 =>
iSTATE <= S3 ;
case iSEL is
-- x 3 , x 5 , x 6
when "0001" | "0011" | "0100" =>
iREG1 <= iREG0 ;
-- x 7 , x 0.875
when "0101" | "1001" =>
iREG1 <= iREG0 ;
iREG2 <= iREG0(7 downto 0) ;
-- x 9 , x 10
when "0111" | "1000" =>
iREG1 <= iREG0 ;
-- x 0.750 , x 0.625 , x 0.375
when "1010" | "1011" | "1101" =>
iREG1 <= iREG0 ;
-- default
when others => NULL ;
end case ;
-- shift
when S3 =>
iSTATE <= S4 ;
-- x 2 , x 3
if ( iSEL(3 downto 1) = "000" ) then
iREG0 <= iREG0(10 downto 0) & '0' ;
end if ;
-- x 4 , x 5
if ( iSEL(3 downto 1) = "001" ) then
iREG0 <= iREG0( 9 downto 0) & "00" ;
end if ;
-- x 6 , x 7
if ( iSEL(3 downto 1) = "010" ) then
iREG0 <= iREG0( 9 downto 0) & "00" ;
iREG1 <= iREG1(10 downto 0) & '0' ;
end if ;
-- x 8 , x 9
if ( iSEL(3 downto 1) = "011" ) then
iREG0 <= iREG0( 8 downto 0) & "000" ;
end if ;
case iSEL is
-- x 10
when "1000" =>
iREG0 <= iREG0( 8 downto 0) & "000" ;
iREG1 <= iREG1(10 downto 0) & '0' ;
-- x 0.875
when "1001" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "00" & iREG1(11 downto 2) ;
iREG2 <= "000" & iREG2(7 downto 3) ;
-- x 0.750
when "1010" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "00" & iREG1(11 downto 2) ;
-- x 0.625
when "1011" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
iREG1 <= "000" & iREG1(11 downto 3) ;
-- x 0.500
when "1100" =>
iREG0 <= '0' & iREG0(11 downto 1) ;
-- x 0.375
when "1101" =>
iREG0 <= "00" & iREG0(11 downto 2) ;
iREG1 <= "000" & iREG1(11 downto 3) ;
-- x 0.250
when "1110" =>
iREG0 <= "00" & iREG0(11 downto 2) ;
-- x 0.125
when "1111" =>
iREG0 <= "000" & iREG0(11 downto 3) ;
-- default
when others => NULL ;
end case ;
-- add
when S4 =>
iSTATE <= S5 ;
iRESULT <= iREG0 + iREG1 + ("0000" & iREG2) ;
-- return first state
when S5 =>
iSTATE <= S0 ;
-- default
when others =>
iSTATE <= S0 ;
end case ;
end if ;
end process ;
end Behavioral;
(under construction)
目次
前
次