電子オルゴール

電気的オルゴール

発端

CPUは小型で比較的高速でメモリ容量も多いATiny85を用いることにします。
この写真は、スピーカー、電池ボックスを接続していない回路部分のみです。非常にシンプルで、スイッチは左から、ON/OFF(赤)、音質選択(黒)、曲選択(白)となっています。

プログラミング環境

ここでは、Atmel Studio 7.0 を用います。
ダウンロードして、インストールしてください。このAtmel Studio 7.0はMicrosoftのVisual Studioを元にしているので、Visual Studioが使えれば、問題なく簡単に使えます。

 プログラミングに当たって、使用するATiny85のハードウェアの概要や、使用方法を理解しておく必要があります。どのように利用するのかを理解しないでプログラムは絶対に出来ません。このマニュアルを重要なところだけでもまずよくでおきましょう。

基本設計

  1. スイッチは3つ。電源、選曲、音質変化
  2. 人間の耳では聞こえない周波数でPWMで変調し音を発生させる。
  3. 一音の減衰時間(decay time)を設定できる。
  4. 小型スピーカを用いる。
変調信号が耳で聞こえないようにするには、20KHz以上のPWMでなければいけませんが、この中で目的の波形を作るにはこの10倍以上が必要があります。このように、かなり早いスピードで処理しなければいけないのでPLLを用いて64MHzで動作させるしかありません。64MHzで256分周させると250KHzのクロックになり、大体必要とされる周波数になります。
ポート設計
CPUの基本的な機能は下図のようになります。一つのピンに幾つかの機能がありますが、実際にはこのうちの一つのみを選択する必要があります。これがポート設計です。


