2017年7月27日木曜日

AD9833ファンクションジェネレータの構想


あまり時間をかけたくないので要件を絞った。(というか実験の結果、簡単にできそうなのを選んだ)

  • 単電源波形のサイン波と三角波を出力。
  • 出力振幅は3Vp-p程度。
  • 出力帯域は可聴帯域から、できる範囲で広く。
  • 電池駆動でコンパクトな筐体(PCM5102ファンクションジェネレータ程度)

迷走の果て・Tiny Objects」さんは、RF帯域のフィルタリングを実験されていて勉強になりますが、私はRFには全く手をつけてなく(手に負えない)、オーディオ帯域のちょっと上ぐらいまでで用は済みそう。

AD9833は出力が0.6Vp-pで振幅が小さすぎるので、OPAMPで増幅することを中心にテストしてみた。

ブレッドボード配線図

Arduinoのひとつ下のブレッドボードがAD9833モジュール用で、ArduinoとSPI接続している。

AD9833の出力から10kΩ/BのPOTで出力レベルを減衰させて、そのひとつ下のブレッドボードの出力部につないでいる。

出力部は非反転増幅回路で振幅を増幅し、同じOPAMPでボルテージフォロアを組んで出力を増強している。

AD9833の出力は単電源波形で出力部の電源も+5Vの単電源としたので、OPAMPは単電源用(もしくはGNDまで出せるフルスイング)のOPAMPを使う。

Arduinoの上のブレッドボードはUI部で、aitendoのI2C LCDのSPLC792-I2Cと周波数選択用のロータリー・エンコーダーと波形選択用のタクトスイッチ。

SPL792-I2Cが3.3V仕様なので、ここは3.3V電源にしている。aitendoのSPLC792-I2Cは在庫がないことが多かったが、ATD1602C-P というのが出たようだ(ちょっと高い)。また、秋月のAQM0802AAQM1602Aもコマンドコンパチなので使えると思う。

SPLC792-I2CはオンボードでI2Cのプルアップ抵抗が載っているので、I2CのSCL、SDAは電源電圧の3.3Vでプルアップされる。RESET線はArduinoからの出力レベルが5Vなので、6.8kと12kのRで分圧して3.3Vとしている。

Arduinoのスケッチ
<AD9833_Test.ino>

#include <SPI.h>
#include <Wire.h>
#include <stdio.h>

#define UART_TRACE  (0)

#define TITLE_STR1  ("AD9833 FG")
#define TITLE_STR2  ("20170726")

//--------------------------------------------------------------------------------
// pin assign
//

// AD8933
const int FSYNC = 10;                       // Standard SPI pins for the AD9833 waveform generator.
const int CLK = 13;
const int DATA = 11;

// Rotary Encoder
const int RE_A = 2;
const int RE_B = 3;

// Tact SW
const int SW1 = 4;

// I2C LCD
const int resetPin = 17;  // analog pin 3
const int sdaPin = 18;    // analog pin 4
const int sclPin = 19;    // analog pin 5
const int i2cadr = 0x3e;
const byte contrast = 32; // 最初は大きめにして調整する

//--------------------------------------------------------------------------------
// constants
//

const float refFreq = 25000000.0;           // On-board crystal reference frequency

// Wave Form
const int wfSine     = 0;
const int wfTriangle = 1;
const int wfIndexMax = 2;

// mask of AD9833 Control Register
const uint16_t waveFormTable[] = {
  0x2000, // Sine
  0x2002  // Triangle
};

const char waveFormName[][20] = {
  "SIN               ",
  "TRI               "  
};

