2018年1月31日水曜日

Arduino LFO LPFの検討

MCP4922の後段に入れるLPFを検討しました。仕様により100Hzまでの波形(ただしサイン波だけではない)でよく、サンプリング周波数が15kHz程度なので8kHzをカットオフ周波数として実験しました。

あまり複雑な回路は組みたくないので、CR2次、CR4次、4次アクティブフィルタを比較しました。

回路図


2次CR LPF

fc = 1 / (2 * π * C * R) ≒ 7962Hz の1次CR LPFを2段直結しました。

4次CR LPF

同じく1次CR LPFを4段直結しました。

4次VCVS LPF バターワース特性

VCVS(正帰還、サレンキ-)フィルタは高周波数で利得が上がってしまう欠点がありますが、マルチフィードバック(多重帰還)フィルタより部品数が少なく済み、位相も反転しないメリットがあります。

参考「サレンキー型フィルタの阻止域特性を改善 (1/2)」「Filtering 101 : サレンキー回路 vs 多重帰還回路

4次バターワース特性は「OPアンプ大全」に載っている<表4-2>バターワース設計表から

ステージ Q F0
1段目 0.5412 1.0000
2段目 1.3065 1.0000

「OKAWA Electric Design」のフィルター計算ツールを使って定数を求めました。(一部手持ちのもので近い値に変更しています)

AC解析


2次CR LPF

15kHzでの減衰率は-16dB程度です。10kHzから100kHzの減衰を見ると、だいたい40dB/decになっています。

4次CR LPF

15kHzでの減衰率は-34dB程度です。10kHzから100kHzの減衰を見ると、だいたい62dB/decになっています。

4次VCVS LPF バターワース特性