制約事項
  1. 音符の進行をタイマー0の割り込みで行う。
  2. PWMの出力をタイマー1で行う。→出力はOC1A(PB1,Pin6、OC1B(PB4,Pin3)
  3. 電源on/offは割り込みINT0(Pin7)で行う。
  4. 選曲と音質変化はPCINT0(Pin5)、PCINT3(Pin2)で行い、割り込んでからどちらからの割り込みなのかを判断する。
  5. 電源はINT0の割り込みを用いる。電源offの場合は省電力モードにする。
  6. 電源の電池は単四を三本とする。
以上の制約事項から以下のようにピンの機能を決定します。

ピン番号機能
1reset
2音質変更スイッチ(PCINT3, PB3)
3スピーカ出力(PB4)
4GND
5選曲変更スイッチ(PCINT0, PB0)
6スピーカ出力(PB1)
7INT0で電源on/off変更スイッチ(PB2)
8+電源(4.5V)
PWMを用いて矩形波の音を作る

ここでは、781.25Hzの波形を作ることを目標にします。単純にこの波形を作るのではなく、PWMを用いて、上記の画像の一周期の時間(パルス幅狭+パルス幅広)を1.28msにします。一つのパルス間隔は4μsで250kHzとなり、人間の耳には聞こえません。従って破線で示した積分された値の781.25Hzの音しか聞こえないことになります。
 
単純な矩形波を作る
 ここで一番重要な役割を担う、タイマー/カウンターのブロック図を示します。
OCRnAはOutput Compare Registerでカウンター(TCNTn)と比較して、等しいなどの比較をします。nには0か1が入ります。
PWMの波形を作るためにタイマー1を用います。
タイマー1Cで基本周波数を設定します。PLLを用いてクロックを64MHzにしているので、256分周すると、64MHz/256=250kHzとなり、周期は4μsとなります。この周波数でPWM変調すればアナログ的な音がスピーカーから出力することが可能になります。パルス幅はタイマー1A及びタイマー1Bを用います。ここでは、Aの方のDuty Cycleを50%としてOCR1A=255、Bの方のDuty Cycleを25%としてOCR1B=63と設定します。



0x61に設定。
Bit7: CTC1
Bit6: PWM1A, AとB二つあるPWM機能のAの方を使用可能にします。
Bit5,4: COM1A1,COM1A0、通常のPWM動作をさせるのでコンベアマッチでクリアさせます。従ってCOM1A1=1、COM1A0=0に設定します。

Bit3,2,1,0: CS13, CS12, CS11, CS10、最高速のPCK/CKを用いるので0x01に設定します。


GTCCRはPWM機能のBの方の使用設定を行います。


Bit6: PWM1B OCR1BをPWMとして用いることを可能にします。OCR1Bとカウンターの値を比較して一致した場合にカウンターをゼロにリセットします。

Bit5,4: COM1B1, COM1B0

ここでは、COM1B1=0, COM1B0=1、すなわちOC1Bをトグル出力にします。
従って、GTCCR=0x50に設定します。





WGM02はTCCR0Bレジスタで定義する。ここでは、CTCモードにするので、mode=2となり、WGM02=0, WGM01=1,WGM00=0となります。
Bit7: FOC0A, PWM mode のときはゼロにする。
Bit6: FOC0B, PWM mode のときはゼロにする。
Bit2,1,0: CS02, CS01, CS00

ここでは一番高速のclkI/O/8を用いるので、TCCR0Bは0x02となります。




矩形波発生プログラム
#include <avr/interrupt.h>
int main(void)
{
    PORTB=0x04;   // PB2はsource、PB4,PB1はsinkの出力設定
    DDRB=0x1E;    // xxxoooox
    PLLCSR= 6;    // PCKE=1(64MHz) PLLE=1
    TCCR1=0x51;   // PMWA1enable,ToggleOC1Aを出力としてOCR1Cでコンベアマッチ(PB1,pin6), PCK
    OCR1C=255;    // 250kHz         // 64MHz/256=250kHz(4μs) 
    OCR1A = 127;  // duty cycle 50%  // コンベアレジスター
    OCR1B =  63;  // duty cycle 25%
       // General Timer/Counter Control Register
    GTCCR=0x50;   // PWMBenable(OCR1B,C)、ToggleOC1B (PB4,pin3を出力指定)、カウント開始
    OCR0A =64;
    TCCR0A=0x02;  // ClearTimer on Compare Match(CTC) mode
    TCCR0B=0x02;  // Clock Select (clkio/8, from prescaler)
    TIMSK=_BV(OCIE0A);      // Timer Interrupt Mask Register
    sei();
    while (1)
    {
    }
}

uint8_t n=0;
int length=20;

ISR(TIMER0_COMPA_vect){
    if(--length==0) 
    {
        length=20;          // 基本周波数を決める
        n=~n;
        if(n==0) OCR1A=250; // PB1(Pin6) パルス幅を変える
        else OCR1A=10;      // Duty Cycleの変化で音質が変わる 
    }
}
 1 割り込み処理のため
 2 
 3 
 4 
 5 方向レジスタの設定
 6 PLLで高速の設定
 7 
 8 
 9 
10 
11 
12 
13 2MHz/64=31.3kHz
14 
15 CLKIO/8=2MHz
16 タイマー0からの割り込み許可
17 CPUの割り込み許可
18 永久ループ
19 
20 
21 
22 
23 
24 
25 
26 タイマー0からの割り込み処理ルーチン
27 32μs毎に割り込む
28 
29 781.25Hzの波形を作る
30 1.0/(40*32e-6)=781.25
31 
32 
33 
34 




単純な正弦波の音を作る
 正弦波といっても、ここではsin波ではなく、8bitの中に納まるようにまた、使いやすいようにしています。数式的には128*(1-cos(θ))の一周期分を表にして保持しています。

正弦波の音を作る
#include <avr/interrupt.h>
uint8_t wave[]={0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,17,18,20,21,23,25,27,29,31,33,35,37,40,
    42,44,47,49,52,54,57,59,62,65,67,70,73,76,79,82,85,88,90,93,97,100,103,106,109,112,115,118,121,
    124,127,131,134,137,140,143,146,149,152,155,158,162,165,167,170,173,176,179,182,185,188,190,193,
    196,198,201,203,206,208,211,213,215,218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,
    244,245,246,248,249,250,250,251,252,253,253,254,254,254,255,255,255,255,255,255,255,254,254,254,
    253,253,252,251,250,250,249,248,246,245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,
    220,218,215,213,211,208,206,203,201,198,196,193,190,188,185,182,179,176,173,170,167,165,162,158,
    155,152,149,146,143,140,137,134,131,128,124,121,118,115,112,109,106,103,100,97,93,90,88,85,82,79,
    76,73,70,67,65,62,59,57,54,52,49,47,44,42,40,37,35,33,31,29,27,25,23,21,20,18,17,15,14,12,11,10,
    9,7,6,5,5,4,3,2,2,1,1,1,0,0,0};

int main(void)
{
    PORTB=0x04;   // PB2はsource、PB4,PB1はsinkの出力設定
    DDRB=0x1E;    // xxxoooox
    PLLCSR= 6;    // PCKE=1(64MHz)
    TCCR1=0x51;   // PMWA1enable,ToggleOC1Aを出力としてOCR1Cでコンベアマッチ(PB1,pin6), PCK
    OCR1C=255;    // 250kHz         // 64MHz/256=250kHz(4μs)
    OCR1A = 127;  // duty cycle 50%  // コンベアレジスター
    OCR1B =  63;  // duty cycle 25%
    // General Timer/Counter Control Register
    GTCCR=0x50;   // PWMBenable(OCR1B,C)、ToggleOC1B (PB4,pin3を出力指定)、カウント開始
    OCR0A =64;
    TCCR0A=0x02;  // ClearTimer on Compare Match(CTC) mode
    TCCR0B=0x02;  // Clock Select (clkio/8, from prescaler)
    TIMSK=_BV(OCIE0A);      // Timer Interrupt Mask Register
    sei();
    while (1)
    {
    }
}

uint8_t cnt=0, n=8;

ISR(TIMER0_COMPA_vect){
    OCR1A = wave[cnt];
    cnt+=n;
}
 1 割り込みを使う
 2 正弦波表
 3 128*(1-cos(θ))
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 nは基本周波数を決める
35 
36 タイマーからの割り込み
37 正弦波表の値
38 表を飛び飛びに読み込む
39 


正弦波を強弱をつけてワウワウさせる


正弦波強弱プログラム
#include <avr/io.h>

#include <avr/interrupt.h>
uint8_t wave[]={0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,17,18,20,21,23,25,27,29,31,33,35,37,40,
  42,44,47,49,52,54,57,59,62,65,67,70,73,76,79,82,85,88,90,93,97,100,103,106,109,112,115,118,121,
  124,127,131,134,137,140,143,146,149,152,155,158,162,165,167,170,173,176,179,182,185,188,190,193,
  196,198,201,203,206,208,211,213,215,218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,
  244,245,246,248,249,250,250,251,252,253,253,254,254,254,255,255,255,255,255,255,255,254,254,254,
  253,253,252,251,250,250,249,248,246,245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,
  220,218,215,213,211,208,206,203,201,198,196,193,190,188,185,182,179,176,173,170,167,165,162,158,
  155,152,149,146,143,140,137,134,131,128,124,121,118,115,112,109,106,103,100,97,93,90,88,85,82,79,
  76,73,70,67,65,62,59,57,54,52,49,47,44,42,40,37,35,33,31,29,27,25,23,21,20,18,17,15,14,12,11,10,
9,7,6,5,5,4,3,2,2,1,1,1,0,0,0};

int main(void)
{
  PORTB=0x04;   // PB2はsource、PB4,PB1はsinkの出力設定
  DDRB=0x1E;    // xxxoooox
  PLLCSR= 6;    // PCKE=1(64MHz)
  TCCR1=0x51;   // PMWA1enable,ToggleOC1Aを出力としてOCR1Cでコンベアマッチ(PB1,pin6), PCK
  OCR1C=255;    // 250kHz        64MHz/256=250kHz(4μs)
  OCR1A = 127;  // duty cycle 50%    コンベアレジスター
  OCR1B =  63;  // duty cycle 25%
  // General Timer/Counter Control Register
  GTCCR=0x50;   // PWMBenable(OCR1B,C)、ToggleOC1B (PB4,pin3を出力指定)、カウント開始
  OCR0A =64;
  TCCR0A=0x02;  // ClearTimer on Compare Match(CTC) mode
  TCCR0B=0x02;  // Clock Select (clkio/8, from prescaler)
  TIMSK=_BV(OCIE0A);    // Timer Interrupt Mask Register
  sei();
  while (1)
  {
  }
}
#define modulate 60
uint8_t cnt=0, n=16,length=modulate,m=0;

ISR(TIMER0_COMPA_vect){     // 32μ秒で割り込む
  if(--length==0){      // 32e-6*60*256=0.5   0.5秒周期でワウワウ
    m++;
    length=modulate;
  }
  OCR1A = wave[(cnt+m)%256];
  OCR1B = wave[cnt];
  cnt+=n;
}
 1 
 2 
 3 割り込みを使う
 4 正弦波表
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 変調周期の設定
36 
37 
38 タイマー0からの割り込み
39 
40 
41 
42 
43 低周波で変調するために飛び飛びにする
44 基本の正弦波
45 
46 


減衰正弦波の音を作る
 ピーンという金属をはじいたような、風鈴のような音を作ります。これが、オルゴール音になります。
 タクトスイッチを押すと鳴るようにします。
波形では、現実の波形ではありませんが、雰囲気として次の図のような波形になります。

8 bitの値なので0~255の間の値になります。

減衰正弦波の音を作るプログラム
#include <avr/io.h>
#include <avr/interrupt.h>
uint8_t wave[]={0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,10,11,12,14,15,17,18,20,21,23,25,27,29,31,33,35,37,40,
  42,44,47,49,52,54,57,59,62,65,67,70,73,76,79,82,85,88,90,93,97,100,103,106,109,112,115,118,121,
  124,127,131,134,137,140,143,146,149,152,155,158,162,165,167,170,173,176,179,182,185,188,190,193,
  196,198,201,203,206,208,211,213,215,218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,
  244,245,246,248,249,250,250,251,252,253,253,254,254,254,255,255,255,255,255,255,255,254,254,254,
  253,253,252,251,250,250,249,248,246,245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,
  220,218,215,213,211,208,206,203,201,198,196,193,190,188,185,182,179,176,173,170,167,165,162,158,
  155,152,149,146,143,140,137,134,131,128,124,121,118,115,112,109,106,103,100,97,93,90,88,85,82,79,
  76,73,70,67,65,62,59,57,54,52,49,47,44,42,40,37,35,33,31,29,27,25,23,21,20,18,17,15,14,12,11,10,
9,7,6,5,5,4,3,2,2,1,1,1,0,0,0};

volatile uint8_t stop=0;
int i;
int main(void)
{
  PORTB=0x0C;   //
  DDRB=0x12;    // xxxo1iox
  PLLCSR= 6;    // PCKE=1(64MHz)
  TCCR1=0x51;   // PMWA1enable,ToggleOC1Aを出力としてOCR1Cでコンベアマッチ(PB1,pin6), PCK
  OCR1C=255;    // 250kHz        64MHz/256=250kHz(4μs)
  OCR1A = 127;  // duty cycle 50%    コンベアレジスター
  OCR1B =  63;  // duty cycle 25%
  // General Timer/Counter Control Register
  GTCCR=0x50;   // PWMBenable(OCR1B,C)、ToggleOC1B (PB4,pin3を出力指定)、カウント開始
  OCR0A =64;
  TCCR0A=0x02;  // ClearTimer on Compare Match(CTC) mode
  TCCR0B=0x02;  // Clock Select (clkio/8, from prescaler)
  TIMSK=_BV(OCIE0A);    // Timer Interrupt Mask Register
  PCMSK=0;      // その他の割り込みoff
  MCUCR |= 2;     // INT0 立ち下がりトリガー
  GIMSK =(1<<INT0); // INT0 割り込みON
  sei();        // CPU割り込みON
  stop=1;
  while (1)
  {
    for(i=0; i<10000; i++);
  }
}
#define modulate 80
uint8_t cnt=0, n=16,length=modulate,m=128, k=0;

ISR(TIMER0_COMPA_vect){     // 32μ秒で割り込む
  if(--length==0 && stop){ 
    length=modulate;
    if(m++==255) {
      stop=0;
      m=0;
    }
  }
  if(stop)
  {
    OCR1A = wave[(cnt+m)%256];
    OCR1B = wave[cnt];
    cnt+=n;
  }
}

ISR(INT0_vect){                   // 外部スイッチ(INT0)
  cli();
  stop=1;
  length=modulate;
  cnt=0;
  m=128;
  sei();
}
 1 
 2 
 3 割り込みを使う
 4 正弦波表
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 変調周期の設定
42 mは最大値の位置
43 
44 タイマー0からの割り込み
45 半周期で止まるようにする
46 
47 最小になった時に停止
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67