Memprogram game untuk perangkat tertanam pada ESP32: drive, baterai, suara

gambar


Mulai: sistem perakitan, input, tampilan .

Bagian 4: drive


Odroid Go memiliki slot kartu microSD, yang akan berguna untuk mengunduh sumber daya (sprite, file suara, font), dan bahkan mungkin untuk menyelamatkan keadaan permainan.

Pembaca kartu terhubung melalui SPI, tetapi IDF membuatnya mudah untuk berinteraksi dengan kartu SD dengan mengabstraksi panggilan SPI dan menggunakan fungsi POSIX standar seperti fopen , fread dan fwrite . Semua ini didasarkan pada pustaka FatFs , sehingga kartu SD harus diformat dalam format FAT standar.

Ini terhubung ke bus SPI yang sama dengan LCD, tetapi menggunakan jalur pemilihan chip yang berbeda. Ketika kita perlu membaca atau menulis ke kartu SD (dan ini tidak sering terjadi), driver SPI akan mengalihkan sinyal CS dari layar ke pembaca kartu SD, dan kemudian melakukan operasi. Ini berarti bahwa saat mengirim data ke layar, kami tidak dapat melakukan operasi apa pun dengan kartu SD, dan sebaliknya.

Saat ini kami sedang melakukan semuanya dalam satu utas dan menggunakan transmisi pemblokiran melalui SPI ke layar, sehingga tidak ada transaksi simultan dengan kartu SD dan dengan layar LCD. Bagaimanapun, ada kemungkinan besar bahwa kami akan memuat semua sumber daya pada saat peluncuran.

Modifikasi ESP-IDF


Jika kami mencoba menginisialisasi antarmuka kartu SD setelah inisialisasi tampilan, kami akan menghadapi masalah yang membuatnya tidak mungkin memuat Odroid Go. ESP-IDF v4.0 tidak mendukung akses bersama ke bus SPI saat digunakan dengan kartu SD. Baru-baru ini, pengembang telah menambahkan fungsi ini, tetapi belum dalam rilis yang stabil, jadi kami akan melakukan sedikit modifikasi pada IDF sendiri.

Mengomentari baris 303 esp-idf / components / driver / sdspi_host.c :

// Initialize SPI bus
esp_err_t ret = spi_bus_initialize((spi_host_device_t)slot, &buscfg,
    slot_config->dma_channel);
if (ret != ESP_OK) {
    ESP_LOGD(TAG, "spi_bus_initialize failed with rc=0x%x", ret);
    //return ret;
}

Setelah melakukan perubahan ini, kami masih akan melihat kesalahan selama inisialisasi, tetapi itu tidak akan lagi menyebabkan ESP32 untuk memulai kembali, karena kode kesalahan tidak menyebar di atas.

Inisialisasi




Kita perlu memberi tahu IDF pin ESP32 mana yang terhubung ke pembaca MicroSD sehingga mengkonfigurasi dengan benar driver SPI yang mendasarinya, yang sebenarnya berkomunikasi dengan pembaca.

Catatan umum VSPI.XXXX lagi digunakan dalam diagram , tetapi kita dapat melewatinya ke nomor kontak aktual pada ESP32.

Inisialisasi mirip dengan inisialisasi LCD, tetapi alih-alih struktur konfigurasi SPI umum, kami menggunakan sdspi_slot_config_t , yang dirancang untuk kartu SD yang terhubung melalui bus SPI. Kami mengonfigurasi nomor kontak yang sesuai dan properti pemasangan kartu di sistem FatFS.

Dokumentasi IDF tidak merekomendasikan penggunaan fungsi esp_vfs_fat_sdmmc_mountdalam kode program yang selesai. Ini adalah fungsi pembungkus yang melakukan banyak operasi untuk kita, tetapi sejauh ini berfungsi cukup normal, dan mungkin tidak ada yang akan berubah di masa depan.

Parameter "/ sdcard" dari fungsi ini menetapkan titik pemasangan virtual kartu SD, yang kemudian akan kita gunakan sebagai awalan saat bekerja dengan file. Jika kami memiliki file bernama "test.txt" pada kartu SD kami, jalur yang akan kami gunakan untuk menautkannya adalah "/sdcard/test.txt".

Setelah inisialisasi antarmuka kartu SD, interaksi dengan file sepele: kita cukup menggunakan panggilan standar ke fungsi POSIX , yang sangat nyaman.

8.3, . , fopen . make menuconfig, , 8.3.



Saya membuat sprite 64x64 dalam Aseprite (mengerikan) yang hanya menggunakan dua warna: benar-benar hitam (piksel dinonaktifkan) dan sepenuhnya putih (piksel diaktifkan). Aseprite tidak memiliki opsi untuk menyimpan warna RGB565 atau mengekspor sebagai bitmap mentah (mis. Tanpa header kompresi dan gambar), jadi saya mengekspor sprite ke format PNG sementara.

