Bagian 2. Memulai โ
Perpustakaan Generator Kode Assembler untuk Mikrokontroler AVR
Bagian 1. Kenalan pertama
Selamat siang, Khabrovit sayang. Saya ingin menyampaikan kepada Anda proyek berikutnya (dari banyak sekali yang tersedia) untuk pemrograman mikrokontroler populer dari seri AVR.
Adalah mungkin untuk menghabiskan banyak teks untuk menjelaskan mengapa ini diperlukan, tetapi sebaliknya, lihat saja contoh bagaimana hal itu berbeda dari solusi lain. Dan semua penjelasan dan perbandingan dengan sistem pemrograman yang ada akan, sebagaimana diperlukan, dalam proses parsing contoh. Perpustakaan saat ini sedang dalam proses penyelesaian, sehingga implementasi beberapa fungsi mungkin tidak terlihat optimal. Juga, beberapa tugas yang ditugaskan untuk programmer dalam versi ini seharusnya lebih dioptimalkan atau diotomatisasi.
Jadi mari kita mulai. Saya ingin segera mengklarifikasi bahwa materi yang disajikan tidak boleh dianggap sebagai deskripsi yang lengkap, tetapi hanya sebagai demonstrasi dari beberapa fitur perpustakaan yang dikembangkan untuk membantu memahami betapa menariknya pendekatan ini bagi pembaca.
Kami tidak akan menyimpang dari praktik yang berlaku dan mulai dengan contoh klasik, semacam "Halo dunia" untuk mikrokontroler. Yaitu, kami mengedipkan LED yang terhubung ke salah satu kaki prosesor. Mari kita buka VisualStudio dari Microsoft (rilis apa pun akan dilakukan) dan buat aplikasi konsol untuk C #. Bagi mereka yang tidak tahu, Edisi Komunitas, cukup untuk bekerja, benar-benar gratis.
Sebenarnya teks itu sendiri adalah sebagai berikut:
Contoh Kode Sumber 1using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].Toggle();}); Console.WriteLine(AVRASM.Text(m)); } } }
Tentu saja, agar semuanya berfungsi dan Anda membutuhkan perpustakaan yang saya wakili.
Setelah mengkompilasi dan menjalankan program, pada output konsol kita akan melihat hasil berikut dari program ini.
Hasil Kompilasi Contoh 1 #include โcommon.incโ RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 L0000: in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL xjmp L0000 .DSEG
Jika Anda menyalin hasilnya ke lingkungan apa pun yang tahu cara bekerja dengan assembler AVR dan menghubungkan perpustakaan makro Common.inc ( perpustakaan makro juga merupakan salah satu komponen sistem pemrograman yang disajikan dan bekerja bersama dengan NanoRTOSLib ), maka program ini dapat dikompilasi dan diperiksa pada emulator atau chip nyata dan pastikan semuanya bekerja.
Pertimbangkan kode sumber program lebih terinci. Pertama-tama, kami menetapkan variabel tipe kristal yang digunakan. Selanjutnya, atur mode keluaran digital untuk bit nol port B kristal dan aktifkan port. Baris berikutnya terlihat sedikit aneh, tetapi artinya cukup sederhana. Di dalamnya, kita mengatakan bahwa kita ingin mengatur infinite loop, di mana kita mengubah nilai bit nol dari port B ke kebalikannya. Baris terakhir dari program ini benar-benar memvisualisasikan hasil dari semua yang sebelumnya ditulis dalam bentuk kode assembler. Semuanya sangat sederhana dan kompak. Dan hasilnya praktis tidak berbeda dari apa yang bisa ditulis di assembler. Hanya ada dua pertanyaan pada kode keluaran: yang pertama - mengapa menginisialisasi tumpukan jika kita masih tidak menggunakannya, dan xjmp seperti apa? Jawaban untuk pertanyaan pertama dan sekaligus penjelasan mengapa assembler ditampilkan, daripada HEX yang sudah selesai, adalah sebagai berikut: hasil dalam bentuk assembler memungkinkan Anda untuk menganalisis dan mengoptimalkan program lebih lanjut, memungkinkan programmer untuk memilih dan memodifikasi fragmen kode yang tidak disukainya. Dan inisialisasi tumpukan ditinggalkan setidaknya karena alasan-alasan itu tanpa menggunakan tumpukan Anda dapat datang dengan tidak banyak program. Namun, jika Anda tidak menyukainya, jangan ragu untuk membersihkannya. Output ke assembler untuk tujuan ini dimaksudkan. Adapun xjmp , ini adalah contoh menggunakan makro untuk meningkatkan keterbacaan dari assembler output. Secara khusus, xjmp adalah pengganti jmp dan rjmp dengan substitusi yang benar tergantung pada panjang transisi.
Jika Anda mengisi program dengan sebuah chip, maka tentu saja kita tidak akan melihat berkedip dioda, terlepas dari kenyataan bahwa keadaan pin berubah. Itu terjadi terlalu cepat untuk melihatnya melalui mata. Oleh karena itu, kami mempertimbangkan program berikut, di mana kami terus berkedip dengan dioda, tetapi agar dapat dilihat. Sebagai contoh, penundaan 0,5 detik cukup cocok: tidak terlalu cepat dan tidak terlalu lambat. Dimungkinkan untuk membuat banyak loop bersarang dengan NOP untuk membentuk penundaan, tetapi kami akan melewatkan langkah ini karena tidak menambahkan apa pun pada deskripsi kapabilitas perpustakaan dan segera memanfaatkan peluang untuk menggunakan perangkat keras yang tersedia. Kami mengubah aplikasi kami sebagai berikut.
Contoh Kode Sumber 2 using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.WDT.Clock = eWDTClock.WDT500ms; m.WDT.OnTimeout = () => m.PortB[0].Toggle(); m.WDT.Activate(); m.EnableInterrupt(); var loop = AVRASM.newLabel(); m.GO(loop); Console.WriteLine(AVRASM.Text(m)); } } }
Tentunya, program ini mirip dengan yang sebelumnya, jadi kami hanya akan mempertimbangkan apa yang telah berubah. Pertama, dalam contoh ini, kami menggunakan WDT (pengawas waktu). Untuk bekerja dengan penundaan besar yang tidak membutuhkan keakuratan ekstrem, ini adalah pilihan terbaik. Yang diperlukan untuk menggunakannya adalah mengatur frekuensi yang diperlukan dengan mengatur pembagi melalui properti WDT.Clock dan menentukan tindakan yang harus dilakukan pada saat acara dipicu, dengan menetapkan kode melalui properti WDT.OnTimeout. Karena kita perlu interupsi untuk berfungsi, mereka harus diaktifkan dengan perintah EnableInterrupt. Namun siklus utama bisa diganti oleh boneka. Di dalamnya, kami masih tidak berencana untuk melakukan apa pun. Oleh karena itu, kami akan mendeklarasikan dan menetapkan label dan membuat transisi tanpa syarat untuk mengatur siklus kosong. Jika Anda lebih suka LOOP - silakan. Hasil ini tidak akan berubah.
Nah, di final, mari kita lihat kode yang dihasilkan.
Hasil Kompilasi Contoh 2 #include โcommon.incโ jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop jmp WDT ;Watchdog Timer Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 ldi TempL, (1<<WDCE) | (1<<WDE) sts WDTCSR,TempL ldi TempL, 0x42 sts WDTCSR,TempL sei L0000: xjmp L0000 WDT: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG
Mereka yang terbiasa dengan prosesor ini tidak diragukan lagi akan memiliki pertanyaan di mana beberapa vektor interupsi telah hilang. Di sini kami menggunakan logika berikut - jika kode tidak digunakan - kode tidak diperlukan. Oleh karena itu, tabel interupsi berakhir pada vektor yang terakhir digunakan.
Terlepas dari kenyataan bahwa program mengatasi tugas dengan sempurna, yang paling pilih-pilih mungkin tidak menyukai kenyataan bahwa serangkaian kemungkinan penundaan terbatas, dan langkahnya terlalu kasar. Oleh karena itu, kami akan mempertimbangkan cara lain, dan pada saat yang sama, kami akan melihat bagaimana kerja dengan pengatur waktu diatur di perpustakaan. Dalam kristal Mega328, yang diambil sebagai sampel, ada sebanyak 3 di antaranya. 2 8-bit dan satu 16-bit. Arsitek berusaha sangat keras untuk berinvestasi sebanyak mungkin fitur dalam timer ini, oleh karena itu pengaturan mereka cukup banyak.
Pertama, kami menghitung penghitung mana yang harus digunakan untuk keterlambatan 0,5 detik. Jika kita mengambil frekuensi jam kristal 16 MHz, bahkan dengan pembagi periferal maksimum tidak mungkin untuk tetap berada dalam penghitung 8-bit. Oleh karena itu, kami tidak akan mempersulit dan menggunakan penghitung Timer1 16-bit yang hanya tersedia untuk kami.
Sebagai hasilnya, program ini mengambil bentuk berikut:
Contoh Kode Sumber 3 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) {var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; m.PortB.Activate(); m.Timer1.Mode = eWaveFormMode.CTC_OCRA; m.Timer1.Clock = eTimerClockSource.CLK256; m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); m.Timer1.OnCompareA = () => bit1.Toggle(); m.Timer1.Activate(); m.EnableInterrupt(); m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); Console.WriteLine(AVRASM.Text(m)); } } }
Karena kami menggunakan generator utama sebagai sumber jam untuk penghitung waktu kami, untuk perhitungan penundaan yang benar, Anda harus menentukan frekuensi jam prosesor, pengaturan pembagi, dan sekering jam periferal. Teks utama dari program ini adalah mengatur timer ke mode yang diinginkan. Di sini, seorang musyawarah 256 dan tidak maksimal secara sengaja dipilih untuk pencatatan jam kerja, karena ketika Anda memilih pembagi 1024 untuk frekuensi jam yang diperlukan 500ms, yang ingin kami dapatkan, diperoleh angka pecahan.
Kode assembler yang dihasilkan dari program kami akan terlihat seperti ini:
Hasil Kompilasi Contoh 3 #include โcommon.incโ jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop reti ;Watchdog Timer Handler nop reti ;Timer2 Compare A Handler nop reti ;Timer2 Compare B Handler nop reti ;Timer2 Overflow Handler nop reti ;Timer1 Capture Handler nop jmp TIM1_COMPA ;Timer1 Compare A Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 outiw OCR1A,0x7A12 outi TCCR1A,0 outi TCCR1B,0xC outi TCCR1C,0x0 outi TIMSK1,0x2 outi DDRB,0x1 sei L0000: xjmp L0000 TIM1_COMPA: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG
Tampaknya tidak ada lagi yang perlu dikomentari. Kami menginisialisasi perangkat, mengkonfigurasi interupsi dan menikmati program.
Bekerja melalui interupsi adalah cara termudah untuk membuat program untuk bekerja secara real time. Sayangnya, beralih antara tugas paralel menggunakan hanya penangan interupsi untuk melakukan tugas ini tidak selalu mungkin. Pembatasannya adalah larangan penanganan interupsi bersarang, yang mengarah pada fakta bahwa sampai prosesor keluar, prosesor tidak merespons semua gangguan lain, yang dapat menyebabkan hilangnya kejadian jika prosesor berjalan terlalu lama.
Solusinya adalah dengan memisahkan kode registrasi acara dan pemrosesannya. Inti pemrosesan multi-utas paralel dari perpustakaan diatur sedemikian rupa sehingga ketika suatu peristiwa terjadi, penangan interupsi hanya mendaftarkan peristiwa yang diberikan dan, jika perlu, melakukan operasi pengambilan data minimum yang diperlukan, dan semua pemrosesan dilakukan dalam aliran utama. Kernel secara berurutan memeriksa keberadaan flag yang tidak diproses dan, jika ditemukan, melanjutkan ke tugas yang sesuai.
Menggunakan pendekatan ini menyederhanakan desain sistem dengan beberapa tugas tidak sinkron, memungkinkan Anda untuk mempertimbangkan masing-masing secara terpisah, tanpa berfokus pada masalah peralihan sumber daya di antara tugas. Sebagai contoh, perhatikan implementasi dua tugas independen, yang masing-masing mengalihkan outputnya dengan penundaan tertentu.
Contoh Kode Sumber 4 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 4); tasks.Heap = new StaticHeap(tasks, 64); var t1 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit1.Toggle(); tsk.Delay(32); tsk.TaskContinue(loop); },"Task1"); var t2 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit2.Toggle(); tsk.Delay(48); tsk.TaskContinue(loop); }, "Task2"); var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1); tasks.ActivateNext(ca, tasks.AlwaysOn, t2); ca.Dispose(); m.EnableInterrupt(); tasks.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }
Dalam tugas ini, kami mengkonfigurasi nol dan output pertama port B ke output dan mengubah nilai dari 0 ke 1 dan sebaliknya dengan periode 32 ms untuk nol dan 48 ms untuk output pertama. Tugas terpisah bertanggung jawab untuk mengelola setiap port. Hal pertama yang perlu diperhatikan adalah definisi instance Parallel. Kelas ini adalah inti dari manajemen tugas. Dalam konstruktornya, kami menentukan jumlah maksimum utas yang berjalan secara simultan. Berikut ini adalah alokasi memori untuk menyimpan aliran data. Kelas StaticHeap yang digunakan dalam contoh mengalokasikan sejumlah byte untuk setiap aliran. Untuk mengatasi masalah kami, ini dapat diterima, dan penggunaan alokasi memori tetap dibandingkan dengan dinamis menyederhanakan algoritma dan membuat kode lebih kompak dan lebih cepat. Lebih lanjut dalam kode, kami menguraikan serangkaian tugas yang dirancang untuk dijalankan di bawah kendali kernel. Anda harus memperhatikan fungsi Tunda asinkron, yang kami gunakan untuk membentuk penundaan. Keunikannya adalah ketika fungsi ini dipanggil, penundaan yang diperlukan diatur dalam pengaturan aliran, dan kontrol ditransfer ke kernel. Setelah interval yang ditetapkan berlalu, kernel mengembalikan kontrol ke tugas dari perintah yang mengikuti perintah Delay. Fitur lain dari tugas adalah pemrograman perilaku aliran tugas setelah selesai dalam perintah tugas terakhir. Dalam kasus kami, kedua tugas dikonfigurasikan untuk dieksekusi dalam loop tak terbatas dengan kontrol kembali ke kernel pada akhir setiap siklus. Jika perlu, menyelesaikan tugas dapat membebaskan utas atau meneruskannya untuk melakukan tugas lain.
Alasan untuk memohon tugas adalah untuk mengaktifkan sinyal yang ditugaskan untuk aliran tugas. Sinyal dapat diaktifkan baik secara terprogram dan perangkat keras dengan menyela dari perangkat periferal. Panggilan tugas mengatur ulang sinyal. Pengecualian adalah sinyal yang selalu ditentukan sebelumnya, yang selalu dalam keadaan aktif. Hal ini memungkinkan untuk membuat tugas yang akan menerima kendali di setiap siklus pemungutan suara. Fungsi LOOP diperlukan untuk menjalankan loop eksekusi utama. Sayangnya, ukuran kode keluaran saat menggunakan Paralel sudah menjadi jauh lebih besar daripada contoh sebelumnya (sekitar 600 perintah) dan tidak dapat sepenuhnya dikutip dalam artikel.
Dan untuk manis - sesuatu yang lebih seperti proyek langsung, yaitu termometer digital. Semuanya selalu sederhana. Sensor digital dengan antarmuka SPI, indikator 7-segmen 4-digit dan beberapa utas pemrosesan untuk menjaga semuanya tetap dingin. Dalam satu, kami menggerakkan siklus untuk indikasi dinamis, di lain, peristiwa yang memicu siklus membaca suhu, di ketiga kami membaca nilai yang diterima dari sensor dan mengubahnya dari kode biner ke BCD dan kemudian menjadi kode segmen untuk buffer indikasi dinamis.
Program itu sendiri adalah sebagai berikut.
Contoh Kode Sumber 5 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var led7s = new Led_7(); led7s.SegPort = m.PortC; led7s.Activate(); m.PortD.Direction(0xFF); m.PortD.Activate(); m.PortB[0].Mode = ePinMode.OUT; var tc77 = new TC77(); tc77.CS = m.PortB[0]; tc77.Port = m.SPI; m.Timer0.Clock = eTimerClockSource.CLK64; m.Timer0.Mode = eWaveFormMode.Normal; var reader = m.DREG("Temperature"); var bcdRes = m.DREG("digits"); var tmp = m.BYTE(); var bcd = new BCD(reader, bcdRes); m.subroutines.Add(bcd); var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 64); var tmrSig = os.AddSignal(m.Timer0.OVF_Handler); var spiSig = os.AddSignal(m.SPI.Handler, () => { m.SPI.Read(m.TempL); m.TempL.MStore(tmp); }); var actuator = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureAsync(); tsk.Delay(16); tsk.TaskContinue(loop); }, "actuator"); var treader = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureCallback(os, reader, tmp); reader >>= 7; m.CALL(bcd); tsk.TaskContinue(loop); }, "reader"); var display = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); m.PortD.Write(0xFE); m.TempQL.Load(bcdRes.Low); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFD); m.TempQL.Load(bcdRes.Low); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFB); m.TempQL.Load(bcdRes.High); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xF7); m.TempQL.Load(bcdRes.High); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); tsk.TaskContinue(loop); }, "display"); var ct = os.ContinuousActivate(os.AlwaysOn, actuator); os.ActivateNext(ct, spiSig, treader); os.ActivateNext(ct, tmrSig, display); tc77.Activate(); m.Timer0.Activate(); m.EnableInterrupt(); os.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }
Jelas bahwa ini bukan konsep kerja, tetapi hanya demo teknologi yang dirancang untuk menunjukkan kemampuan perpustakaan NanoRTOS. Tetapi bagaimanapun juga, kurang dari 100 baris sumber dan kurang dari 1kb kode keluaran adalah hasil yang cukup baik untuk aplikasi yang bisa diterapkan.
Dalam artikel-artikel berikut, saya berencana, jika berminat pada proyek ini, untuk membahas lebih rinci tentang prinsip-prinsip dan fitur pemrograman menggunakan perpustakaan ini.