[ Main Page ]

Arduino SPI DAC

Arduinoの良いところは、たくさんのライブラリが公開されており、ハードの面倒な仕様が隠蔽されているところだが、 DAC等を高速に駆動したい場合などはポートを直接いじる必要がある。MCP4922はMicrochipのSPI 12bit DACで、 Settling Timeが4.5µsと比較的高速で、1個数百円程度で入手可能である。ArduinoにはSPIライブラリがあるので それを使用して駆動できるが、この場合はせいぜい数10kHz程度でしか駆動できない。

SPIライブラリを使用した一般的な遅い例

8(LDAC)、10(SS)、11(MOSI)、12(MISO)、13(SCK)

	#include <SPI.h>

	const int PIN_CS = 8;
	const int GAIN_1 = 0x1;
	const int GAIN_2 = 0x0;

	#define MCP4922_DAC_A    0x00
	#define MCP4922_DAC_B    0x80
	#define MCP4922_VREF_BUF 0x40
	#define MCP4922_GAIN_1X  0x20
	#define MCP4922_GAIN_2X  0x00
	#define MCP4922_SHDN     0x10

	void setup()
	{
	  pinMode(PIN_CS, OUTPUT);
	  SPI.begin();  
	  SPI.setBitOrder(MSBFIRST) ;
	  SPI.setClockDivider(SPI_CLOCK_DIV2);
	  SPI.setDataMode(SPI_MODE0);
	}

	void setOutput(unsigned int val)
	{
	  byte highByte = ((val >> 8) & 0xf) | MCP4922_GAIN_1X | MCP4922_VREF_BUF | MCP4922_SHDN;
	  digitalWrite(PIN_CS,HIGH);
	  digitalWrite(SS,LOW);
	  SPI.transfer(highByte);
	  SPI.transfer(val & 0xff);
	  digitalWrite(SS,HIGH);
	  digitalWrite(PIN_CS,LOW); // Latch Vout
	}

	void loop()
	{
	 TIMSK0 &= ~_BV(TOIE0); // disable Timer0 overflow interrupt
	 TIMSK2 &= ~_BV(TOIE2); // disable Timer2 overflow interrupt

	 while(1)
	 {
	   setOutput(1024);
	   setOutput(2048);
	 }
	}
      

直接レジスタを叩いて少し早くする

	#include <SPI.h>
	#define LDAC   8
	#define MCP4922_DAC_A    0x00
	#define MCP4922_DAC_B    0x80
	#define MCP4922_VREF_BUF 0x40
	#define MCP4922_GAIN_1X  0x20
	#define MCP4922_GAIN_2X  0x00
	#define MCP4922_SHDN     0x10

	void setup() {
	  pinMode(LDAC,OUTPUT) ;
	  SPI.begin();
	  SPI.setBitOrder(MSBFIRST);
	  SPI.setClockDivider(SPI_CLOCK_DIV2);
	  SPI.setDataMode(SPI_MODE0);
	}

	inline void setDAC(uint16_t i){
	  PORTB &= ~_BV(2); // CS(10)_Low
	  PORTB |= _BV(0); // LDAC(8) -> high
	  SPDR = (i >> 8) | MCP4922_GAIN_1X | MCP4922_VREF_BUF | MCP4922_SHDN;
	  while(!(SPSR & _BV(SPIF)));
	  SPDR = i & 0xff;
	  while(!(SPSR & _BV(SPIF)));
	  PORTB |= _BV(2); // CS_High
	  PORTB &= ~_BV(0); // LDAC -> low
	}

	void loop() {
	  while(1){
	    setDAC(1024);
	    setDAC(2048);
	  }
	}
      

テーブルからDACに送出 - SPI転送を待たずに次の処理を行う