Kemudian, menggunakan ImageMagick, saya mengkonversi data ke file PPM, yang mengubah gambar menjadi data mentah terkompresi dengan header sederhana. Selanjutnya, saya membuka gambar dalam hex editor, menghapus header dan mengubah warna 24-bit menjadi 16-bit, menghapus semua kejadian 0x000000 hingga 0x0000 , dan semua kejadian 0xFFFFFF ke 0xFFFF. Urutan byte di sini bukan masalah, karena 0x0000 dan 0xFFFF tidak berubah saat mengubah urutan byte.

File mentah dapat diunduh dari sini .

FILE* spriteFile = fopen("/sdcard/key", "r");
assert(spriteFile);

uint16_t* sprite = (uint16_t*)malloc(64 * 64 * sizeof(uint16_t));

for (int i = 0; i < 64; ++i)
{
	for (int j = 0; j < 64; ++j)
	{
		fread(sprite, sizeof(uint16_t), 64 * 64, spriteFile);
	}
}

fclose(spriteFile);

Pertama, kita buka file kunci yang berisi byte mentah dan membacanya ke buffer. Di masa depan, kami akan memuat sumber daya sprite secara berbeda, tetapi untuk demo ini cukup.

int spriteRow = 0;
int spriteCol = 0;

for (int row = y; row < y + 64; ++row)
{
	spriteCol = 0;

	for (int col = x; col < x + 64; ++col)
	{
		uint16_t pixelColor = sprite[64 * spriteRow + spriteCol];

		if (pixelColor != 0)
		{
			gFramebuffer[row * LCD_WIDTH + col] = color;
		}

		++spriteCol;
	}

	++spriteRow;
}

Untuk menggambar sprite, kami mengulangi isinya secara iteratif. Jika pikselnya putih, maka kami menggambarnya dalam warna yang dipilih oleh tombol. Jika berwarna hitam, maka kami menganggapnya sebagai latar belakang dan tidak menggambar.


Kamera ponsel saya sangat mendistorsi warna. Dan maaf karena mengguncangnya.

Untuk menguji perekaman gambar, kami akan memindahkan kunci ke suatu tempat di layar, mengubah warnanya, dan kemudian menulis buffer bingkai ke kartu SD sehingga dapat dilihat di komputer.

if (input.menu)
{
	const char* snapFilename = "/sdcard/framebuf";

	ESP_LOGI(LOG_TAG, "Writing snapshot to %s", snapFilename);

	FILE* snapFile = fopen(snapFilename, "wb");
	assert(snapFile);

		fwrite(gFramebuffer, sizeof(gFramebuffer[0]), LCD_WIDTH * LCD_HEIGHT, snapFile);
	}

	fclose(snapFile);
}

Menekan tombol Menu menyimpan isi buffer bingkai ke file yang disebut framebuf . Ini akan menjadi buffer bingkai mentah, sehingga piksel akan tetap dalam format RGB565 dengan urutan byte terbalik. Kita dapat lagi menggunakan ImageMagick untuk mengonversi format ini ke PNG untuk melihatnya di komputer.

convert -depth 16 -size 320x240+0 -endian msb rgb565:FRAMEBUF snap.png

Tentu saja, kita dapat menerapkan membaca / menulis ke format BMP / PNG dan menyingkirkan semua keributan ini dengan ImageMagick, tetapi ini hanya kode demo. Sejauh ini saya belum memutuskan format file mana yang ingin saya gunakan untuk menyimpan sprite.


Ini dia! Buffer bingkai Odroid Go ditampilkan di komputer desktop.

Referensi



Bagian 5: baterai


Odroid Go memiliki baterai lithium-ion, sehingga kami dapat membuat game yang dapat Anda mainkan saat bepergian. Ini adalah ide yang menggoda bagi seseorang yang memainkan Gameboy pertama sebagai seorang anak.

Oleh karena itu, kita memerlukan cara untuk meminta tingkat baterai Odroid Go. Baterai terhubung ke kontak pada ESP32, sehingga kita dapat membaca tegangan untuk mengetahui perkiraan waktu pengoperasian yang tersisa.

Skema



Diagram menunjukkan IO36 terhubung ke tegangan VBAT setelah ditarik ke tanah melalui resistor. Dua resistor ( R21 dan R23 ) membentuk pembagi tegangan yang serupa dengan yang digunakan pada salib gamepad; resistor sekali lagi memiliki resistansi yang sama sehingga tegangannya setengah dari aslinya.

Karena pembagi tegangan, IO36 akan membaca tegangan sama dengan setengah VBAT . Ini mungkin dilakukan karena kontak ADC pada ESP32 tidak dapat membaca tegangan tinggi baterai lithium-ion (4,2 V pada muatan maksimum). Bagaimanapun, ini berarti bahwa untuk mendapatkan tegangan yang benar, Anda perlu menggandakan tegangan membaca dari ADC (ADC).

Saat membaca nilai IO36, kami mendapatkan nilai digital, tetapi kehilangan nilai analog yang diwakilinya. Kita membutuhkan cara untuk menginterpretasikan nilai digital dengan ADC dalam bentuk tegangan analog fisik.

