EFM8 ‘in üstündeki DAC biriminden bahsetmeye başlamadan önce, digital-to-analog converter denen şeyler hakkında uzun süre devam eden bir kanaatimi not etmek isterim: DAC bana hep lüks bir şey gibi gelmiştir. ADC bir şekilde en başından beri erişilebilirliği olan bir birimdi. Ama DAC, ister bir mcu’da isterse de bir plc’de olsun, sanki hep maliyeti arttıran bir şey gibi geldi bana.
15 sene kadar önce bir iplik boyama makinesinde, boya desenleri oluşturmak için boya püskürten kafaların dönme hızlarını akan ipliğin uzunluğuna bağlı olarak değiştirilebilir yapmam istenmişti. Bulduğum çözüm küçük bir işlemciyle encoder darbeleri saymak ve 8 bitlik bir DAC’a çıkış vererek asenkron motor sürücülerinin hızlarını değiştirmekti. O zaman National’ın paralel bir DAC’ını kullanmıştım, çıkışa bir de opamp koyunca sonuç muhteşem olmuştu. DAC deyince aklıma hep bu iş gelir.
Yıllar sonra da ilginç bir proje için dalga şekli üretme işiyle uğraşmak zorunda kaldım. Artık işlemcilerin üzerinde oldukça hızlı DAC’lar var. Ayrıca çok yüksek çözünürlüklü PWM’lerin olması zaten pek çok durumda DAC ihtiyacını tamamen ortadan kaldırıyor. Ama ben yine de üzerinde DAC birimi olan mcu’lara ayrı bir gözle bakıyorum.
EFM8’de 4 kanal DAC var. Bunlar iki çift olarak kullanılıyor gibi gösterilse de tamamen bağımsız çıkışlar olarak kullanabiliyorsunuz. Bir çifti, birbirinin aynı ya da eşlenik çıkış üretmek istersek beraber kullanabiliriz. DAC çözünürlüğü 12 bit.

Anlatacağım örnek uygulamada DAC çıkışını ses üretmede kullanıyorum. Bunun için 2 DAC kanalını toplam şeklinde kullanmak için bir devre hazırladım. Bu şekilde basit dalgalardan daha çok harmonik üretmek mümkün.

Bir stereo kuvvetlendirici kullanıyor olsam da mono ses üretiyorum. Kuvvetlendiricinin yüksek giriş direncinin karşısında iki çıkışı dirençlerle birbirine bağlamak bir toplama kuvvetlendiricisi elde etmemizi sağlıyor. Benim kullandığım amplifikatörün bir bias off girişi de var. Bununla yükleme/geçiş vb. anlarda pop/click türü gürültüler olmasın diye çıkışı kapatabiliyorum.
Referans gerilimini, referans gerilim bölücüsünü (REFGN) ve çıkış sürücüsü kazancını (DRVGN) ayarlayarak çeşitli çıkış aralıkları elde edebiliriz. Ben bunlardan 4 tanesini kullanıcı konfigürasyonu ile seçilebilir yaptım:

Bir dalga üretmek iki eksende hareket etmemizi gerektirir: Y ekseni tahmin etmesi kolay olacağı üzere
V = Vref * REFGN * DRVGN * (DAC0H:DAC0L) / 4095
çarpanlarıyla belirlenir.
X ekseni ise zaman. Dalga şeklimizin çözünürlüğüne bağlı olarak, istediğimiz frekansı bir tam periyotta üretecek bir hızla DAC güncellemesi yapmamızı gerektirir.
Daha açık bir deyişle, bir dalga şeklini 12 örnekle tanımlamışsak DAC güncelleme frekansımız istenen dalga frekansının 12 katı kadar olmalı. Örneğin;

Yandaki dalga şeklini tanımlayan “normalize” sinüs dizisi;
32,48,60,64,60,48,32,16,4,0,4,16
Bu diziyi, DAC’a 208us periyotla yüklersek yaklaşık 2,5ms periyotlu bir sinüs çıkışı alırız. Bu da 400Hz kadar eder. Ses çıkışını test etmek için bunu deneyebiliriz.
Burada ilginç olan iki nokta var: Birincisi sample genlikleri unipolar. Çünkü DAC çıkışımız 0V ile seçtiğimiz pozitif Vref gerilimi arasında bir gerilim üretebilir. O yüzden, genliği simetrik bir işaret üretmek istiyorsak Vref/2 kadarlık bir offset eklememiz gerekir.
İkinci nokta, DAC sayısal değerimizle ilgili. Ben bir dalga şekli tanımlarken onu 0..64 gibi bir aralıkta normalize ederek üretiyorum. Eğer donanımsal değil, yazılımsal genlik ayarı yapmak isterseniz bu 0..64 aralığını 0dB kabul edip, sample değerlerini istediğiniz bir sabitle çarpıp, anlık bir çıkış genliği üretirsiniz.