レジスタ直操作でも、SPSRを待機して2バイト転送するのは意外と時間がかかるので、さらに高速にするためには、 待機中に別の演算を行うのが良い。但し、下記の方法の場合、関数を3回呼ばないと始めの値がlatch outされない。 100kHzで出力可能で、125kHzでもなんとか可能であった。この方法ではそれ以上の速度は難しかったが、 ATmega328pとBU9480Fで44.1kHzのSDカードプレーヤを作った人はいる様なので、USARTを使用してXCK等を使う別の方法はあるかもしれない。

	#include <TimerOne.h>
	#include <SPI.h>
	#include <math.h>
	#define LDAC 8 // ラッチ動作出力ピン
	#define MCP4922_DAC_A    0x00
	#define MCP4922_DAC_B    0x80
	#define MCP4922_VREF_BUF 0x40
	#define MCP4922_GAIN_1X  0x20
	#define MCP4922_GAIN_2X  0x00
	#define MCP4922_SHDN     0x10

	#define DAC_FS 10 // [us] = 100kHz

	uint8_t regH = 0, regL = 0;
	inline void latchDAC() {
	  // Latch DAC output (It takes three call times to latch out.)
	  PORTB &= ~_BV(0); // LDAC(8) -> Low
	  PORTB |= _BV(0);  // LDAC(8) -> High
	}
	inline void setDAC(uint16_t i, uint8_t option) {
	  // 1. save to regH/L.
	  // 2. clock out to SPI register.
	  // 3. latch out.
	  latchDAC();
	  // Start SPI transfer (DACA)
	  PORTB &= ~_BV(2); // CS(10) -> Low
	  // 1. transfer previous high register
	  SPDR = regH;
	  regH = (i >> 8) | option;
	  while(!(SPSR & _BV(SPIF))); // wait SPI transfer
	  // 2. transfer previous low register
	  SPDR = regL;
	  regL = i & 0xff;
	  while(!(SPSR & _BV(SPIF))); // wait SPI transfer
	  PORTB |= _BV(2); // CS_High
	}

	#define LENGTH_FLAT 100
	volatile uint16_t flat[LENGTH_FLAT];
	volatile uint8_t count;
	volatile uint16_t * p_flat;

	void updateDAC() {
	  uint16_t t;
	  latchDAC();
	  PORTB &= ~_BV(2); // CS(10) -> Low
	  SPDR = regH;
	  t = *p_flat;
	  count ++; p_flat ++;
	  regH = (t >> 8) | MCP4922_DAC_A | MCP4922_GAIN_1X | MCP4922_VREF_BUF | MCP4922_SHDN;
	  while(!(SPSR & _BV(SPIF))); // wait SPI transfer
	  SPDR = regL;
	  regL = t & 0xff;
	  if (count >= LENGTH_FLAT) {
	   count = 0;
	   p_flat = flat;
	  }
	  while(!(SPSR & _BV(SPIF))); // wait SPI transfer
	  PORTB |= _BV(2); // CS_High  
	}

	void setTable(int khz) {
	  float _khz = khz;
	  for(int i = 0;i < LENGTH_FLAT;i ++) {
	    flat[i] = float2dac(sin(khz*2.f*M_PI*(float)i/(float)LENGTH_FLAT));
	  }
	}

	void setup() {
	  setTable(1);
	  pinMode(LDAC,OUTPUT) ;
	  SPI.begin();
	  SPI.setBitOrder(MSBFIRST);
	  SPI.setClockDivider(SPI_CLOCK_DIV2);
	  SPI.setDataMode(SPI_MODE0);
	  Serial.begin(9600);
	  Timer1.initialize();
	  p_flat = flat;
	}

	inline uint16_t float2dac(float v) {
	  return (uint16_t)(1000.f*(v*2.+2.08f));
	}

	#define MAX_CMD_STRING 16

	void loop() {
	  uint8_t cmdString[MAX_CMD_STRING]; uint8_t cmdC = 0;
	  uint16_t cf_param = 1;
	  TIMSK0 &= ~_BV(TOIE0); // disable Timer0 overflow interrupt
	  TIMSK2 &= ~_BV(TOIE2); // disable Timer2 overflow interrupt
	  // noInterrupts();
	  Timer1.attachInterrupt(updateDAC, 10); // background
	  // tiny command line interpreter
	  while (1) {
	    if (Serial.available() > 0) {
	      cmdString[cmdC] = Serial.read();
	      if (!(cmdString[cmdC] == 0x0d||cmdString[cmdC] == 0x0a)) {
	        cmdC ++;
	        if (MAX_CMD_STRING == cmdC) { // reject over maximum length
	          Serial.print("\r\n> ");
	          cmdC = 0;
	          continue;
	        }
	        Serial.print((char)cmdString[cmdC-1]); // echo back only
	        continue;
	      }
	    }
	  }
	}
      

ArduFgxでは、もう少し変えて、メインの割り込み部分は下記の様なコードになっている。DACの設定に必要なビットはテーブル作成時に予め立てておく方が早い。

	// Three calls are needed to latch out.
	//  1. save to regH/L.
	//  2. clock out to SPI register.
	//  3. latch out with timer output trigger (LDAC).
	void updateDAC_cont() {
	  uint8_t regL, count_copy; uint16_t smpl;
	  PORTB |= _BV(2);       // end SPI transfer : CS(10) -> High
	  PORTB &= ~_BV(2);      // start SPI transfer: CS(10) -> Low
	  SPDR = prev_High;      // >- SPI write (1) MSB
	  regL = prev_Low;
	  smpl = *p_flat;        // load table (do heavy task until the SPI transfer ends)
	  prev_High = smpl >> 8;
	  prev_Low  = smpl & 0xff;
	  count_copy = count;
	  count_copy --;
	  if (count_copy == 0) { count = LENGTH_FLAT, p_flat = g_flat; }
	  else { p_flat ++; count = count_copy; }
	  while (!(SPSR & _BV(SPIF))); // <- wait SPI transfer
	  SPDR = regL;     // >- SPI write (2) LSB
	  // while (!(SPSR & _BV(SPIF))); // <- wait SPI transfer
	  return;
	}
      
Let's start by acknowledging one external factor that does affect the
popularity of a programming language. To become popular, a programming
language has to be the scripting language of a popular system. Fortran and
Cobol were the scripting languages of early IBM mainframes. C was the
scripting language of Unix, and so, later, was Perl. Tcl is the scripting
language of Tk. Java and Javascript are intended to be the scripting languages
of web browsers.

Lisp is not a massively popular language because it is not the scripting
language of a massively popular system. What popularity it retains dates back
to the 1960s and 1970s, when it was the scripting language of MIT. A lot of
the great programmers of the day were associated with MIT at some point. And
in the early 1970s, before C, MIT's dialect of Lisp, called MacLisp, was one
of the only programming languages a serious hacker would want to use.

    -- Paul Graham
    -- Being Popular ( http://www.paulgraham.com/popular.html )

  <yacoob>  a quickie: are 'adjustments layers' planned to be implemented
            in gimp?
   <nomis>  yacoob: at some point in the future, yes.
  <yacoob>  nomis: dare to estimate how far this future is?
   <nomis>  yacoob: no.
 <rindolf>  yacoob: faster if you contribute.
         *  nomis waits for the "oh, I cannot program at all".
 <rindolf>  nomis: faster if he learns how to program, and then
            contributes.
   <nomis>  :)
  <yacoob>  rindolf: you wouldn't like me to contribute, believe me ;)

    -- Faster, faster!
    -- #gimp, GimpNet


Powered by UNIX fortune(6)
[ Main Page ]