IDF memungkinkan Anda untuk mengkalibrasi ADC, yang mencoba memberikan level tegangan berdasarkan tegangan referensi. Tegangan referensi ini ( Vref ) adalah 1100 mV secara default, tetapi karena karakteristik fisik, setiap perangkat sedikit berbeda. ESP32 di Odroid Go memiliki Vref yang ditentukan secara manual, "di-flash" di eFuse, yang dapat kita gunakan sebagai Vref yang lebih akurat.

Prosedurnya adalah sebagai berikut: pertama, kita akan mengkonfigurasi kalibrasi ADC, dan ketika kita ingin membaca voltase, kita akan mengambil sejumlah sampel tertentu (misalnya, 20) untuk menghitung rata-rata bacaan; maka kami menggunakan IDF untuk mengubah pembacaan ini menjadi tegangan. Perhitungan rata-rata menghilangkan kebisingan dan memberikan pembacaan yang lebih akurat.

Sayangnya, tidak ada koneksi linear antara tegangan dan pengisian daya baterai. Ketika muatan berkurang, tegangan turun, ketika meningkat, itu naik, tetapi dengan cara yang tidak terduga. Semua itu dapat dikatakan: jika tegangan di bawah sekitar 3,6 V, maka baterai habis, tetapi sangat sulit untuk secara akurat mengubah tingkat tegangan menjadi persentase dari muatan baterai.

Untuk proyek kami, ini tidak terlalu penting. Kami dapat menerapkan perkiraan kasar untuk memberi tahu pemain tentang perlunya mengisi daya perangkat dengan cepat, tetapi kami tidak akan menderita, mencoba untuk mendapatkan persentase yang tepat.

Status LED



Pada panel depan di bawah layar Odroid Go ada LED biru (LED), yang dapat kita gunakan untuk tujuan apa pun. Anda dapat menunjukkan kepada mereka bahwa perangkat dihidupkan dan berfungsi, tetapi dalam kasus ini, saat bermain dalam gelap, LED biru cerah akan bersinar di wajah Anda. Oleh karena itu, kami akan menggunakannya untuk menunjukkan daya baterai rendah (meskipun saya lebih suka warna merah atau kuning untuk ini).

Untuk menggunakan LED, Anda perlu mengatur IO2 sebagai output, dan kemudian menerapkan sinyal tinggi atau rendah untuk menyalakan dan mematikan LED.

Saya pikir bahwa resistor 2 kฮฉ ( resistor pembatas arus ) akan cukup sehingga kita tidak membakar LED dan memasok terlalu banyak arus dari pin GPIO.

LED memiliki resistansi yang agak rendah, jadi jika 3.3 V diterapkan, maka kita akan membakarnya dengan mengubah arus. Untuk melindungi dari ini, sebuah resistor biasanya dihubungkan secara seri dengan LED.

Namun, resistor pembatas arus untuk LED biasanya jauh lebih kecil dari 2 kฮฉ, jadi saya tidak mengerti mengapa resistor R7 adalah hambatan seperti itu.

Inisialisasi


static const adc1_channel_t BATTERY_READ_PIN = ADC1_GPIO36_CHANNEL;
static const gpio_num_t BATTERY_LED_PIN = GPIO_NUM_2;

static esp_adc_cal_characteristics_t gCharacteristics;

void Odroid_InitializeBatteryReader()
{
	// Configure LED
	{
		gpio_config_t gpioConfig = {};

		gpioConfig.mode = GPIO_MODE_OUTPUT;
		gpioConfig.pin_bit_mask = 1ULL << BATTERY_LED_PIN;

		ESP_ERROR_CHECK(gpio_config(&gpioConfig));
	}

	// Configure ADC
	{
		adc1_config_width(ADC_WIDTH_BIT_12);
    	adc1_config_channel_atten(BATTERY_READ_PIN, ADC_ATTEN_DB_11);
    	adc1_config_channel_atten(BATTERY_READ_PIN, ADC_ATTEN_DB_11);

    	esp_adc_cal_value_t type = esp_adc_cal_characterize(
    		ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &gCharacteristics);

    	assert(type == ESP_ADC_CAL_VAL_EFUSE_VREF);
    }

	ESP_LOGI(LOG_TAG, "Battery reader initialized");
}

Pertama-tama kita mengatur LED GPIO sebagai output sehingga kita dapat mengubahnya jika perlu. Kemudian kita mengkonfigurasi pin ADC, seperti yang kita lakukan dalam kasus cross - dengan lebar bit 12 dan atenuasi minimal.

esp_adc_cal_characterize melakukan penghitungan bagi kita untuk mengkarakterisasi ADC sehingga kita nanti dapat mengubah pembacaan digital menjadi tekanan fisik.

Baca Baterai


