Mikrokontroler AVR cukup murah dan tersebar luas. Mungkin, hampir semua pengembang tertanam mulai dengan mereka. Dan di antara para amatir, Arduino menguasai bola, yang hatinya biasanya ATmega328p. Tentunya banyak yang bertanya-tanya: bagaimana Anda bisa membuatnya terdengar?
Jika Anda melihat proyek yang ada, ada beberapa jenis:
- Generator pulsa persegi. Hasilkan menggunakan PWM atau pin yank dalam interupsi. Bagaimanapun, suara mencicit yang sangat khas diperoleh.
- Menggunakan peralatan eksternal seperti dekoder MP3.
- Menggunakan PWM untuk menghasilkan suara 8 bit (kadang-kadang 16 bit) dalam format PCM atau ADPCM. Karena memori dalam mikrokontroler jelas tidak cukup untuk ini, mereka biasanya menggunakan kartu SD.
- Menggunakan PWM untuk menghasilkan suara berdasarkan tabel gelombang seperti MIDI.
Jenis yang terakhir ini sangat menarik bagi saya, karena hampir tidak memerlukan peralatan tambahan. Saya menyajikan opsi saya kepada komunitas. Pertama, demo kecil:
Tertarik, saya minta kucing.
Jadi, peralatannya:
- ATmega8 atau ATmega328. Porting ke ATmega lain tidak sulit. Dan bahkan pada ATtiny, tetapi lebih lanjut tentang itu nanti;
- Resistor;
- Kapasitor;
- Speaker atau headphone;
- Nutrisi;
Suka semuanya.
Sirkuit RC sederhana dengan speaker terhubung ke output mikrokontroler. Outputnya adalah suara 8-bit dengan frekuensi sampling 31250Hz. Pada frekuensi kristal 8 MHz, hingga 5 saluran suara + satu saluran derau untuk perkusi dapat dihasilkan. Ia menggunakan hampir seluruh waktu prosesor, tetapi setelah mengisi buffer, prosesor dapat diisi dengan sesuatu yang berguna selain suara:
Contoh ini sangat cocok dengan memori ATmega8, 5 saluran + noise diproses pada frekuensi kristal 8 MHz dan ada sedikit waktu untuk animasi di layar.
Dalam contoh ini, saya juga ingin menunjukkan bahwa perpustakaan dapat digunakan tidak hanya sebagai kartu pos musik biasa, tetapi juga untuk menghubungkan suara ke proyek yang ada, misalnya, untuk pemberitahuan. Dan bahkan ketika menggunakan hanya satu saluran suara, notifikasi dapat jauh lebih menarik daripada tweeter sederhana.
Dan sekarang detailnya ...
Tabel gelombang atau tabel gelombang
Matematika itu sangat sederhana. Ada fungsi nada periodik, misalnya
nada (t) = sin (t * freq / (2 * Pi)) .
Ada juga fungsi untuk mengubah volume nada fundamental dari waktu ke waktu, misalnya
volume (t) = e ^ (- t) .
Dalam kasus yang paling sederhana, bunyi suatu instrumen adalah produk dari
instrumen fungsi ini
(t) = nada (t) * volume (t) :
Pada bagan, semuanya terlihat seperti ini:

Selanjutnya, kami mengambil semua instrumen yang terdengar pada waktu tertentu dan merangkumnya dengan beberapa faktor volume (kode semu):
for (i = 0; i < CHANNELS; i++) { value += channels[i].tone(t) * channels[i].volume(t) * channels[i].volume; }
Hanya perlu memilih volume sehingga tidak ada luapan. Dan itu hampir semuanya.
Saluran noise bekerja dengan cara yang hampir sama, tetapi alih-alih fungsi nada, generator urutan pseudo-acak.
Perkusi adalah campuran saluran noise dan gelombang frekuensi rendah, sekitar 50-70 Hz.
Tentu saja, suara berkualitas tinggi dengan cara ini sulit dicapai. Tapi kami hanya punya 8 kilobyte untuk semuanya. Semoga ini bisa dimaafkan.
Apa yang bisa saya peras dari 8 bit
Awalnya, saya fokus pada ATmega8. Tanpa kuarsa eksternal, beroperasi pada frekuensi 8 MHz dan memiliki PWM 8-bit, yang memberikan frekuensi sampling dasar 8000000/256 = 31250 Hz. Satu timer menggunakan PWM untuk mengeluarkan suara, dan menyebabkan gangguan selama overflow untuk mengirimkan nilai berikutnya ke generator PWM. Oleh karena itu, kami memiliki 256 siklus untuk menghitung nilai sampel untuk semuanya, termasuk interupsi overhead, memperbarui parameter saluran suara, melacak waktu ketika Anda perlu memainkan nada berikutnya, dll.
Untuk optimasi, kami akan secara aktif menggunakan trik berikut:
- Karena kami memiliki prosesor delapan bit, kami akan mencoba membuat variabel yang sama. Terkadang kita akan menggunakan 16 bit.
- Perhitungan secara kondisional dibagi menjadi sering dan tidak demikian. Yang pertama perlu dihitung untuk setiap sampel, yang kedua - jauh lebih jarang, sekali setiap beberapa puluh / ratusan sampel.
- Untuk mendistribusikan beban secara merata, kami menggunakan buffer bundar. Di loop utama program, kami mengisi buffer, kurangi di interrupt. Jika semuanya baik-baik saja, maka buffer mengisi lebih cepat daripada yang dikosongkan dan kita punya waktu untuk sesuatu yang lain.
- Kode ditulis dalam C dengan banyak inline. Latihan menunjukkan bahwa itu jauh lebih cepat.
- Semua itu dapat dihitung oleh preprocessor, terutama dengan partisipasi divisi, dilakukan oleh preprocessor.
Pertama, bagi waktu menjadi interval 4 milidetik (saya menyebutnya kutu). Pada frekuensi sampling 31250Hz, kami mendapatkan 125 sampel per tick. Fakta bahwa setiap sampel harus dibaca harus dihitung setiap sampel, dan sisanya - sekali per centang atau kurang. Misalnya, dalam satu centang, volume instrumen akan konstan:
instrumen (t) = nada (t) * currentVolume ; dan currentVolume itu sendiri akan dihitung ulang sekali per centang dengan memperhitungkan volume akun (t) dan volume yang dipilih dari saluran suara.
Durasi tick dari 4ms dipilih berdasarkan batas 8-bit sederhana: dengan penghitung sampel delapan-bit, Anda dapat bekerja dengan frekuensi sampling hingga 64 kHz, dengan penghitung centang delapan-bit kita dapat mengukur waktu hingga 1 detik.
Beberapa kode
Saluran itu sendiri dijelaskan oleh struktur ini:
typedef struct { // Info about wave const int8_t* waveForm; // Wave table array uint16_t waveSample; // High byte is an index in waveForm array uint16_t waveStep; // Frequency, how waveSample is changed in time // Info about volume envelope const uint8_t* volumeForm; // Array of volume change in time uint8_t volumeFormLength; // Length of volumeForm uint8_t volumeTicksPerSample; // How many ticks should pass before index of volumeForm is changed uint8_t volumeTicksCounter; // Counter for volumeTicksPerSample // Info about volume uint8_t currentVolume; // Precalculated volume for current tick uint8_t instrumentVolume; // Volume of channel } waveChannel;
Secara kondisional, data di sini dibagi menjadi 3 bagian:
- Informasi tentang bentuk gelombang, fase, frekuensi.
waveForm: informasi tentang fungsi nada (t): referensi ke array dengan panjang 256 byte. Mengatur nada, suara instrumen.
waveSample: byte tinggi menunjukkan indeks array waveForm saat ini.
waveStep: mengatur frekuensi di mana waveSample akan ditingkatkan saat menghitung sampel berikutnya.
Setiap sampel dianggap seperti ini:
int8_t tone = channelData.waveForm[channelData.waveSample >> 8]; channelData.waveSample += channelaData.waveStep; return tone * channelData.currentVolume;
- Informasi volume. Mengatur fungsi perubahan volume seiring waktu. Karena volume tidak terlalu sering berubah, Anda dapat menghitungnya lebih jarang, sekali per centang. Ini dilakukan seperti ini:
if ((channel->volumeTicksCounter--) == 0 && channel->volumeFormLength > 0) { channel->volumeTicksCounter = channel->volumeTicksPerSample; channel->volumeFormLength--; channel->volumeForm++; } channel->currentVolume = channel->volumeForm * channel->instrumentVolume >> 8;
- Setel volume saluran dan volume saat ini yang dihitung.
Harap dicatat: bentuk gelombang adalah delapan-bit, volumenya juga delapan-bit, dan hasilnya adalah 16-bit. Dengan sedikit kehilangan kinerja, Anda dapat membuat suara (hampir) 16 bit.
Dalam perjuangan untuk produktivitas, saya harus menggunakan sihir hitam.
Contoh nomor 1. Cara menghitung ulang volume saluran:
if ((tickSampleCounter--) == 0) { // tickSampleCounter = SAMPLES_PER_TICK β 1; // - } // volume recalculation should no be done so often for all channels if (tickSampleCounter < CHANNELS_SIZE) { recalculateVolume(channels[tickSampleCounter]); }
Dengan demikian, semua saluran menghitung volume sekali per centang, tetapi tidak secara bersamaan.
Contoh nomor 2. Menyimpan informasi saluran dalam struktur statis lebih murah daripada dalam array. Tanpa merinci implementasi wavechannel.h, saya akan mengatakan bahwa file ini dimasukkan ke dalam kode beberapa kali (sama dengan jumlah saluran) dengan arahan preprosesor yang berbeda. Setiap sisipan membuat variabel global baru dan fungsi penghitungan saluran baru, yang kemudian dimasukkan ke dalam kode utama:
Contoh nomor 3. Jika kita mulai memainkan not berikutnya sedikit kemudian, maka tidak ada yang akan memperhatikan. Mari kita bayangkan situasinya: kita mengambil prosesor dengan sesuatu dan selama ini buffer hampir kosong. Kemudian kita mulai mengisinya dan tiba-tiba ternyata ada ukuran baru: kita perlu memperbarui catatan saat ini, membaca dari array apa selanjutnya, dll. Jika kita tidak punya waktu, maka akan ada ciri kegagapan. Adalah jauh lebih baik untuk mengisi buffer sedikit dengan data lama, dan hanya kemudian memperbarui keadaan saluran.
while ((samplesToWrite) > 4) { // fillBuffer(SAMPLES_PER_TICK); // - updateMusicData(); // }
Dengan cara yang baik, perlu untuk mengisi ulang buffer setelah loop, tetapi karena kita memiliki hampir semuanya inline, ukuran kode terasa meningkat.
Musik
Pencatat centang delapan bit digunakan. Ketika nol tercapai, ukuran baru dimulai, penghitung diberikan durasi pengukuran (dalam kutu), sedikit kemudian array perintah musik diperiksa.
Data musik disimpan dalam array byte. Ada tertulis seperti ini:
const uint8_t demoSample[] PROGMEM = { DATA_TEMPO(160), // Set beats per minute DATA_INSTRUMENT(0, 1), // Assign instrument 1 (see setSample) to channel 0 DATA_INSTRUMENT(1, 1), // Assign instrument 1 (see setSample) to channel 1 DATA_VOLUME(0, 128), // Set volume 128 to channel 0 DATA_VOLUME(1, 128), // Set volume 128 to channel 1 DATA_PLAY(0, NOTE_A4, 1), // Play note A4 on channel 0 and wait 1 beat DATA_PLAY(1, NOTE_A3, 1), // Play note A3 on channel 1 and wait 1 beat DATA_WAIT(63), // Wait 63 beats DATA_END() // End of data stream };
Semua yang dimulai dengan DATA_ adalah macro preprocessor yang memperluas parameter ke jumlah byte data yang diperlukan.
Misalnya, perintah DATA_PLAY diperluas menjadi 2 byte, di mana disimpan: penanda perintah (1 bit), jeda sebelum perintah berikutnya (3 bit), nomor saluran tempat memainkan not (4 bit), informasi tentang not (8 bit). Keterbatasan yang paling signifikan adalah bahwa perintah ini tidak dapat digunakan untuk jeda panjang, dengan maksimum 7 langkah. Jika Anda membutuhkan lebih banyak, maka Anda perlu menggunakan perintah DATA_WAIT (hingga 63 tindakan). Sayangnya, saya tidak menemukan apakah makro dapat diperluas ke jumlah byte array yang berbeda tergantung pada parameter makro. Dan bahkan peringatan saya tidak tahu cara menampilkan. Mungkin Anda memberi tahu saya.
Gunakan
Dalam direktori demo ada beberapa contoh untuk mikrokontroler yang berbeda. Namun singkatnya, ini adalah bagian dari readme, saya benar-benar tidak perlu menambahkan:
Jika Anda ingin melakukan hal lain selain musik, maka Anda dapat menambah ukuran buffer menggunakan BUFFER_SIZE. Ukuran buffer harus 2 ^ n, tetapi, sayangnya, dengan ukuran 256, terjadi penurunan kinerja. Sampai saya menemukan jawabannya.
Untuk meningkatkan produktivitas, Anda dapat meningkatkan frekuensi dengan kuarsa eksternal, Anda dapat mengurangi jumlah saluran, Anda dapat mengurangi frekuensi pengambilan sampel. Dengan trik terakhir, Anda dapat menggunakan interpolasi linier, yang agak mengompensasi penurunan kualitas suara.
Penundaan tidak disarankan, karena Waktu CPU terbuang sia-sia. Sebagai gantinya, metode sendiri diimplementasikan dalam file
microsound / delay.h , yang, selain jeda itu sendiri, terlibat dalam mengisi buffer. Metode ini mungkin tidak bekerja dengan sangat akurat pada jeda pendek, tetapi pada jeda panjang lebih atau kurang waras.
Buat musik Anda sendiri
Jika Anda menulis perintah secara manual, Anda harus dapat mendengarkan apa yang terjadi. Menuangkan setiap perubahan ke dalam mikrokontroler tidak nyaman, terutama jika ada alternatif.
Ada layanan yang agak lucu
wavepot.com - editor JavaScript online di mana Anda perlu mengatur fungsi sinyal suara dari waktu ke waktu, dan sinyal ini adalah output ke kartu suara. Contoh paling sederhana:
function dsp(t) { return 0.1 * Math.sin(2 * Math.PI * t * 440); }
Saya porting mesin ke JavaScript, itu terletak di
demo / wavepot.js . Isi file harus dimasukkan dalam editor
wavepot.com dan Anda dapat melakukan percobaan. Kami menulis data kami ke array soundData, dengarkan, jangan lupa untuk menyimpan.
Kita juga harus menyebutkan variabel simulate8bits. Dia, sesuai namanya, mensimulasikan suara delapan-bit. Jika tiba-tiba terlihat bahwa drum berdengung, dan suara muncul dalam instrumen yang basah dengan suara yang tenang, maka inilah dia, sebuah distorsi dari suara delapan-bit. Anda dapat mencoba menonaktifkan opsi ini dan mendengarkan perbedaannya. Masalahnya kurang terlihat jika tidak ada keheningan dalam musik.
Koneksi
Dalam versi sederhana, rangkaiannya terlihat seperti ini:
+5V ^ MCU | +-------+ +---+VC | R1 | Pin+---/\/\--+-----> OUT | | | +---+GN | === C1 | +-------+ | | | --- Grnd --- Grnd
Pin output tergantung pada mikrokontroler. Resistor R1 dan kapasitor C1 harus dipilih berdasarkan pada beban, penguat (jika ada), dll. Saya bukan seorang insinyur elektronik dan saya tidak akan memberikan formula, mereka mudah di-google bersama dengan kalkulator online.
Saya memiliki R1 = 130 Ohm, C1 = 0,33 uF. Untuk output saya menghubungkan headphone Cina biasa.
Apa yang ada di sana tentang 16 bit suara?
Seperti yang saya katakan di atas, ketika kita mengalikan dua angka delapan-bit (frekuensi dan volume), kita mendapatkan angka 16-bit. Anda tidak dapat membulatkannya menjadi delapan bit, tetapi output kedua byte dalam 2 saluran PWM. Jika Anda mencampur 2 saluran ini dalam rasio 1/256, maka kami bisa mendapatkan suara 16 bit. Perbedaan dengan delapan-bit sangat mudah didengar pada suara dan drum yang memudar dengan lancar di saat-saat ketika hanya satu instrumen yang berbunyi.
Koneksi output 16-bit:
+5V ^ MCU | +-------+ +---+VCC | R1 | PinH+---/\/\--+-----> OUT | | | | | R2 | | PinL+---/\/\--+ +---+GND | | | +-------+ === C1 | | --- Grnd --- Grnd
Penting untuk mencampur 2 output dengan benar: resistansi R2 harus 256 kali lebih besar dari resistansi R1. Semakin akurat, semakin baik. Sayangnya, bahkan resistor dengan kesalahan 1% tidak memberikan akurasi yang diperlukan. Namun, bahkan dengan pemilihan resistor yang tidak terlalu akurat, distorsi dapat secara nyata dilemahkan.
Sayangnya, ketika menggunakan suara 16-bit, kinerja menurun dan 5 saluran + noise tidak lagi punya waktu untuk diproses dalam siklus 256 jam yang diberikan.
Apakah mungkin di Arduino?
Ya kamu bisa. Saya hanya memiliki klon nano Cina di ATmega328p, itu berfungsi di atasnya. Kemungkinan besar, arduin lain pada ATmega328p juga harus berfungsi. ATmega168 tampaknya memiliki register pengatur waktu yang sama. Kemungkinan besar mereka akan bekerja tidak berubah. Pada mikrokontroler lain yang perlu Anda periksa, Anda mungkin perlu menambahkan driver.
Ada sketsa di
demo / arduino328p , tetapi untuk itu bisa dibuka secara normal di IDE Arduino, Anda perlu menyalinnya ke root proyek.
Dalam contoh, suara 16-bit dihasilkan dan output D9 dan D10 digunakan. Untuk mempermudah, Anda dapat membatasi diri pada suara 8-bit dan hanya menggunakan satu output D9.
Karena hampir semua arduin beroperasi pada 16 MHz, maka, jika diinginkan, Anda dapat menambah jumlah saluran menjadi 8.
Bagaimana dengan ATtiny?
ATtiny tidak memiliki perkalian perangkat keras. Penggandaan perangkat lunak yang digunakan kompilator sangat lambat dan sebaiknya dihindari. Saat menggunakan sisipan assembler yang dioptimalkan, kinerja turun 2 kali dibandingkan ATmega. Tampaknya tidak ada gunanya menggunakan ATtiny sama sekali, tapi ...
Beberapa ATtiny memiliki pengganda frekuensi, PLL. Dan ini berarti bahwa pada mikrokontroler semacam itu ada 2 fitur menarik:
- Frekuensi generator PWM adalah 64 MHz, yang memberikan periode PWM 250 kHz, yang jauh lebih baik daripada 31250 Hz pada 8 MHz atau 62500 Hz dengan kuarsa pada 16 MHz pada ATmega apa pun.
- Pengganda frekuensi yang sama memungkinkan kristal untuk clock pada 16 MHz tanpa kuarsa.
Maka kesimpulannya: beberapa ATtiny dapat digunakan untuk menghasilkan suara. Mereka berhasil memproses 5 instrumen + saluran noise yang sama, tetapi pada 16 MHz dan mereka tidak memerlukan kuarsa eksternal.
Kelemahannya adalah bahwa frekuensinya tidak dapat ditingkatkan lagi, dan perhitungannya memakan waktu hampir sepanjang waktu. Untuk membebaskan sumber daya, Anda dapat mengurangi jumlah saluran atau laju sampel.
Minus lainnya adalah kebutuhan untuk menggunakan dua timer sekaligus: satu untuk PWM, yang kedua untuk gangguan. Di sinilah timer biasanya berakhir.
Dari mikrokontroler PLL yang saya tahu, saya bisa menyebutkan ATtiny85 / 45/25 (8 kaki), ATtiny861 / 461/261 (20 kaki), ATtiny26 (20 kaki).
Sedangkan untuk memori, perbedaannya dengan ATmega tidak besar. Dalam 8kb, beberapa instrumen dan melodi cocok dengan sempurna. Dalam 4kb Anda dapat menempatkan 1-2 instrumen dan 1-2 lagu. Sulit untuk memasukkan sesuatu ke dalam 2 kilobyte, tetapi jika Anda benar-benar ingin, maka Anda bisa. Penting untuk memisahkan metode, menonaktifkan beberapa fungsi seperti kontrol volume atas saluran, mengurangi frekuensi pengambilan sampel dan jumlah saluran. Secara umum, untuk seorang amatir, tetapi ada contoh yang berfungsi pada ATtiny26.
Masalahnya
Ada masalah. Dan masalah terbesar adalah kecepatan komputasi. Kode ini sepenuhnya ditulis dalam C dengan sisipan multiplikasi assembler kecil untuk ATtiny. Optimasi diberikan kepada kompiler dan terkadang berperilaku aneh. Dengan perubahan kecil yang tampaknya tidak memengaruhi apa pun, Anda bisa mendapatkan penurunan kinerja yang nyata. Selain itu, mengubah dari -O ke -O3 tidak selalu membantu. Salah satu contohnya adalah penggunaan buffer 256 byte. Terutama tidak menyenangkan adalah bahwa tidak ada jaminan bahwa dalam versi baru dari kompiler kita tidak akan mendapatkan penurunan kinerja pada kode yang sama.
Masalah lain adalah bahwa mekanisme atenuasi sebelum not selanjutnya tidak diimplementasikan sama sekali. Yaitu ketika pada satu saluran satu not diganti dengan yang lain, suara lama tiba-tiba terganggu, kadang-kadang terdengar bunyi klik kecil. Saya ingin menemukan cara untuk menyingkirkan ini tanpa kehilangan kinerja, tetapi sejauh ini.
Tidak ada perintah untuk meningkatkan / mengurangi volume dengan lancar. Ini sangat penting untuk nada dering notifikasi pendek, di mana pada akhirnya Anda perlu membuat redaman cepat pada volume sehingga tidak ada suara yang tajam. Bagian dari masalah adalah dengan menulis serangkaian perintah dengan mengatur volume dan jeda singkat secara manual.
Pendekatan yang dipilih, pada prinsipnya, tidak mampu memberikan suara naturalistik untuk instrumen. Untuk suara yang lebih alami, Anda perlu membagi suara instrumen menjadi serangan-berkelanjutan-rilis, gunakan setidaknya 2 bagian pertama dan dengan durasi yang jauh lebih lama dari satu periode osilasi. Tetapi kemudian data untuk alat tersebut akan membutuhkan lebih banyak. Ada ide untuk menggunakan tabel gelombang yang lebih pendek, misalnya, dalam 32 byte, bukan 256, tetapi tanpa interpolasi, kualitas suara menurun secara dramatis, dan dengan interpolasi, kinerja menurun. Dan 8 bit sampel lainnya jelas tidak cukup untuk musik, tetapi ini dapat dielakkan.
Ukuran buffer dibatasi hingga 256 sampel. Ini sesuai dengan sekitar 8 milidetik dan ini adalah periode waktu integral maksimum yang dapat diberikan untuk tugas-tugas lain. Pada saat yang sama, pelaksanaan tugas masih ditangguhkan secara berkala oleh interupsi.
Mengganti penundaan standar tidak bekerja dengan sangat akurat untuk jeda singkat.
Saya yakin ini bukan daftar lengkap.
Referensi