なぜだか通過域のゲインが-3dBになってしまいました(@@?

15kHzでの減衰率は-27dB程度です。通過域と比較すると-24dB程度。10kHzから100kHzの減衰を見ると、だいたい80dB/decになっています。

4次CR LPFと比較するとカットオフ周波数付近の肩が急峻になっています。

過渡解析


2次CR LPF

4次CR LPF

4次VCVS LPF バターワース特性

CR LPFの2次、4次は波形がなまっているだけですが、4次VCVS LPF バターワース特性は立ち上がりが速い代わりにオーバーシュートが現れています。

WaveSpectraで測定


Arduino LFOで50Hzのサイン波を出力してWaveSpectraでスペクトラムを見てみました。

4次VCVS LPF バターワース特性 ブレッドボード配線図


OPAMP: NJM13404
電源: 5V/3.3V安定化電源 Ver.2

Arduinoの電源: 単3✕6
Audio I/F: TASCAM US-144 MKII Guitar入力
窓関数: FlatTop
Avg: 40
DDSテーブルサイズ: 12bit✕2048

フィルタなし

2次CR LPF

4次CR LPF

4次VCVS LPF バターワース特性

単純に50Hzのサイン波の綺麗さで比較すると4次CR LPFが良さそうです。

ステップ応答


Arduino LFOで50Hzの矩形波を出力してオシロで測定しました。

フィルタなし

2次CR LPF

4次CR LPF

4次VCVS LPF バターワース特性

ステップ応答はシミュレーションと同じく、CR LPFは次数が高いとなまりが大きくなり、4次VCVS LPF バターワース特性は立ち上がりが急峻でオーバーシュートが出ています。

どれにするかは悩み中。

2018年1月29日月曜日

Arduino LFO DDSのテーブルサイズの影響&処理時間の計測

直感的にDDSで使う波形テーブルはビット数と要素数が大きいほどきれいな波形が出力できそうな感じがします。数学的な検証はわたくしにはムリなのでWaveSpectraで出力波形の歪を測定しました。

テストスケッチ <MCP4922_DDS_WaveTableSize_Test.ino>

/*
   Arduino LFO
     DDSの波形テーブルの検証

   2018.01.26

*/
#include <SPI.h>
#include "avr/pgmspace.h"

#include "wavetable_12bit_8k.h"

#define PIN_CHECK  (0)
#define BIT_LENGTH_8  (0)

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

// Pin Assign
const int MCP4922Ldac = 9;
const int MCP4922Cs = 10;

#if (PIN_CHECK)
const int CheckPin1 = 18;      // A4
const int CheckPin2 = 19;      // A5
#endif

// MCP4922
SPISettings MCP4922_SPISetting(8000000, MSBFIRST, SPI_MODE0);

// Parameter
double drate = 50.0;                 // initial output rate (Hz)
const double refclk = 15625.0;       // = 16MHz / 8 / 128

// DDS
volatile uint32_t phaccu;
volatile uint32_t tword_m;

//-------------------------------------------------------------------------------------------------
// Interrupt Service Routine
//

// param
//   channel: 0, 1
//   val: 0 .. 4095
void MCP4922Write(bool channel, uint16_t val)
{
  uint16_t cmd = channel << 15 | 0x3000;
  cmd |= (val & 0x0fff);

  digitalWrite(MCP4922Ldac, HIGH);
  digitalWrite(MCP4922Cs, LOW);
  SPI.transfer(highByte(cmd));
  SPI.transfer(lowByte(cmd));
  digitalWrite(MCP4922Cs, HIGH);
  digitalWrite(MCP4922Ldac, LOW);
}

ISR(TIMER2_OVF_vect)
{
#if (PIN_CHECK)
  digitalWrite(CheckPin1, HIGH);
#endif

  // synthesize
  phaccu = phaccu + tword_m;

  // テーブルサイズに合わせてシフトするビットを変更
  int idx = phaccu >> 19;  // use upper n bits (table size)

#if (BIT_LENGTH_8)
  MCP4922Write(0, pgm_read_word_near(sin_table + idx) << 4);
#else
  MCP4922Write(0, pgm_read_word_near(sin_table + idx));
#endif

#if (PIN_CHECK)
  digitalWrite(CheckPin1, LOW);
#endif
}

//-------------------------------------------------------------------------------------------------
// Setup
//

// TIMER2 setup
void Setup_timer2()
{
  // non-PWM / Normal port operation, OC0A disconnected.
  cbi (TCCR2A, COM2A0);
  cbi (TCCR2A, COM2A1);

  // Mode 7 / Fast PWM
  sbi (TCCR2A, WGM20);
  sbi (TCCR2A, WGM21);
  sbi (TCCR2B, WGM22);

  // 16000000 / 8 / 128 = 15625 Hz clock
  OCR2A = 127;

  // Timer2 Clock Prescaler to : 8
  cbi (TCCR2B, CS20);
  sbi (TCCR2B, CS21);
  cbi (TCCR2B, CS22);
}

void setup()
{
  tword_m = pow(2, 32) * drate / refclk;  // calculate DDS tuning word;

#if PIN_CHECK
  pinMode(CheckPin1, OUTPUT);
#endif

  pinMode(MCP4922Cs, OUTPUT);
  digitalWrite(MCP4922Cs, HIGH);  // set CS as inactive
  pinMode(MCP4922Ldac, OUTPUT);
  SPI.begin();
  SPI.beginTransaction(MCP4922_SPISetting);

  Setup_timer2();

  // disable interrupts to avoid timing distortion
  cbi(TIMSK0, TOIE0);             // disable Timer0 !!! delay() is now not available
  sbi(TIMSK2, TOIE2);             // enable Timer2 Interrupt

  sei();
}

//-------------------------------------------------------------------------------------------------
// Main Loop
//
void loop()
{
}

Github:
https://github.com/ryood/ArduinoLFO/tree/e2cc9572cc76aa3309c5c6d5e4b51de9a17d279d/Arduino/MCP4922_DDS_WaveTableSize_Test

製作中のArduino LFOのスケッチから不要な部分を削除しました。

テーブルは、ぴゅんぴゅん2号で使っていた8bit✕256と、MCP4922のビット長12bitにして要素数が1024、2048、4096、8192にしたものを比較しました。

DDSはフェーズアキュムレータの上位bitをテーブルのインデックスとして使うので、テーブルの要素数は2^nである必要があります。

Arduino Unoのフラッシュメモリのサイズが32kBで(その一部はプログラムで使用)、int16_t型(2Byte)のテーブルなので、8192以上はメモリーオーバーします。

測定時にはUSB経由のノイズを回避するためにArduinoは電池電源(単3✕6)を使用しました。

Audio I/F: TASCAM US-144 MKII MIC/Line入力
窓関数: FlatTop
Avg: 100

8bit長 256要素

12bit長 1024要素

12bit長 2048要素

12bit長 4096要素

12bit長 8192要素

テーブルが8bit長のときと、12bit長のときでははっきりと歪が改善されます。また、テーブルの要素数が増えると高次の歪が減ります。

聴感で敏感な1kHz~10kHzあたりを比較すると、やはりテーブルの要素数は大きければ多いほどよさそうです。8192になると、サンプリング周波数の15kHz付近のエイリアスが支配的になってきます。

サイン波の傾きが大きい原点あたりのテーブルの値を比較すると、

12bit長 1024要素
// table of 2048 values / one period / stored in flash memory/*** MAX_VALUE = 4096 SAMPLE_NUM = 1024 delta = 3.999023***/
/*** sine wave ***/
const PROGMEM uint16_t sin_table[] = {
  2047  ,
  2060  ,
  2072  ,
  2085  ,
  2097  ,
  2110  ,
  2122  ,
  2135  ,
  2147  ,
  2160  ,

12bit長 8192要素
// table of 8192 values / one period / stored in flash memory
const PROGMEM  uint16_t sin_table[]  = {
  2047,
  2049,
  2050,
  2052,
  2053,
  2055,
  2056,
  2058,
  2060,
  2061,
  2063,

1024要素の場合は値がトビトビで、サンプリング・ポイントでの誤差が歪となって現れる結果だと思います。

ちなみに、12bit長8192要素のテーブルでも1kHzのサイン波を出力すると以下のような波形になります。



随分ガタガタしてますが、12bit@15.625kHzなのでしかたありません(^q^; Arduino Uno+MCP4922で出せる波形もこのあたりが上限だと思います。

処理時間


前回のUIを付けたスケッチで処理時間を測定しました。テーブルは12bit長、2048要素です。UART_TRACEは無効にしています。

Arduinoのスケッチ <MCP4922_LFO.ino>

Github:
https://github.com/ryood/ArduinoLFO/tree/e2cc9572cc76aa3309c5c6d5e4b51de9a17d279d/Arduino/MCP4922_LFO

割り込みとSPI処理

ch1:LDAC ch2:A4

SPI通信にかかっている時間は20usで同じですが、割り込み処理時間は38.4us→44usと増えています。増やした処理は、配列をint16_tにしたことと、switch文で出力する波形を切り替えているぐらいですが、もはやカツカツな感じです。

割り込みとメインループ

ch1:A5 ch2:A4

analogRead()でPOT2個の値を読み取るようにしただけですが、loop()内の処理時間(ch1:A5がHの時間)が増えています。

SPI通信


ch1:MOSI ch2:SCK

メモ:


ノコギリ波はテーブル参照しなくても、位相値(phaccu)の値を見ればできそう?上昇下降は足し算引き算で?

三角波もできそう?(計算量によりそうですが)

矩形波も位相値でH/Lを切り替え?

処理時間次第ですがノイズ(S&H)もできるかも?

2018年1月26日金曜日

Arduino LFO ブレッドボードで実験

UIをつけて、スケッチを書いてみました。

ブレッドボード配線図


Arduinoのスケッチ <MCP4922_LFO.ino>

https://github.com/ryood/ArduinoLFO/tree/de0d2a7960f098d09f66701f78884aede1355075/Arduino/MCP4922_LFO

外部割り込みの使用


波形選択用のタクトスイッチの読み取りは外部割り込み(INT0)を利用しました。

attachInterrupt(digitalPinToInterrupt(ButtonWaveShape), waveshape_pushed, FALLING);

loop()内のポーリングでdigitalRead()を使って読み取ると、スイッチの押し下げ→開放状態をそれぞれ読み取って比較する必要がありますが、外部割り込みを使うと立ち上がり、または立ち下がりを補足できるので便利です。

デメリットはArduino Unoの場合、外部割り込みに使えるピンがD2とD3の2本に限られていることです。MCP4922とのSPI通信でD9~D13を使っていて、D0とD1はPCとのシリアル通信のために開けておいた方がいいので、その他に使えるピンがなかなか制限されてきます。

また、波形生成に使っているTimer2よりも割り込みの優先順位が高いので気をつけておく必要があります。

ATMega328PのDATASHEETに割り込みベクタテーブルがのっていますが、割り込みベクター番号が若いほうが優先順位が高くなります。


INT0の割り込み処理を長々とやっているとTimer2の割り込みのタイミングが遅れてしまって、波形に揺れが生じる可能性があります。

チャタリング対策


外部割り込みをそのまま読み取るとチャタリングが発生したので対策しました。外部割り込み時に、読み取り状態を確定させるまでの時間(waveshape_pushed_wait)をセットして、Timer2の割り込み時に設定時間経過後、再度押し下げ状態を読み取って変化がなければ確定するようにしています。

単純に一定時間INT0を無視するようにしても、ある程度効果はありそうですが(^q^?

A/Dコンバーターの読み取り


LFOのRateとパルス幅をPOTで設定してADCで読み取る様にしましたが(analogRead())、無事処理できているようです。

ADC読み取りはloop()内でポーリングしていて、上記割り込みベクタテーブルを見ると、ADCは22番で優先順位が低く、Timer2の割り込みのタイミングには影響しないと思います。

※Arduinoのソースを見ると、ADCの完了待ちはADCSRAレジスタを監視していて、割り込みは使用していないようです。

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino\wiring_analog.c
// start the conversion
sbi(ADCSRA, ADSC);
// ADSC is cleared when the conversion finishes
while (bit_is_set(ADCSRA, ADSC));

LEDの点灯はPORTで処理


複数のLEDの点灯はdigitalOut()で処理するとやたら煩雑です。ArduinoのD0~D7の実体はATMega328PのPORTDなので直接レジスタ操作するようにしました。

  // Write to LEDs (D3~D7)
  byte portd_bits = (1 << (waveshape_sel + 3)) | (PORTD & 0x07);
  PORTD = portd_bits;

しかし毎度のことながらビット演算はややこしくて混乱してしまいます。Serial.print()で状態を確認してプログラミングしました(^q^;;;

次回は動作状態や、DDSの波形テーブルのサイズが与える影響を調べてみたいと思います。

メモ:


矩形波のパルス幅は未実装

2018年1月24日水曜日

Tr回路の実験 OPAMP+エミッタフォロワ 発振対策

迷走の果て・Tiny Objects」のedyさんに、エミッタ・フォロワの発振対策にベース抵抗を入れる方法を教えていただいたので実験してみました。

トランジスタ技術の記事を参考にしました。

シミュレーション「Tr回路の実験 OPAMP+エミッタフォロワ
実験(発振した)「Tr回路の実験 OPAMP+エミッタフォロワ その2

回路図

ブレッドボード配線図


ベース抵抗R5を入れたのと、コレクタにバイパス・コンデンサ(0.1uF積セラ)を入れたのが主な変更点です。

ベース抵抗R5の値を、47Ω、100Ω、200Ω、1kΩにして入出力を測定しました。また、電源電圧が±4.5Vだと出力波形がクリップしてしまうので±5Vにして測定しました。負荷抵抗RL=1kΩです。

OPAMP: NJM4580D
NPN: 2SC1815GR
信号源: PCM5102Aファンクションジェネレータ+4次バターワースLPF  1Vp-p / 1kHz
電源電圧: +5.02V / -5.01V

ベース抵抗R5=47Ω

ch1:IN ch2:OUT

R5=47Ωだと発振波形が現れています。

ベース抵抗R5=100Ω

ch1:IN ch2:OUT

R5=100Ωでもまだ発振波形が現れています。

ベース抵抗R5=200Ω

ch1:IN ch2:OUT

R5=200Ωにすると発振波形が見られなくなりました(^q^/

ベース抵抗R5=1kΩ

ch1:IN ch2:OUT

ベース抵抗R5は負帰還ループ内に入っているので抵抗値による影響は少ないですが、調子に乗って1kΩにすると出力波形がクリップしてしまいました。

負荷による出力のクリップ


ベース抵抗R5=200Ωで負荷抵抗RL=100Ωと重くしてみました。

R5=200Ω RL=100Ω

ch1:IN ch2:OUT

負荷が重くなると、出力波形の下側がクリップします。が、特に発振は見られないようです。クリップは電源電圧による制約なので、電源電圧を±6.5Vまで上げてみると、クリップしなくなります。

R5=200Ω RL=100Ω (電源電圧±6.5V)

ch1:IN ch2:OUT

周波数特性


ベース抵抗R5=200Ω、負荷RL=100Ωで信号源を高周波数まで出せるAD9833ファンクションジェネレータにして測定しました。

OPAMP: NJM4580D
NPN: 2SC1815GR
信号源: AD9833ファンクションジェネレータ(バイパス出力)
電源電圧: +5.98V / -5.99V

500kHz

ch1:IN ch2:OUT

増幅率Avは、Av = 3.12V / 560mV ≒ 5.57(14.9dB)です。

1kHzの時の増幅率Avは、Av = 6.04V / 1.04V ≒ 5.81(15.3dB)なので、比較すると-0.38dBです。

1MHz

ch1:IN ch2:OUT

増幅率Avは、Av = 1.24V / 520mV ≒ 2.38(7.53dB)、@1kHzと比較すると-7.77dBです。

高速広帯域のOPAMP


OPAMPの特性が回路の周波数特性に大きく影響するので、高速広帯域のNJM2137に差し替えて測定してみました。

NJM2137のは電源電圧が定格で±1.35V~±6Vで、差動入力が絶対定格で±3Vとなっているのであんまりむちゃなことはできません(^q^;

OPAMP: NJM2137D
NPN: 2SC1815GR
信号源: AD9833ファンクションジェネレータ(バイパス出力)
電源電圧: +5.98V / -5.99V

500kHz

ch1:IN ch2:OUT

増幅率Avは、Av = 3.04V / 560mV ≒ 5.43(14.7dB)です。

1MHz

ch1:IN ch2:OUT

増幅率Avは、Av = 2.80V / 520mV ≒ 5.38(14.6dB)です。確かに帯域はNJM4580より広くなっています。

NJM2137は発振しやすくて使い所が難しかったのですが、ベース抵抗を入れると発振波形が出なくなったようです。

ためしにベース抵抗を外してみても(@1kHz)


ch1:IN ch2:OUT

発振波形は現れません。ただしOPAMPの帯域が広いので(GBW 200MHz)、トラ技の記事にあるようにオシロの帯域(100MHz)より高い周波数で発振している可能性が高いと思います。←これは確認できない(^q^;

ビーズ


ベース抵抗またはフェライトビーズを入れるとよいと書いてあったのでフェライトビーズをいれてみましたが効果はあまりありませんでした。(ビーズの仕様は不明)


※こういう使い方でいいのかどうかもわかりません(@@?


ch1:IN ch2:OUT

単電源OPAMP


ベース抵抗を入れない場合は、単電源OPAMPでも負荷が47Ωの場合発振してそうだったので、試してみました。

回路図

ブレッドボード配線図


OPAMP: NJM13404D
NPN: 2SC1815GR
信号源: AD9833ファンクションジェネレータ(バイパス出力)
電源電圧: +9.01V

ベース抵抗R5=200Ω 負荷抵抗RL=100Ω

ch1:IN ch2:OUT

ベース抵抗R5=200Ω 負荷抵抗RL=47Ω

ch1:IN ch2:OUT

ベース抵抗R5=200Ω 負荷抵抗RL=10Ω

ch1:IN ch2:OUT

振幅は多少小さくなりますが、負荷抵抗10Ωでも駆動できているようで、発振波形も見られません。

周波数特性


オーソドックスな単電源OPAMPのNJM13404と、高速単電源OPAMPのNJM2742も試してみました。RL=100Ω。

NJM13404 500kHz

ch1:IN ch2:OUT

NJM2742 500kHz

ch1:IN ch2:OUT

500kHzではNJM13404は三角波に近い波形になってしまいましたが、高速なNJM2742はサイン波形を保っています。

Av = 1.8V / 560mV ≒ 3.21(10.1dB)。

1kHzのとき(NJM13404の場合)、Av = 3.16 / 560mV = 5.64(15.0dB)なので、比較すると-4.92dB。振幅は小さくなりますが、波形は両電源のNJM2137よりきれいかもしれません。

NJM2742 1MHz

ch1:IN ch2:OUT

Av = 960mV / 520mV ≒ 1.85(5.33dB)で、1kHのときと比較して-9.67dB。

NJM2742も発振しやすくて使いにくかったのですが、ベース抵抗を入れることによって発振しなくなったようです。エミッタフォロワの発振対策としてベース抵抗を入れる効果は高いと言えます。