Eğer bu bir ses işareti ise ters-logaritmik bir adım bölümlemesi volume ayarı için uygun olacaktır. 4095 verilebilecek en büyük çıkış değeridir. DAC çıkışını yüklüyorsanız ve 3V gibi bir range ayarı yaptıysanız çıkışın max. değerden önce doyuma ulaşması tehlikesi vardır.
Aşağıda, genel amaçlı kullanıma uygun bir dalga tablosu tanımı paylaşıyorum. Burada her bir dalga tanımı 24 byte’tan oluşuyor (her sample little endian yerleşmiş 12 word). Bunu denemek isterseniz dikkatinizi çekmek istediğim bir şey var: İlk 4 dalga haricindeki dalga tanımları kendisini 3 kez tekrar ediyor. Yani bunların frekansını hesaplarken x12 değil x4 hızla yükleme yapmalısınız.
code quailfier ile dizinin flash bellekte yerleşmesini sağlıyorum ancak mutlak adres veremiyorum. _at_ directive kullandığınızda değişkenleri başlatamazsınız.
Bu yüzden de, bu işleri denerken en başta yaptığım gibi, çalma kesmesi içinde doğrudan bir code pointer’ı çalıştıramıyorum. Bunun yerine waveTable dizi indeksini volatile bir değişken olarak kullanıp compiler’ın verimliliğine güveniyoruz.
unsigned char code waveTable[288] =
{
// wav-1 : sine +18dB
0x00,0x01,0x80,0x01,0xE0,0x01,0xF8,0x01,
0xE0,0x01,0x80,0x01,0x00,0x01,0x80,0x00,
0x20,0x00,0x00,0x00,0x20,0x00,0x80,0x00,
// wav-2 : sine +28dB
0x00,0x03,0x80,0x04,0xA0,0x05,0xE8,0x05,
0xA0,0x05,0x80,0x04,0x00,0x03,0x80,0x01,
0x60,0x00,0x00,0x00,0x60,0x00,0x80,0x01,
// wav-3 : sine +32dB
0x40,0x05,0xE0,0x07,0xD8,0x09,0x56,0x0A,
0xD8,0x09,0xE0,0x07,0x40,0x05,0xA0,0x02,
0xA8,0x00,0x00,0x00,0xA8,0x00,0xA0,0x02,
// wav-4: sine +36dB
0x80,0x07,0x0B,0x40,0x10,0x0E,0xC4,0x0E,
0x10,0x0E,0x40,0x0B,0x80,0x07,0xC0,0x03,
0xF0,0x00,0x00,0x00,0xF0,0x00,0xC0,0x03,
// wav-5: symm.sawtooth +12dB
0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
0xC0,0x00,0xFC,0x00,0x00,0x00,0x40,0x00,
// wav-6 symm.sawtooth +24dB
0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
0x00,0x03,0xF0,0x03,0x00,0x00,0x00,0x01,
// wav-7: symm.sawtooth +32dB
0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
0xE0,0x07,0x56,0x0A,0x00,0x00,0xA0,0x02,
// wav-8: symm.sawtooth +36dB
0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
0x00,0x0C,0xC0,0x0F,0x00,0x00,0x00,0x04,
// wav-9: sq.wave: +12dB
0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
0xFC,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,
// wav-10: sq.wave: +24dB
0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
0xF0,0x03,0xF0,0x03,0x00,0x00,0x00,0x00,
// wav-11: sq.wave: +32dB
0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
0x56,0x0A,0x56,0x0A,0x00,0x00,0x00,0x00,
// wav-12: sq.wave: +36dB
0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00,
0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00,
0xC0,0x0F,0xC0,0x0F,0x00,0x00,0x00,0x00
};
DAC güncelleme “frekans” ayarı konusuna geri dönelim: Ben bu iş için Timer-2 kesmesini kullanıyorum. TMR2’ye vereceğimiz bir reload değeri ile DAC güncelleme hızını ayarlarız, böylece dalga üretecimizin X ekseni hassas biçimde ayarlanmış olur.
CKCON0 |= 0x30; // TMR2 clock source -> SYSCLK
TMR2CN0 = 0; //
// çalıştırmak:
TMR2CN0_TR2 = 1; // bu register bit addressable
// çalma frekansını değiştirmek:
TMR2RLL = rld_L;
TMR2RLH = rld_H;
TDAC (DAC Erişim Periyodu) = istenen işaret periyodu / N
N : Bir periyodun örnekleme sayısı (yukarıdaki örnekte bu 12)
T2 : (TMR2 tick period) = 1/49M
Reload Değeri = 65536 – (TDAC / T2)
Bir çalma ayarında istenen frekans değerini değil, TMR2 reload değerini saklamayı tercih ediyorum.
TMR2 kesmesinde yapılacak iş “sıradaki” sample’ı DAC’a yazmak. Oynatılacak (veya çalınacak diyelim) dalga örneklerini flash bellekte saklıyorum. Bu noktada iki farklı yaklaşım geliştirdim:
Birinci yöntemde, çalma ayarları yüklenirken (mesela bir sequencer’ın belli bir adımının zamanı geldiğinde) flash’tan, sequencer’ın belirttiği dalga setini okuyorum ve yukarıda anlattığım şekilde düzey ölçeklemesi yapıp (normalize sample’ların volume ayar değeriyle çarpılması) ram’de bir çalma tablosu oluşturuyorum (hatta iki tablo oluşturup biri çalarken diğerini yükleme ve geçişleri hızlı yapma gibi bir şey denedim ama 49MHz çalışmada bu çok kritik bir hızlandırma değil).
Çalınacak sesin genliği ya da dalga şekli değişinceye dek çalma tablosu artık değişmiyor.
İkinci yaklaşım daha sade: Çalınacak sample’ları flash’ta normalize halleriyle değil, ölçeklenmiş halleriyle saklıyorum ve çalma sırasında doğrudan flash’tan okuma yapıyorum. Okunacak dalga tablosu indeksini sequencer yüklemesinde bir rom pointer’ında belirliyorum ve sonra kesme kodunda doğrudan pointer’ı çalıştırıyorum. Bu yaklaşımda TMR2 kesme kodu şöyle:
// Play Kesmesi:
void ISR_TMR2 (void) interrupt 5
{
TMR2CN0_TF2H = 0;
// playBuffer ba$a sarmasi gerekiyor mu?:
if ( smpIndex > 11 )
{
smpIndex = 0;
playIndex = wavIndex; // çalma indeksini de ba$a sar.
// durdurma istegi verilmi$ mi?
if (STOP_REQ)
{
TMR2CN0_TR2 = 0; // artik player kesmesi tetiklenmeyecek..
STOP_REQ = 0;
return;
}
}
///// DAC Update:
SFRPAGE = 0x30;
DAC0L = waveTable[playIndex]; // yukarida anlattigim mevzu!!
++playIndex;
DAC0H = waveTable[playIndex];
SFRPAGE = 0;
++playIndex;
++smpIndex;
}
playIndex waveTable’ın sıradaki elemanına erişmek için kullandığımız dizi indeksi. Bunu, çalma adımını yüklediğimizde (ne çalacağımızı bize söyleyen veri) öğreniyoruz ve indeksi başlatıyoruz. Bunun yerine, muhtemelen daha efektif bir kesme kodu için
unsigned char code * volatile data playPtr;
şeklinde bir işaretçi tanımı yapıp bunu çalınacak rom sample’ları boyunca da koşturabilirdik.
Burada, özellikle dikkat edilecek bir nokta var: Kesme kodu içinde SFRPAGE değiştiriliyor. Interrupt handler yedeklemeyi hallediyor mu halletmiyor mu hiç endişe etmemek için, bu kesmeyi başka bir kesmenin kesmesi ihtimalini ortadan kaldıralım. (Böyle cümleleri şık hale getirmeye çalışmak işi sulandırmak olur, sonuçta anlatım bozukluğu yok).
Daha açık konuşursam, bu işlemci bir de UART kesmesi çalıştırıyor (native priority’si daha yüksek). Çipi başlatırken UART kesmesini TMR2 kesmesinden daha düşük öncelikli olacak şekilde ayarlıyorum. Çünkü gelen bir byte biraz bekleyebilir (baudrate düşük).
DAC yazması yaparken başka bir koda atlamak SFRPAGE anahtarlaması dışında da sorunlar yaratacaktır. Çünkü DAC güncelleme hızımız SYSCLK hızında. Bu arada yeri gelmişken onu da not edeyim: DAC güncelleme hızını SYSCLK yapmamızla TMR2 kesmesi içinde DAC yazması yapmamız arasında bir bağlantı yok. DAC’ın kendi güncellemesi başka bir şey. Sanırım yazılımdan daha bağımsız DAC otomasyonları yapmak istersek kullanışlı olur. Yine de, ben merak edip DAC update source ile benim DAC yazma kesmemi aynı timer ile çalıştırdım. Sonuç değişmedi. Sonuçta SYSCLK çok hızlı olsa da biz çıkışı 200kHz hızıyla güncelleyebiliriz (bunu da denedim). Bu, tıpkı ADC’de olduğu gibi, “elektriksel” bir kısıt. Kişisel görüşüm, 200k gayet yüksek bir hız!
Son olarak, bir sequencer (kullanıcı tarafından belirlenmiş dalga şekillerini verilen ayarlara göre bir diziden okumak ve art arda üretmek) yapacaksak, çalmayı anlık olarak durdurmak ve devam ettirmek için kullanabileceğimiz makroları da vereyim:
#define Play_Sequencer() SFRPAGE = 0x30; DAC0CF0=0x80; SFRPAGE=0; \
TMR2L = seqData.pt_rld_L; \
TMR2H = seqData.pt_rld_H; \
TMR2CN0_TR2=1; IE_ET2=1; \
PLAYING=1; MUTE=0; LED1=0
#define Stop_Sequencer() MUTE=1; \
TMR2CN0_TR2 = 0; IE_ET2=0; \
SFRPAGE=0x30; \
DAC0L=0; DAC0H=0; \
DAC0CF0 = 0; \
SFRPAGE=0; PLAYING=0; LED1=1
Sequencer’ın verilen adımındaki ayarlara bakarak DAC’ı konfigüre etmek de şöyle olabilir:
// cfg.<pwr> bitlerine bak:
switch ( seqData.cfg & 0x03 )
{
case 0: // 2V çiki$
REF0CN = 0x80;
DACGCF2 = 0x11;
DAC0CF1 = 0x00;
break;
case 1: // 1,2V çiki$
REF0CN = 0x40;
DACGCF2 = 0x10;
DAC0CF1 = 0x00;
break;
case 2: // 1,6V çiki$
REF0CN = 0x80;
DACGCF2 = 0x12;
DAC0CF1 = 0x00;
break;
case 3: // 2,4V çiki$
REF0CN = 0x80;
DACGCF2 = 0x10;
DAC0CF1 = 0x00;
break;
}
/// wav pointer'ini ba$latalim:
wav_address = 0x3C00 + 24 * seqData.wav;
playPtr = (unsigned char xdata *) wav_address;
smpIndex = 0;
/// çalma frekansini güncelle:
TMR2RLL = seqData.pt_rld_L;
TMR2RLH = seqData.pt_rld_H;
Yukarıdaki örnek kodda 12 sample’lık bir wav dizisi kullanılıyor ve flash bellekteki 0x3C00 sayfası bu işe ayrılmış. Kullanıcının seçtiği wav indeksi seqData.wav parametresinden okunuyor.
smpIndex, sınır denetimi yaparken 16 bit sayılarla kendimizi yormayalım diye kullanılan yardımcı bir değişkendir. Kesme koduna bakabilirsiniz.
Sonuçta, gördüğünüz gibi, DAC kullanarak bir dalga şekli üretmek, eğer çıkışta analog bir işaret kullanma ihtiyacınız varsa PWM ile aynı sonucu elde etmeye çalışmaktan daha basit bir iştir. Yukarıda bir audio çıkışı (benim aklıma waveform generator deyince ilk bu geliyor) üzerinden örnek verdim. Ancak daha önce, programlanabilir bir sabit akım kaynağı işi için de bu DAC çıkışını kullanmıştım. Çalıştığımız alan, LED parlaklığını PWM ile ayarlamamızın mümkün olmayacağı bir uygulamaydı. Bu durumda EFM8’in bir çipte 4 adet bağımsız kullanılabilen DAC çıkışlarını bir sabit akım kaynağına referans girişi yaparak basit ama doğruluğu yüksek bir çözüm üretebildik. (Bunu da burada paylaşırım bir ara)