Arduinoの良いところは、たくさんのライブラリが公開されており、ハードの面倒な仕様が隠蔽されているところだが、 DAC等を高速に駆動したい場合などはポートを直接いじる必要がある。MCP4922はMicrochipのSPI 12bit DACで、 Settling Timeが4.5µsと比較的高速で、1個数百円程度で入手可能である。ArduinoにはSPIライブラリがあるので それを使用して駆動できるが、この場合はせいぜい数10kHz程度でしか駆動できない。
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);
}
}
レジスタ直操作でも、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;
}
Even APL won't make friends with XSLT.
-- Zuu
-- XSLT Facts by Shlomi Fish and Friends ( http://www.shlomifish.org/humour/bits/facts/XSLT/ )
Make it idiot proof and someone will make
a better idiot.
-- One of Nadav Har'El's Email Signatures.