uint32_t Odroid_ReadBatteryLevel(void)
{
	const int SAMPLE_COUNT = 20;


	uint32_t raw = 0;

	for (int sampleIndex = 0; sampleIndex < SAMPLE_COUNT; ++sampleIndex)
	{
		raw += adc1_get_raw(BATTERY_READ_PIN);
	}

	raw /= SAMPLE_COUNT;


	uint32_t voltage = 2 * esp_adc_cal_raw_to_voltage(raw, &gCharacteristics);

	return voltage;
}

Kami mengambil dua puluh sampel mentah ADC dari kontak ADC, dan kemudian membaginya untuk mendapatkan nilai rata-rata. Seperti disebutkan di atas, ini membantu mengurangi kebisingan bacaan.

Kemudian kita menggunakan esp_adc_cal_raw_to_voltage untuk mengkonversi nilai mentah ke tegangan nyata. Karena pembagi tegangan yang disebutkan di atas, kami menggandakan nilai kembali: nilai baca akan menjadi setengah dari tegangan baterai yang sebenarnya.

Alih-alih menghasilkan cara-cara rumit untuk mengubah tegangan ini menjadi persentase daya baterai, kami akan mengembalikan tegangan sederhana. Biarkan fungsi panggilan memutuskan sendiri apa yang harus dilakukan dengan voltase - apakah mengubahnya menjadi persentase muatan, atau hanya menafsirkannya sebagai nilai tinggi atau rendah.

Nilai dikembalikan dalam milivolt, sehingga fungsi panggilan perlu melakukan konversi yang sesuai. Ini mencegah float overflow.

Pengaturan LED


void Odroid_EnableBatteryLight(void)
{
	gpio_set_level(BATTERY_LED_PIN, 1);
}

void Odroid_DisableBatteryLight(void)
{
	gpio_set_level(BATTERY_LED_PIN, 0);
}

Dua fungsi sederhana ini cukup menggunakan LED. Kita bisa menyalakan atau mematikan lampu. Biarkan fungsi panggilan memutuskan kapan melakukannya.

Kami dapat membuat tugas yang secara berkala akan memonitor tegangan baterai dan karenanya mengubah status LED, tetapi saya sebaiknya menginterogasi tegangan baterai dalam siklus utama kami, dan kemudian memutuskan cara mengatur tegangan baterai dari sana.

Demo


uint32_t batteryLevel = Odroid_ReadBatteryLevel();

if (batteryLevel < 3600)
{
	Odroid_EnableBatteryLight();
}
else
{
	Odroid_DisableBatteryLight();
}

Kami hanya dapat meminta tingkat baterai dalam siklus utama, dan jika voltase di bawah nilai ambang batas, nyalakan LED, yang menunjukkan perlunya pengisian daya. Berdasarkan bahan yang dipelajari, saya dapat mengatakan bahwa 3600 mV (3,6 V) adalah pertanda baik dari baterai lithium-ion yang rendah, tetapi baterainya sendiri kompleks.

Referensi



Bagian 6: suara


Langkah terakhir untuk mendapatkan antarmuka lengkap untuk semua perangkat keras Odroid Go adalah menulis lapisan suara. Setelah selesai dengan ini, kita bisa mulai bergerak ke arah pemrograman yang lebih umum dari gim, kurang terkait dengan pemrograman untuk Odroid. Semua interaksi dengan periferal akan dilakukan melalui fungsi Odroid .

Karena kurangnya pengalaman saya dengan pemrograman suara dan kurangnya dokumentasi yang baik pada bagian IDF, ketika bekerja pada suatu proyek, implementasi suara mengambil paling banyak waktu.

Pada akhirnya, tidak begitu banyak kode yang diperlukan untuk memutar suara. Sebagian besar waktu dihabiskan untuk bagaimana mengkonversi data audio ke ESP32 yang diinginkan dan cara mengkonfigurasi driver audio ESP32 agar sesuai dengan konfigurasi perangkat keras.

Dasar-Dasar Suara Digital


Suara digital terdiri dari dua bagian: perekaman dan pemutaran .

Merekam


Untuk merekam suara pada komputer, pertama-tama kita perlu mengubahnya dari ruang sinyal kontinyu (analog) menjadi ruang sinyal diskrit (digital). Tugas ini diselesaikan dengan menggunakan konverter analog-ke-digital (ADC) (yang kami bicarakan ketika kami bekerja dengan salib di Bagian 2).

ADC menerima sampel gelombang yang masuk dan mendigitalkan nilainya, yang kemudian dapat disimpan ke file.

Bermain


File suara digital dapat dikembalikan dari ruang digital ke analog menggunakan Digital-to-Analog Converter (DAC) . DAC dapat mereproduksi nilai hanya dalam rentang tertentu. Misalnya, DAC 8-bit dengan sumber 3,3 V dapat menampilkan tegangan analog dalam kisaran dari 0 hingga 3,3 mV dalam langkah 12,9 mV (3,3 V dibagi dengan 256).

DAC mengambil nilai digital dan mengubahnya kembali menjadi tegangan, yang dapat ditransmisikan ke amplifier, speaker, atau perangkat lain yang mampu menerima sinyal audio analog.

Tingkat pengambilan sampel