const uint32_t frequencyTable[] = {  
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  9,
  10,
  20,
  30,
  40,
  50,
  60,
  70,
  80,
  90,
  100,
  200,
  300,
  400,
  500,
  600,
  700,
  800,
  900,
  1000,
  2000,
  3000,
  4000,
  5000,
  6000,
  7000,
  8000,
  9000,
  10000,
  20000,
  30000,
  40000,
  50000,
  60000,
  70000,
  80000,
  90000,
  100000,
  200000,
  300000,
  400000,
  500000,
  600000,
  700000,
  800000,
  900000,
  1000000,
  2000000,
  3000000,
  4000000,
  5000000,
  6000000,
  7000000,
  8000000,
  9000000,
  10000000,
  11000000,
  12000000,
};

const int frequencyIndexMax = (sizeof(frequencyTable) / sizeof(uint32_t));

//--------------------------------------------------------------------------------
// Variables
//

int frequencyIndex = 27; // 1kHz
int waveFormIndex = 0;  // Sine wave

int prevFrequencyIndex = frequencyIndex;
int prevWaveFormIndex = waveFormIndex;

const char strBuffer[80];

//--------------------------------------------------------------------------------
// Main routins
//
void setup() {
  pinMode(RE_A, INPUT_PULLUP);
  pinMode(RE_B, INPUT_PULLUP);
  pinMode(SW1, INPUT_PULLUP);

#if UART_TRACE
  Serial.begin(9600);
  Serial.println("AD9833 UI Test.");
  sprintf(strBuffer, "%s %s", TITLE_STR1, TITLE_STR2);
  Serial.println(strBuffer);
  delay(1000);  
#endif

  // I2C LCD
  lcd_init();
  lcd_puts(TITLE_STR1);
  //lcd_move(0x40);
  lcd_pos(1, 1);
  lcd_puts(TITLE_STR2);

  // AD8933
  SPI.begin();
  delay(50);
  
  AD9833reset();
  delay(50);
  AD9833setFrequency(frequencyTable[frequencyIndex], waveFormTable[wfSine]);
}

void loop()
{
  readParams();

  uint32_t frequency = frequencyTable[frequencyIndex];
  int waveForm = waveFormTable[waveFormIndex];

#if UART_TRACE
  displayParamsSerial(frequency, waveForm);
#endif

  if (frequencyIndex != prevFrequencyIndex || waveFormIndex != prevWaveFormIndex) {
    prevFrequencyIndex = frequencyIndex;
    prevWaveFormIndex = waveFormIndex;

    // LCDに表示
    displayParamsI2CLCD(frequency, waveFormIndex);

    // AD9833に出力
    AD9833setFrequency(frequencyTable[frequencyIndex], waveFormTable[waveFormIndex]);
  }
}

//--------------------------------------------------------------------------------
// UI Input functions
//
void readParams()
{
  // 周波数設定 Rotary Encoderの読み取り
  frequencyIndex += readRE();
  if (frequencyIndex <= 0) {
    frequencyIndex = 0;
  }
  else if (frequencyIndex >= frequencyIndexMax) {
    frequencyIndex = frequencyIndexMax - 1;
  }

  // 波形設定SWの読み取り
  if (digitalRead(SW1) == LOW) {
    waveFormIndex++;
    if (waveFormIndex >= wfIndexMax) {
      waveFormIndex = 0;  // wfSine;
    }
    delay(200); // (とりあえず)チャタリング防止
  }
}

// Rotary Encoderの読み取り akizuki/Alps
int readRE()
{
  static uint8_t index;
  int retVal = 0;
  index = (index << 2) | (digitalRead(RE_B) << 1) | (digitalRead(RE_A));
  index &= 0b1111;
  switch (index) {
  // 時計回り
  case 0b0111:  // 01 -> 11
    retVal = 1;
    break;
  // 反時計回り
  case 0b1101:  // 11 -> 01
    retVal = -1;
    break;
  }
  delay(1);  // (とりあえず)チャタリング防止
  return retVal;
}

