← Bagian 4. Memprogram perangkat dan menangani interupsi
Perpustakaan Generator Kode Assembler untuk Mikrokontroler AVR
Bagian 5. Merancang aplikasi multi-utas
Pada bagian sebelumnya dari artikel ini, kami menguraikan dasar-dasar pemrograman menggunakan perpustakaan. Di bagian sebelumnya, kami berkenalan dengan implementasi interupsi dan batasan yang mungkin muncul saat bekerja dengan mereka. Di bagian postingan ini, kita akan membahas salah satu opsi yang mungkin untuk memprogram proses paralel menggunakan kelas Paralel . Penggunaan kelas ini memungkinkan untuk menyederhanakan pembuatan aplikasi di mana data harus diproses di beberapa aliran program independen.
Semua sistem multitasking untuk sistem single-core mirip satu sama lain. Multithreading diimplementasikan melalui pekerjaan dispatcher, yang mengalokasikan slot waktu untuk setiap utas, dan ketika selesai, ia mengambil kendali dan memberikan kendali ke utas berikutnya. Perbedaan antara berbagai implementasi hanya pada detailnya, jadi kami akan membahas lebih detail terutama pada fitur-fitur spesifik dari implementasi ini.
Unit pelaksanaan proses di utas adalah tugas. Jumlah tugas yang tidak terbatas dapat ada dalam sistem, tetapi pada waktu tertentu hanya sejumlah tugas yang dapat diaktifkan, dibatasi oleh jumlah alur kerja di operator. Dalam implementasi ini, jumlah alur kerja ditentukan dalam konstruktor manajer dan selanjutnya tidak dapat diubah. Dalam prosesnya, utas dapat melakukan tugas atau tetap gratis. Tidak seperti solusi lain, Parallel Manager tidak berganti tugas. Agar tugas mengembalikan kontrol ke operator, perintah yang sesuai harus dimasukkan dalam kodenya. Dengan demikian, tanggung jawab untuk durasi slot waktu dalam tugas ada pada programmer, yang harus memasukkan perintah interupsi di tempat-tempat tertentu dalam kode jika tugas tersebut terlalu lama, dan juga menentukan perilaku utas setelah menyelesaikan tugas. Keuntungan dari pendekatan ini adalah bahwa programmer mengontrol titik-titik peralihan di antara tugas-tugas, yang memungkinkan Anda untuk secara signifikan mengoptimalkan kode simpan / mengembalikan ketika berpindah tugas, serta menyingkirkan sebagian besar masalah yang terkait dengan akses data yang aman untuk thread.
Untuk mengontrol pelaksanaan tugas yang sedang berjalan, kelas Sinyal khusus digunakan. Sinyal adalah variabel bit, pengaturan yang digunakan sebagai sinyal aktif untuk memulai tugas dalam aliran. Nilai sinyal dapat diatur baik secara manual atau oleh peristiwa yang terkait dengan sinyal ini.
Sinyal diatur ulang ketika tugas diaktifkan oleh operator atau dapat dilakukan secara terprogram.
Tugas dalam sistem dapat dalam status berikut:
Deactivated - state awal untuk semua tugas. Tugas tidak mengambil aliran dan kontrol pelaksanaan tidak ditransfer. Kembali ke keadaan ini untuk tugas yang diaktifkan terjadi setelah perintah selesai.
Diaktifkan - keadaan di mana tugas itu berada setelah aktivasi. Proses aktivasi mengaitkan tugas dengan utas eksekusi dan sinyal aktivasi. Manajer polling utas dan memulai tugas jika sinyal tugas diaktifkan.
Diblokir - saat tugas diaktifkan, sinyal mungkin sudah ditetapkan sebagai sinyal, yang sudah digunakan untuk mengontrol utas lainnya. Dalam hal ini, untuk menghindari ambiguitas perilaku program, tugas yang diaktifkan masuk ke keadaan terkunci. Dalam keadaan ini, tugas menempati utas, tetapi tidak dapat menerima kontrol, bahkan jika sinyalnya diaktifkan. Setelah menyelesaikan tugas atau ketika mengubah sinyal aktivasi, operator memeriksa dan mengubah status tugas di utas. Jika utas telah memblokir tugas yang sinyalnya cocok dengan yang dilepaskan, yang pertama ditemukan diaktifkan. Jika perlu, pemrogram dapat mengunci dan membuka kunci tugas secara independen, berdasarkan pada logika program yang diperlukan.
Menunggu - status tugas setelah menjalankan perintah Delay . Dalam keadaan ini, tugas tidak menerima kontrol sampai interval yang diperlukan telah berlalu. Di kelas Paralel , interupsi WDT 16 ms digunakan untuk mengontrol penundaan, yang memungkinkan untuk tidak menggunakan timer untuk kebutuhan sistem. Jika Anda membutuhkan lebih banyak stabilitas atau resolusi dalam interval kecil, alih-alih Tunda, Anda dapat menggunakan aktivasi dengan sinyal timer. Harus diingat bahwa keakuratan penundaan masih rendah dan akan berfluktuasi dalam kisaran "waktu respons operator" - "durasi slot waktu maksimum dalam sistem + waktu respons operator" . Untuk tugas dengan rentang waktu yang tepat, mode hybrid harus digunakan, di mana timer tidak digunakan di kelas Paralel bekerja secara independen dari aliran tugas dan memproses interval dalam mode interrupt murni.
Setiap tugas yang dijalankan dalam utas adalah proses yang terisolasi. Ini memerlukan definisi dari dua jenis data: data lokal dari suatu aliran, yang harus terlihat dan diubah hanya dalam kerangka aliran ini, dan data global untuk pertukaran antara aliran dan akses ke sumber daya bersama. Dalam kerangka implementasi ini, data global dibuat oleh perintah yang sebelumnya dianggap di tingkat perangkat. Untuk membuat variabel tugas lokal, mereka harus dibuat menggunakan metode dari kelas tugas. Perilaku variabel tugas lokal adalah sebagai berikut: ketika tugas terputus sebelum mentransfer kontrol ke operator, semua variabel register lokal disimpan dalam memori stream. Ketika kontrol dikembalikan, variabel register lokal dikembalikan sebelum perintah berikutnya dijalankan.
Kelas dengan antarmuka IHeap yang terkait dengan properti Heap dari kelas Paralel bertanggung jawab untuk menyimpan data lokal dari aliran. Implementasi paling sederhana dari kelas ini adalah StaticHeap , yang mengimplementasikan alokasi statis dari blok memori yang sama untuk setiap utas. Jika tugas memiliki penyebaran besar sesuai dengan permintaan untuk jumlah data lokal, Anda dapat menggunakan DynamicHeap , yang memungkinkan Anda untuk menentukan ukuran memori lokal secara individual untuk setiap tugas. Jelas, overhead bekerja dengan memori aliran dalam hal ini akan jauh lebih tinggi.
Sekarang mari kita melihat lebih dekat pada sintaks kelas menggunakan dua aliran sebagai contoh, yang masing-masing secara independen beralih output port terpisah.
var m = new Mega328 { FCLK = 16000000, CKDIV8 = false }; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 2); tasks.Heap = new StaticHeap(tasks, 16); 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();
Baris teratas dari program ini sudah tidak asing lagi bagi Anda. Di dalamnya, kami menentukan jenis pengontrol dan menetapkan bit pertama dan kedua port B sebagai output. Berikutnya adalah inisialisasi variabel kelas Paralel , di mana pada parameter kedua kita menentukan jumlah maksimum utas eksekusi. Pada baris berikutnya, kami mengalokasikan memori untuk mengakomodasi aliran variabel lokal. Kami memiliki tugas yang sama, jadi kami menggunakan StaticHeap . Blok kode berikutnya adalah definisi tugas. Di dalamnya, kita mendefinisikan dua tugas yang hampir identik. Satu-satunya perbedaan adalah port kontrol dan jumlah penundaan. Untuk bekerja dengan objek tugas lokal, sebuah pointer ke tsk tugas lokal dilewatkan ke blok kode tugas. Teks tugas itu sendiri sangat sederhana:
- label lokal dibuat untuk mengatur siklus switching yang tak terbatas
- status port dibalik
- kontrol dikembalikan ke operator, dan tugas masuk ke status menunggu untuk jumlah milidetik yang ditentukan
- Pointer kembali diatur ke blok awal blok dan kontrol dikembalikan ke operator.
Jelas, dalam contoh konkret, perintah terakhir dapat diganti dengan perintah normal untuk pergi ke awal blok dan diberikan dalam contoh hanya untuk tujuan menunjukkannya. Jika diinginkan, contoh dapat dengan mudah diperluas untuk mengontrol sejumlah besar kesimpulan, dengan menyalin tugas dan meningkatkan jumlah utas.
Daftar lengkap perintah pembatalan tugas untuk mentransfer kontrol ke operator adalah sebagai berikut
AWAIT (signal) - stream menyimpan semua variabel dalam memori stream dan mentransfer kontrol ke dispatcher. Kali berikutnya aliran diaktifkan, variabel dikembalikan dan eksekusi berlanjut, dimulai dengan instruksi berikutnya setelah AWAIT . Perintah ini dirancang untuk membagi tugas ke dalam slot waktu dan untuk mengimplementasikan mesin keadaan sesuai dengan skema. Sinyal → Pemrosesan 1 → Sinyal → Pemrosesan 2 , dll.
Perintah AWAIT mungkin memiliki sinyal sebagai parameter opsional. Jika parameter kosong, sinyal aktivasi disimpan. Jika ditentukan dalam parameter, maka semua panggilan tugas selanjutnya akan dilakukan ketika sinyal yang ditentukan diaktifkan, dan komunikasi dengan sinyal sebelumnya terputus.
TaskContinue (label, signal) - perintah menghentikan streaming dan memberikan kontrol kepada operator tanpa menyimpan variabel. Lain kali aliran diaktifkan, kontrol ditransfer ke label label . Parameter Sinyal opsional memungkinkan Anda mengganti sinyal aktivasi streaming untuk panggilan berikutnya. Jika tidak ditentukan, sinyalnya tetap sama. Perintah tanpa menentukan sinyal dapat digunakan untuk mengatur siklus dalam satu tugas, di mana setiap siklus dilakukan dalam slot waktu yang terpisah. Itu juga dapat digunakan untuk menetapkan tugas baru ke utas saat ini setelah menyelesaikan yang sebelumnya. Keuntungan dari pendekatan ini dibandingkan dengan siklus Membebaskan utas → Menyoroti aliran adalah program yang lebih efisien. Menggunakan TaskContinue menghilangkan kebutuhan bagi manajer untuk mencari utas gratis di kolam renang dan menjamin kesalahan ketika mencoba mengalokasikan utas tanpa adanya utas gratis.
TaskEnd () - menghapus aliran setelah tugas selesai. Tugas berakhir, utas dibebaskan, dan dapat digunakan untuk menetapkan tugas baru dengan perintah Aktifkan .
Delay (ms) - stream, seperti dalam kasus menggunakan AWAIT , menyimpan semua variabel dalam memori stream dan mentransfer kontrol ke dispatcher. Dalam kasus ini, nilai penundaan dalam milidetik direkam di header aliran. Di loop dispatcher, dalam kasus nilai yang tidak nol di bidang penundaan, aliran tidak diaktifkan. Mengubah nilai dalam bidang penundaan untuk semua aliran dilakukan dengan memotong timer WDT setiap 16 ms. Ketika nilai nol tercapai, larangan eksekusi dihapus dan sinyal aktivasi aliran diatur. Hanya nilai byte tunggal untuk penundaan yang disimpan dalam header, yang memberikan kisaran kemungkinan penundaan yang relatif sempit, oleh karena itu, untuk menerapkan penundaan yang lebih lama, Delay () membuat loop internal menggunakan variabel aliran lokal.
Aktivasi perintah dalam contoh dilakukan menggunakan perintah ContinuousActivate dan ActivateNext . Ini adalah jenis khusus aktivasi tugas awal saat startup. Pada tahap aktivasi awal, kami dijamin tidak memiliki utas sibuk tunggal, sehingga proses aktivasi tidak memerlukan pencarian pendahuluan untuk utas gratis untuk suatu tugas dan memungkinkan Anda untuk mengaktifkan tugas secara berurutan. ContinuousActivate mengaktifkan tugas di utas nol dan mengembalikan pointer ke header utas berikutnya, dan fungsi ActivateNext menggunakan pointer ini untuk mengaktifkan tugas berikut dalam utas berurutan.
Sebagai sinyal aktivasi, contoh menggunakan sinyal AlwaysOn . Ini adalah salah satu sinyal sistem. Tujuannya berarti bahwa tugas akan selalu dieksekusi, karena ini adalah satu-satunya sinyal yang selalu diaktifkan dan tidak diatur ulang saat digunakan.
Contoh ini diakhiri dengan panggilan Loop . Fungsi ini memulai siklus operator, jadi perintah ini harus menjadi yang terakhir dalam kode.
Pertimbangkan contoh lain di mana penggunaan perpustakaan dapat secara signifikan menyederhanakan struktur kode. Biarkan itu menjadi perangkat kontrol bersyarat yang mendaftarkan sinyal analog dan mengirimkannya dalam bentuk kode HEX ke terminal.
var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var cData = m.DREG(); var outDigit = m.ARRAY(4); var chex = Const.String("0123456789ABCDEF"); m.ADC.Clock = eADCPrescaler.S64; m.ADC.ADCReserved = 0x01; m.ADC.Source = eASource.ADC0; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 8); var ADS = os.AddSignal(m.ADC.Handler, () => m.ADC.Data(cData)); var trm = os.AddSignal(m.Usart.TXC_Handler); var starts = os.AddLocker(); os.PrepareSignals(); var t0 = os.CreateTask((tsk) => { m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { m.ADC.ConvertAsync(); tsk.Delay(500); }); }, "activate"); var t1 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); var mref = m.ROMPTR(); mref.Load(chex); m.TempL.Load(cData.High); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[0]); mref.Load(chex); m.TempL.Load(cData.High); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[1]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[2]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[3]); starts.Set(); tsk.TaskContinue(loop); }); var t2 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); trm.Clear(); m.TempL.Load('0'); m.Usart.Transmit(m.TempL); tsk.AWAIT(trm); m.TempL.Load('x'); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[0]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[1]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[2]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[3]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(13); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(10); m.Usart.Transmit(m.TempL); tsk.TaskContinue(loop, starts); }); var p = os.ContinuousActivate(os.AlwaysOn, t0); os.ActivateNext(p, ADS, t1); os.ActivateNext(p, starts, t2); m.ADC.Activate(); m.Usart.Activate(); m.EnableInterrupt(); os.Loop();
Ini bukan untuk mengatakan bahwa kami melihat banyak hal baru di sini, tetapi Anda dapat melihat sesuatu yang menarik dalam kode ini.
Dalam contoh ini, ADC (konverter analog-ke-digital) pertama kali disebutkan. Perangkat periferal ini dirancang untuk mengubah tegangan sinyal input menjadi kode digital. Siklus konversi dimulai oleh fungsi ConvertAsync , yang hanya memulai proses tanpa menunggu hasilnya. Ketika konversi selesai, ADC menghasilkan interupsi yang mengaktifkan sinyal adcSig . Perhatikan definisi sinyal adcSig . Selain penunjuk interupsi, ini juga berisi blok kode untuk menyimpan nilai dari register data ADC. Semua kode yang sebaiknya dijalankan segera setelah gangguan terjadi (misalnya, membaca data dari register perangkat) harus ditempatkan di tempat ini.
Tugas konversi adalah untuk mengubah kode tegangan biner menjadi representasi HEX empat karakter untuk terminal kondisional kami. Di sini kita dapat mencatat penggunaan fungsi untuk menggambarkan fragmen berulang untuk mengurangi ukuran kode sumber dan penggunaan string konstan untuk konversi data.
Masalah transmisi menarik dari sudut pandang implementasi output string yang diformat di mana output data statis dan dinamis digabungkan. Mekanisme itu sendiri tidak dapat dianggap ideal, melainkan merupakan demonstrasi kemungkinan mengelola penangan. Di sini Anda juga dapat memperhatikan redefinisi sinyal aktivasi selama eksekusi, yang mengubah sinyal aktivasi dari ConvS ke TxS dan sebaliknya.
Untuk pemahaman yang lebih baik, kami menjelaskan algoritma kata-kata dari program ini.
Pada kondisi awal, kami telah meluncurkan tiga tugas. Dua di antaranya memiliki sinyal tidak aktif, karena sinyal untuk tugas konversi (adcSig) diaktifkan pada akhir siklus pembacaan sinyal analog, dan ConvS untuk tugas transmisi diaktifkan oleh kode yang belum dieksekusi. Akibatnya, tugas pertama yang akan diluncurkan setelah peluncuran akan selalu menjadi pengukuran. Kode untuk tugas ini memulai siklus konversi ADC, setelah itu tugas 500 ms masuk ke siklus tunggu. Di akhir siklus konversi, flag adcSig diaktifkan , yang memicu tugas konversi . Dalam tugas ini, siklus mengubah data yang diterima ke string diimplementasikan. Sebelum keluar dari tugas, kami mengaktifkan bendera ConvS , membuatnya jelas bahwa kami memiliki data baru untuk dikirim ke terminal. Perintah keluar me-reset titik kembali ke awal tugas dan memberikan kontrol kepada operator. Set bendera ConvS memungkinkan transfer kontrol ke tugas transmisi . Setelah mentransmisikan byte pertama dari urutan, sinyal aktivasi dalam tugas berubah menjadi TxS . Sebagai akibatnya, setelah transfer byte selesai, tugas transmisi akan dipanggil lagi, yang akan mengarah pada transfer byte berikutnya. Setelah byte terakhir dari urutan ditransmisikan, tugas mengembalikan sinyal aktivasi ConvS dan mengatur ulang titik kembali ke awal tugas. Siklus selesai. Siklus berikutnya akan dimulai ketika tugas pengukuran selesai menunggu dan mengaktifkan siklus pengukuran berikutnya.
Di hampir semua sistem multitasking, ada konsep antrian untuk interaksi antara utas. Kami telah menemukan bahwa sejak beralih antar tugas dalam sistem ini adalah proses yang sepenuhnya terkontrol, menggunakan variabel global untuk bertukar data antar tugas sangat mungkin dilakukan. Namun, ada sejumlah tugas di mana penggunaan antrian dibenarkan. Karenanya, kami tidak akan mengesampingkan topik ini dan melihat bagaimana penerapannya di perpustakaan.
Untuk mengimplementasikan antrian dalam suatu program, yang terbaik adalah menggunakan kelas RingBuff . Kelas, seperti namanya, mengimplementasikan buffer cincin dengan perintah tulis dan ambil. Membaca dan menulis data dilakukan oleh perintah Baca dan Tulis . Perintah baca dan tulis tidak memiliki parameter. Buffer menggunakan variabel register yang ditentukan dalam konstruktor sebagai sumber / penerima data. Akses ke variabel ini dilakukan melalui parameter kelas IOReg . Status buffer ditentukan oleh dua flag Ovf dan Empty , yang membantu menentukan status overflow selama penulisan dan overflow saat membaca. Selain itu, kelas memiliki kemampuan untuk menentukan kode yang berjalan pada peristiwa overflow / overflow. RingBuff tidak memiliki dependensi pada kelas Paralel dan dapat digunakan secara terpisah. Batasan ketika bekerja dengan kelas adalah kapasitas yang diijinkan, yang seharusnya merupakan kelipatan dari kekuatan dua (8.16.32, dll.) Karena alasan optimasi kode.
Contoh bekerja dengan kelas diberikan di bawah ini.
var m = new Mega328(); var io = m.REG();
Bagian ini menyimpulkan gambaran umum fitur perpustakaan. Sayangnya, masih ada sejumlah aspek berkenaan dengan kemampuan perpustakaan, yang bahkan tidak disebutkan. Di masa depan, jika tertarik dengan proyek ini, artikel direncanakan didedikasikan untuk menyelesaikan masalah spesifik menggunakan perpustakaan dan deskripsi yang lebih rinci tentang masalah kompleks yang membutuhkan publikasi terpisah.