Saat merekam suara analog melalui ADC, sampel diambil pada frekuensi tertentu, dan setiap sampel adalah "potret" sinyal suara pada suatu titik waktu. Parameter ini disebut frekuensi sampling dan diukur dalam hertz .

Semakin tinggi frekuensi pengambilan sampel, semakin akurat kita menciptakan frekuensi sinyal asli. Teorema Nyquist-Shannon (Kotelnikov) menyatakan (secara sederhana) bahwa frekuensi sampling harus dua kali frekuensi sinyal tertinggi yang ingin kami rekam.

Telinga manusia dapat mendengar kira-kira dalam kisaran dari 20 Hz hingga 20 kHz , sehingga frekuensi pengambilan sampel 44,1 kHz paling sering digunakan untuk menciptakan kembali musik berkualitas tinggi., Yang sedikit lebih dari dua kali frekuensi maksimum yang dapat dikenali telinga manusia. Ini memastikan bahwa satu set lengkap frekuensi instrumen dan suara akan diciptakan kembali.

Namun, setiap sampel membutuhkan ruang dalam file, jadi kami tidak dapat memilih tingkat pengambilan sampel maksimum. Namun, jika Anda tidak mengambil sampel dengan cukup cepat, Anda dapat kehilangan informasi penting. Frekuensi pengambilan sampel yang dipilih harus bergantung pada frekuensi yang ada dalam suara yang diciptakan kembali.

Pemutaran ulang harus dilakukan pada frekuensi pengambilan sampel yang sama dengan sumbernya, jika tidak suara dan durasinya akan berbeda.

Misalkan sepuluh detik suara direkam pada frekuensi sampling 16 kHz. Jika Anda memainkannya dengan frekuensi 8 kHz, maka nadanya akan lebih rendah, dan durasinya akan menjadi dua puluh detik. Jika Anda memainkannya dengan frekuensi sampling 32 kHz, maka nada yang terdengar akan lebih tinggi, dan suara itu sendiri akan bertahan lima detik.

Video ini menunjukkan perbedaan dalam tingkat sampel dengan contoh.

Kedalaman bit


Frekuensi pengambilan sampel hanya setengah dari persamaan. Suara juga memiliki kedalaman bit , yaitu jumlah bit per sampel.

Ketika ADC menangkap sampel sinyal audio, ADC harus mengubah nilai analog ini menjadi digital, dan kisaran nilai yang ditangkap tergantung pada jumlah bit yang digunakan. 8 bit (nilai 256), 16 bit (nilai 65.526), โ€‹โ€‹32 bit (4.294.967.296 nilai), dll.

Jumlah bit per sampel terkait dengan rentang dinamis suara, mis. dengan bagian paling keras dan paling tenang. Kedalaman bit yang paling umum untuk musik adalah 16 bit.

Selama pemutaran, perlu untuk memberikan kedalaman bit yang sama dengan sumbernya, jika tidak suara dan durasinya akan berubah.

Misalnya, Anda memiliki file audio dengan empat sampel disimpan sebagai 8 bit: [0x25, 0xAB, 0x34, 0x80]. Jika Anda mencoba memainkannya seolah-olah 16-bit, Anda hanya akan mendapatkan dua sampel: [0x25AB, 0x3480]. Ini tidak hanya akan menyebabkan nilai sampel suara yang salah, tetapi juga mengurangi separuh jumlah sampel, dan karenanya durasi suara.

Penting juga untuk mengetahui format sampel. 8-bit unsigned, 8-bit unsigned, 16-bit unsigned, 16-bit unsigned, dll. Biasanya 8-bit tidak ditandatangani, dan 16-bit ditandatangani. Jika mereka bingung, suara akan sangat terdistorsi.

Video ini menunjukkan perbedaan kedalaman bit dengan contoh.

File WAV


Paling sering, data audio mentah pada komputer disimpan dalam format WAV , yang memiliki header sederhana yang menggambarkan format suara (frekuensi sampling, kedalaman bit, ukuran, dll.), Diikuti oleh data audio itu sendiri.

Suara tidak dikompresi sama sekali (tidak seperti format seperti MP3), jadi kita dapat dengan mudah memainkannya tanpa perlu perpustakaan codec.

Masalah utama dengan file WAV adalah karena kurangnya kompresi, mereka bisa sangat besar. Ukuran file secara langsung terkait dengan durasi, laju sampling, dan kedalaman bit.

Ukuran = Durasi (dalam detik) x Laju Pengambilan Sampel (sampel / s) x Kedalaman Bit (bit / sampel)

Frekuensi pengambilan sampel paling banyak memengaruhi ukuran file, sehingga cara termudah untuk menghemat ruang adalah dengan memilih nilai yang cukup rendah. Kami akan membuat suara old-school, jadi frekuensi pengambilan sampel yang rendah cocok untuk kami.

I2S


ESP32 memiliki periferal, karena itu relatif sederhana untuk menyediakan antarmuka dengan peralatan audio: Inter-IC Sound (I2S) .