//--------------------------------------------------------------------------------
// UI Display functions
//
#if UART_TRACE
void displayParamsSerial(uint32_t frequency, int waveForm)
{
  Serial.print(waveFormIndex);
  Serial.print('\t');
  Serial.print("0x");
  Serial.print(waveForm, HEX);

  Serial.print('\t');
  
  Serial.print(frequencyIndex);
  Serial.print('\t');
  Serial.println(frequency);  
}
#endif

void displayParamsI2CLCD(uint32_t frequency, int waveFormIndex)
{
  //lcd_clear();

  // 周波数表示
  lcd_pos(0, 0);
  sprintf(strBuffer, "%8luHz", frequency);
  lcd_puts(strBuffer);

  // 波形表示
  lcd_pos(1, 0);
  lcd_puts(waveFormName[waveFormIndex]);
}

//--------------------------------------------------------------------------------
// I2C LCD: akizuki AQM0802 / aitendo SPLC792-I2C
//
void lcd_cmd(byte x)
{
  Wire.beginTransmission(i2cadr);
  Wire.write(0x00);
  Wire.write(x);
  Wire.endTransmission();
}

void lcd_data(byte x)
{
  Wire.beginTransmission(i2cadr);
  Wire.write(0x40);
  Wire.write(x);
  Wire.endTransmission();
}

void lcd_puts(const char *s)
{
  while(*s) lcd_data(*s++);
}

void lcd_init()
{
  // reset
  delay(500);
  pinMode(resetPin, OUTPUT);
  digitalWrite(resetPin, LOW);
  delay(1);
  digitalWrite(resetPin, HIGH);
  delay(10);
  // LCD initialize
  delay(40);
  Wire.begin();
  lcd_cmd(0x38); // function set
  lcd_cmd(0x39); // function set
  lcd_cmd(0x14); // interval osc
  lcd_cmd(0x70 | (contrast & 15)); // contrast low
  lcd_cmd(0x5c | (contrast >> 4 & 3)); // contrast high / icon / power
  lcd_cmd(0x6c); // follower control
  delay(300);
  lcd_cmd(0x38); // function set
  lcd_cmd(0x0c); // display on
  lcd_cmd(0x01); // clear display
  delay(2);
}

/*
void lcd_move(byte pos){
  lcd_cmd(0x80 | pos);
}
*/

void lcd_pos(byte raw, byte col) {
  lcd_cmd(0x80 | ((raw & 0x01) << 6) | col);
}

void lcd_clear() {
  lcd_cmd(0x01);
}

//--------------------------------------------------------------------------------
// AD9833: Waveform Generator
//

// AD9833 documentation advises a 'Reset' on first applying power.
void AD9833reset() {
  WriteRegister(0x100);   // Write '1' to AD9833 Control register bit D8.
  delay(10);
}

// Set the frequency and waveform registers in the AD9833.
void AD9833setFrequency(long frequency, int Waveform) {

  long FreqWord = (frequency * pow(2, 28)) / refFreq;

  int MSB = (int)((FreqWord & 0xFFFC000) >> 14);    //Only lower 14 bits are used for data
  int LSB = (int)(FreqWord & 0x3FFF);

  //Set control bits 15 ande 14 to 0 and 1, respectively, for frequency register 0
  LSB |= 0x4000;
  MSB |= 0x4000;

  WriteRegister(0x2100);
  WriteRegister(LSB);                  // Write lower 16 bits to AD9833 registers
  WriteRegister(MSB);                  // Write upper 16 bits to AD9833 registers.
  WriteRegister(0xC000);               // Phase register
  WriteRegister(Waveform);             // Exit & Reset to SINE, SQUARE or TRIANGLE
}

void WriteRegister(int dat) {

  // Display and AD9833 use different SPI MODES so it has to be set for the AD9833 here.
  SPI.setDataMode(SPI_MODE2);

  digitalWrite(FSYNC, LOW);           // Set FSYNC low before writing to AD9833 registers
  delayMicroseconds(10);              // Give AD9833 time to get ready to receive data.

  SPI.transfer(highByte(dat));        // Each AD9833 register is 32 bits wide and each 16
  SPI.transfer(lowByte(dat));         // bits has to be transferred as 2 x 8-bit bytes.

  digitalWrite(FSYNC, HIGH);          //Write done. Set FSYNC high
}

