[ 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;
	}
      
Rule of Open-Source Programming #5:

A project is never finished.

    -- Shlomi Fish
    -- "Rules of Open Source Programming"

 <Quetzalcoatl_>  How do I write a computer vision program in C on a
                  microcontroller?
           <dyf>  Quetzalcoatl_: with a text editor?
 <Quetzalcoatl_>  Hmm.. Never thought of that. But which editor? Is
                  Notepad good enough?
         <mauke>  no, you need at least Wordpad
       <rindolf>  mauke: I suggest MS Word or at least OpenOffice.org
       <rindolf>  mauke: but in order to really be able to write well, you
                  need a desktop publishing program like Scribus or Adobe
                  FrameMaker.
               *  rindolf wonders which compiler will accept PDFs as
                  input.
       <waiting>  rindolf: /usr/bin/pdftotext
       <rindolf>  waiting: and pray.
       <rindolf>  There's an estoric programming language called Piet (I
                  think) that accepts images as input.

    -- How to write stylistic code
    -- ##programming, Freenode


Powered by UNIX fortune(6)
[ Main Page ]