Protokol I2S cukup sederhana dan hanya terdiri dari tiga sinyal: sinyal jam, pilihan saluran (kiri atau kanan), dan juga jalur data itu sendiri.

Frekuensi jam tergantung pada frekuensi pengambilan sampel, kedalaman bit, dan jumlah saluran. Ketukan diganti untuk setiap bit data, oleh karena itu, untuk reproduksi suara yang tepat, Anda harus mengatur frekuensi jam yang sesuai.

Frekuensi jam = Frekuensi pengambilan sampel (sampel / s) x Kedalaman bit (bit / sampel) x Jumlah saluran

Driver I2S mikrokontroler ESP32 memiliki dua mode yang memungkinkan: dapat mengeluarkan data ke kontak yang terhubung ke penerima I2S eksternal, yang dapat men-decode protokol dan mentransfer data ke amplifier, atau dapat mentransfer data ke ESP32 DAC internal yang menghasilkan sinyal analog yang dapat ditransmisikan ke penguat.

Odroid Go tidak memiliki decoder I2S di papan tulis, jadi kita harus menggunakan ESP32 DAC 8-bit internal, yaitu, kita harus menggunakan suara 8-bit. Perangkat ini memiliki dua DAC, satu terhubung ke IO25 , yang lainnya ke IO26 .

Prosedurnya terlihat seperti ini:

  1. Kami mentransfer data audio ke driver I2S
  2. Driver I2S mengirimkan data audio ke DAC internal 8-bit
  3. DAC internal menghasilkan sinyal analog
  4. Sinyal analog ditransmisikan ke penguat suara



Jika kita melihat sirkuit audio di sirkuit Odroid Go , kita akan melihat dua pin GPIO ( IO25 dan IO26 ) yang terhubung ke input penguat suara ( PAM8304A ). IO25
juga terhubung ke sinyal / SD amplifier, yaitu kontak yang menghidupkan atau mematikan amplifier (sinyal rendah berarti shutdown). Keluaran amplifier terhubung ke satu speaker ( P1 ).

Ingat bahwa IO25 dan IO26 adalah output dari 8-bit ESP32 DACs, yaitu, satu DAC terhubung ke IN- dan yang lainnya ke IN + .

IN- dan IN + adalahinput diferensial dari penguat suara. Input diferensial digunakan untuk mengurangi noise yang disebabkan oleh interferensi elektromagnetik . Setiap noise yang ada dalam satu sinyal juga akan hadir di yang lain. Satu sinyal dikurangi dari yang lain, yang menghilangkan noise.

Jika Anda melihat spesifikasi penguat suara , maka ia memiliki Khas Aplikasi Sirkuit , yang merupakan cara yang direkomendasikan pabrikan untuk menggunakan penguat.


Dia merekomendasikan menghubungkan IN- ke ground, IN + ke sinyal input, dan / SD ke sinyal on / off. Jika ada suara 0,005 V, maka dengan IN- 0V + 0,005V akan dibaca , dan dengan IN + - VIN + 0,005V . Sinyal input harus dikurangkan satu sama lain dan mendapatkan nilai sinyal yang sebenarnya ( VIN ) tanpa noise.

Namun, para perancang Odroid Go tidak menggunakan konfigurasi yang disarankan.

Sekali lagi melihat sirkuit Odroid Go, kita melihat bahwa desainer menghubungkan output DAC ke IN- dan bahwa output DAC yang sama terhubung ke / SD . / SD- Ini adalah sinyal penonaktifan dengan level rendah aktif, jadi agar amplifier bekerja, Anda perlu mengatur sinyal tinggi.

Ini berarti bahwa untuk menggunakan amplifier, kita tidak boleh menggunakan IO25 sebagai DAC, tetapi sebagai keluaran GPIO dengan sinyal yang selalu tinggi. Namun, dalam kasus ini, sinyal tinggi diatur ke IN- , yang tidak direkomendasikan oleh spesifikasi amplifier (harus di-ground). Maka kita harus menggunakan DAC yang terhubung ke IO26 , karena output I2S kita harus dimasukkan ke IN + . Ini berarti bahwa kami tidak akan mencapai pengurangan kebisingan yang diperlukan, karena IN- tidak terhubung ke ground. Suara lembut terus-menerus keluar dari speaker.

Kami perlu memastikan konfigurasi driver I2S yang benar, karena kami hanya ingin menggunakan DAC yang terhubung ke IO26 . Jika kami menggunakan DAC yang terhubung ke IO25 , itu akan terus-menerus mematikan sinyal amplifier, dan suaranya akan mengerikan.

Selain keanehan ini, ketika menggunakan DAC internal 8-bit, driver I2S di ESP32 memerlukan sampel 16-bit untuk ditransmisikan ke sana, tetapi hanya mengirimkan byte tinggi ke DAC 8-bit. Oleh karena itu, kita perlu mengambil suara 8-bit kami dan menempelkannya ke buffer dua kali lebih besar, sementara buffer akan menjadi setengah kosong. Kemudian kami meneruskannya ke driver I2S dan melewati DAC byte tinggi dari setiap sampel. Sayangnya, ini berarti bahwa kita harus "membayar" untuk 16 bit, tetapi kita hanya bisa menggunakan 8 bit.