AD9833の正弦波、三角波の出力レベルは0.6Vp-p、矩形波は出力レベルが電源電圧なので、5V駆動だと0V/5Vの波形になり危険なので使わないことにした。

周波数切替は上位1桁を可変することにして、ロジックを考えるのがめんどくさかったのでテーブル参照にした。←まだメモリ余ってますし(^q^;

自作のPCM5102ファンクションジェネレータはキーパッドで1Hz単位で周波数を指定できるようにしているが、あんまり使わなくて、上位2桁の増減でほとんど間に合っている。上位2桁でも、周波数を大きく変えるときはロータリーエンコーダーをガチャガチャ回さないといけないのでこういう仕様にした。

出力部のOPAMP


単電源用のNJM13404、高速単電源用のNJM2742、高精度単電源用のNJM2119、フルスイングのAD8532、AD822を差し替えてようすをみてみた。

単電源用の定番のLM358はクロスオーバー歪があるので候補から外した。(ほんとにLM358は難儀な子)

周波数は300kHzあたりからOPAMPの品種の違いが顕著になる。

NJM13404

ch1:出力部の出力 ch2:AD9833の出力

ダメダメ

AD8532

少しいびつ

AD822

比較的きれい

NJM2742

スルーレート:10V/us、GBW:2MHz。高速と銘打っているので期待したが、変なノイズが乗っている。発振してるのかも。
もしかすると、ボルテージフォロアに耐えられない?

NJM2119

高精度だが、300kHzぐらいだと帯域外のようだ。

AD822を使ってみる。


手持ちの中ではAD822が良さそうなので周波数、波形を変えて測定した。

出力部の非反転増幅回路は1.2kΩと4.7kΩのRを使って、A = 1 + (4.7kΩ / 1.2kΩ) ≒ 4.9倍。AD9833の出力に入れているPOTは右いっぱいにまわして最大出力とした。

1kHz

ch1:出力部の出力 ch2:AD9833の出力

10kHz

100kHz

1MHz

1MHzになると出力レベルが低下し、ほとんど増幅されない。この辺の帯域は出力部をバイパスしたほうがよさそうだ。

三角波1kHz

三角波100kHz

100kHzになると三角波の上下端がなまり始める。

OPAMPの非反転増幅回路を通すと、正弦波で数100kHz程度までしか使えなさそうだが、高周波数のノイズはOPAMPの帯域によって自然と取れるようだ。

WaveSpectra


WaveSpectraを使って可聴帯域のようすをみてみた。USB経由のノイズを回避するために電池(単3×6)をArduinoの電源とした。

AD9833の出力

出力部通過

振幅が同程度になるようにオーディオ・インターフェイスの入力レベルを調整しているので正確ではないと思うが、出力部を通したほうがノイズが小さくなっている。

SPLC792-I2Cまわり


I2C信号

ch1:SDA ch2:SCL

信号レベルは3.3Vになっている。

RESET線の電圧はテスタで測って、3.13Vだった。

メモ:


消費電流(Arduino込) 73mA

RFアンプとは言わないまでも、OPAMPでもう少し高性能のものもあるが、だいたい両電源なので電源をちゃんと作らないといけない。いずれは試してみるとして、まずは数100kHzまでの単電源波形出力のファンクションジェネレータとして作ってみる。

現状だと5V/3.3Vの電源が必要になる。3.3VはI2C LCDだけに使うのでわざわざ作るのもくやしいが、2014年末に作った可変安定化電源で使っていて実績があるので、しょうがないかな~

3.3V電源だけだと出力振幅を稼げない。

設定パラメータを内部EEPROMに保存して、電源ON時に再現できる?