Multitasking


Sayangnya, permainan tidak dapat bekerja pada satu inti, seperti yang saya inginkan, karena sepertinya ada bug pada driver I2S.

Driver I2S harus menggunakan DMA (seperti driver SPI), yaitu, kita bisa saja memulai transfer I2S, dan kemudian melanjutkan pekerjaan kita sementara driver I2S mentransmisikan data audio.

Tetapi sebaliknya, CPU diblokir selama durasi suara, yang sama sekali tidak cocok untuk permainan. Bayangkan Anda menekan tombol lompat, dan kemudian sprite pemain menghentikan gerakannya selama 100 ms saat suara lompat diputar.

Untuk mengatasi masalah ini, kita dapat memanfaatkan fakta bahwa ada dua inti di papan ESP32. Kita dapat membuat tugas (mis. Utas) di inti kedua, yang akan menangani reproduksi suara. Berkat ini, kami dapat mentransfer pointer ke buffer suara dari tugas utama game ke tugas suara, dan tugas suara memulai transfer I2S dan diblokir selama durasi pemutaran suara. Tetapi tugas utama pada inti pertama (dengan pemrosesan input dan rendering) akan terus dieksekusi tanpa memblokir.

Inisialisasi


Mengetahui hal ini, kita dapat memulai driver I2S dengan benar. Untuk melakukan ini, Anda hanya perlu beberapa baris kode, tetapi kesulitannya adalah untuk mengetahui parameter apa yang perlu Anda set untuk reproduksi suara yang tepat.

static const gpio_num_t AUDIO_AMP_SD_PIN = GPIO_NUM_25;

static QueueHandle_t gQueue;

static void PlayTask(void *arg)
{
	for(;;)
	{
		QueueData data;

		if (xQueueReceive(gQueue, &data, 10))
		{
			size_t bytesWritten;
			i2s_write(I2S_NUM_0, data.buffer, data.length, &bytesWritten, portMAX_DELAY);
			i2s_zero_dma_buffer(I2S_NUM_0);
		}

		vTaskDelay(1 / portTICK_PERIOD_MS);
	}
}

void Odroid_InitializeAudio(void)
{
	// Configure the amplifier shutdown signal
	{
		gpio_config_t gpioConfig = {};

		gpioConfig.mode = GPIO_MODE_OUTPUT;
		gpioConfig.pin_bit_mask = 1ULL << AUDIO_AMP_SD_PIN;

		ESP_ERROR_CHECK(gpio_config(&gpioConfig));

		gpio_set_level(AUDIO_AMP_SD_PIN, 1);
	}

	// Configure the I2S driver
	{
		i2s_config_t i2sConfig= {};

		i2sConfig.mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN;
		i2sConfig.sample_rate = 5012;
		i2sConfig.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
		i2sConfig.communication_format = I2S_COMM_FORMAT_I2S_MSB;
		i2sConfig.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
		i2sConfig.dma_buf_count = 8;
		i2sConfig.dma_buf_len = 64;

		ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2sConfig, 0, NULL));
		ESP_ERROR_CHECK(i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN));
	}

	// Create task for playing sounds so that our main task isn't blocked
	{
		gQueue = xQueueCreate(1, sizeof(QueueData));
		assert(gQueue);

		BaseType_t result = xTaskCreatePinnedToCore(&PlayTask, "I2S Task", 1024, NULL, 5, NULL, 1);
		assert(result == pdPASS);
	}
}

Pertama, kami mengonfigurasi IO25 (yang terhubung ke sinyal turn-off amplifier) โ€‹โ€‹sebagai output sehingga dapat mengontrol amplifier suara, dan menerapkan sinyal tinggi untuk mengaktifkan amplifier.

Selanjutnya, kita mengkonfigurasi dan menginstal driver I2S itu sendiri. Saya akan menguraikan setiap bagian dari konfigurasi baris demi baris, karena masing-masing baris memerlukan penjelasan:

  • mode
    • kami menetapkan driver sebagai master (mengendalikan bus), pemancar (karena kami mentransfer data ke penerima), dan mengkonfigurasinya untuk menggunakan DAC 8-bit bawaan (karena papan Odroid Go tidak memiliki DAC eksternal).
  • sample_rate
    • 5012, , , . , , . -, 2500 .
  • bits_per_sample
    • , ESP32 8-, I2S , 16 , 8 .
  • communication_format
    • , , - , 8- 16- .
  • channel_format
    • GPIO, IN+ โ€” IO26, ยซยป I2S. , I2S , IO25, , .
  • dma_buf_count dma_buf_len
    • DMA- ( ) , , , IDF. , .

Lalu kami membuat antrian - ini adalah cara FreeRTOS mengirim data di antara tugas-tugas. Kami menempatkan data dalam antrian satu tugas dan mengekstraknya dari antrian tugas lain. Buat struct yang disebut QueueData yang menggabungkan pointer ke buffer suara dan panjang buffer ke dalam struktur tunggal yang dapat antri.

Selanjutnya, buat tugas yang berjalan di inti kedua. Kami menghubungkannya ke fungsi PlayTask , yang melakukan pemutaran suara. Tugas itu sendiri adalah loop tanpa akhir yang terus-menerus memeriksa untuk melihat apakah ada data dalam antrian. Jika ya, ia mengirimnya ke driver I2S agar dapat dimainkan. Ini akan memblokir panggilan i2s_write, dan ini cocok untuk kami, karena tugas dilakukan pada kernel yang terpisah dari utas utama permainan.

Panggilan ke i2s_zero_dma_buffer diperlukan sehingga setelah pemutaran selesai tidak ada suara yang tersisa dari speaker. Saya tidak tahu apakah ini bug dari driver I2S atau perilaku yang diharapkan, tetapi tanpa itu, setelah buffer suara selesai diputar, speaker mengeluarkan sinyal sampah.

Memainkan suara


void Odroid_PlayAudio(uint16_t* buffer, size_t length)
{
	QueueData data = {};

	data.buffer = buffer;
	data.length = length;

	xQueueSendToBack(gQueue, &data, portMAX_DELAY);
}

Karena fakta bahwa seluruh konfigurasi telah selesai, panggilan ke fungsi pemutaran buffer suara itu sendiri sangat sederhana, karena pekerjaan utama dilakukan pada tugas lain. Kami menempatkan pointer ke buffer dan panjang buffer ke dalam struktur QueueData , dan kemudian memasukkannya ke dalam antrian yang digunakan oleh fungsi PlayTask .

Karena pola operasi ini, satu buffer suara harus menyelesaikan pemutaran sebelum dapat memulai buffer kedua. Oleh karena itu, jika lompatan dan pemotretan terjadi secara bersamaan, suara pertama akan dimainkan sebelum yang kedua, dan tidak bersamaan dengan itu.

Kemungkinan besar, di masa depan saya akan mencampur suara bingkai yang berbeda ke buffer suara yang ditransmisikan ke driver I2S. Ini akan memungkinkan Anda untuk memutar beberapa suara sekaligus.

Demo


Kami akan menghasilkan efek suara kami sendiri menggunakan jsfxr , alat yang dirancang khusus untuk menghasilkan jenis suara permainan yang kami butuhkan. Kita dapat langsung mengatur frekuensi sampling dan kedalaman bit, dan kemudian menampilkan file WAV.

Saya menciptakan efek suara lompatan sederhana yang menyerupai suara lompatan Mario. Ini memiliki frekuensi sampling 5012 (seperti yang kami konfigurasikan selama inisialisasi) dan kedalaman 8 bit (karena DAC adalah 8-bit).


Alih-alih mengurai file WAV langsung dalam kode, kami akan melakukan sesuatu yang mirip dengan apa yang kami lakukan untuk memuat sprite dalam demo Bagian 4: kami akan menghapus header WAV dari file menggunakan hex editor. Berkat ini, file yang dibaca dari kartu SD hanya akan menjadi data mentah. Juga, kita tidak akan membaca durasi suara, kita akan menuliskannya dalam kode. Di masa depan, kami akan memuat sumber daya suara secara berbeda, tetapi ini sudah cukup untuk demo.

File mentah dapat diunduh dari sini .

// Load sound effect
uint16_t* soundBuffer;
int soundEffectLength = 1441;
{
	FILE* soundFile = fopen("/sdcard/jump", "r");
	assert(soundFile);

	uint8_t* soundEffect = malloc(soundEffectLength);
	assert(soundEffect);

	soundBuffer = malloc(soundEffectLength*2);
	assert(soundBuffer);

	fread(soundEffect, soundEffectLength, 1, soundFile);

    for (int i = 0; i < soundEffectLength; ++i)
    {
        // 16 bits required but only MSB is actually sent to the DAC
        soundBuffer[i] = (soundEffect[i] << 8u);
    }
}

Kami memuat data 8-bit ke dalam buffer soundEffect 8-bit , dan kemudian menyalin data ini ke buffer soundBuffer 16-bit , di mana data akan disimpan dalam delapan bit tinggi. Saya ulangi - ini perlu karena fitur implementasi IDF.

Setelah membuat buffer 16-bit, kita dapat memutar suara klik tombol. Adalah logis untuk menggunakan tombol volume untuk ini.

int lastState = 0;

for (;;)
{
	[...]

	int thisState = input.volume;

	if ((thisState == 1) && (thisState != lastState))
	{
		Odroid_PlayAudio(soundBuffer, soundEffectLength*2);
	}

	lastState = thisState;

	[...]
}

Kami memantau keadaan tombol sehingga secara tidak sengaja, dengan satu klik tombol, Anda tidak sengaja memanggil Odroid_PlayAudio beberapa kali.


Sumber


Semua kode sumber ada di sini .

Referensi



All Articles