برمجة لعبة لجهاز مضمن على ESP32: محرك ، بطارية ، صوت

صورة


ابدأ: نظام التجميع ، الإدخال ، العرض .

الجزء 4: القيادة


يحتوي Odroid Go على فتحة لبطاقة microSD ، والتي ستكون مفيدة لتنزيل الموارد (العفاريت وملفات الصوت والخطوط) ، وربما حتى لحفظ حالة اللعبة.

يتم توصيل قارئ البطاقة عبر SPI ، ولكن IDF يجعل من السهل التفاعل مع بطاقة SD عن طريق تلخيص مكالمات SPI واستخدام وظائف POSIX القياسية مثل fopen و fread و fwrite . كل هذا يعتمد على مكتبة FatFs ، لذا يجب تهيئة بطاقة SD بتنسيق FAT القياسي.

وهو متصل بنفس ناقل SPI لشاشة LCD ، ولكنه يستخدم خط اختيار رقاقة مختلفًا. عندما نحتاج إلى القراءة أو الكتابة إلى بطاقة SD (وهذا لا يحدث كثيرًا) ، سيقوم برنامج تشغيل SPI بتبديل إشارة CS من الشاشة إلى قارئ بطاقة SD ، ثم إجراء العملية. هذا يعني أنه أثناء إرسال البيانات إلى الشاشة ، لا يمكننا إجراء أي عمليات باستخدام بطاقة SD ، والعكس صحيح.

في الوقت الحالي ، نقوم بكل شيء في سلسلة واحدة ونستخدم حظر الإرسال عبر SPI على الشاشة ، لذلك لا يمكن أن تكون هناك معاملات متزامنة مع بطاقة SD ومع شاشة LCD. على أي حال ، هناك احتمال كبير بأن نقوم بتحميل جميع الموارد في وقت الإطلاق.

تعديل ESP-IDF


إذا حاولنا تهيئة واجهة بطاقة SD بعد تهيئة الشاشة ، فسوف نواجه مشكلة تجعل من المستحيل تحميل Odroid Go. لا يدعم ESP-IDF v4.0 الوصول المشترك إلى ناقل SPI عند استخدامه مع بطاقة SD. في الآونة الأخيرة ، أضاف المطورون هذه الوظيفة ، ولكن لم يتم إصدارها بعد بشكل مستقر ، لذلك سنقوم بتعديل صغير لجيش الدفاع الإسرائيلي بأنفسنا.

التعليق خارج السطر 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;
}

بعد إجراء هذا التغيير ، سنستمر في رؤية خطأ أثناء التهيئة ، ولكنه لن يتسبب في إعادة تشغيل ESP32 ، لأن رمز الخطأ لا ينتشر أعلاه.

التهيئة




نحتاج إلى إخبار IDF عن دبابيس ESP32 المتصلة بقارئ MicroSD بحيث يقوم بتكوين برنامج تشغيل SPI الأساسي ، والذي يتصل فعليًا بالقارئ.

يتم استخدام الملاحظات العامة VSPI.XXXX مرة أخرى في الرسم التخطيطي ، ولكن يمكننا أن نراجعها إلى أرقام الاتصال الفعلية على ESP32.

يشبه التهيئة تهيئة شاشة LCD ، ولكن بدلاً من هيكل تكوين SPI العام ، نستخدم sdspi_slot_config_t ، المصمم لبطاقة SD متصلة عبر ناقل SPI. نقوم بتهيئة أرقام الاتصال المقابلة وخصائص تركيب البطاقة في نظام FatFS.

لا توصي وثائق جيش الدفاع الإسرائيلي باستخدام الدالة esp_vfs_fat_sdmmc_mountفي كود البرنامج النهائي. هذه وظيفة مجمعة تؤدي الكثير من العمليات بالنسبة لنا ، لكنها تعمل حتى الآن بشكل طبيعي ، وربما لن يتغير شيء في المستقبل. تعيّن

المعلمة "/ sdcard" لهذه الوظيفة نقطة التثبيت الافتراضية لبطاقة SD ، والتي سنستخدمها بعد ذلك كبادئة عند العمل مع الملفات. إذا كان لدينا ملف باسم "test.txt" على بطاقة SD الخاصة بنا ، فإن المسار الذي سنستخدمه للارتباط به سيكون "/sdcard/test.txt".

بعد تهيئة واجهة بطاقة SD ، يصبح التفاعل مع الملفات تافها: يمكننا ببساطة استخدام المكالمات القياسية لوظائف POSIX ، وهو أمر مريح للغاية.

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



لقد أنشأت نقشًا متحركًا 64 × 64 في Aseprite (رهيب) يستخدم لونين فقط: أسود تمامًا (معطل البكسل) وأبيض تمامًا (تم تمكين البكسل). ليس لدى Aseprite خيار حفظ لون RGB565 أو تصديره كصورة نقطية أولية (أي بدون ضغط ورؤوس صور) ، لذلك قمت بتصدير العفريت إلى تنسيق PNG مؤقت.

ثم ، باستخدام ImageMagick ، ​​قمت بتحويل البيانات إلى ملف PPM ، والذي حول الصورة إلى بيانات أولية غير مضغوطة برأس بسيط. بعد ذلك ، فتحت الصورة في محرر سداسي ، وحذفت الرأس وحولت لون 24 بت إلى 16 بت ، وحذف جميع التكرارات من 0x000000 إلى 0x0000 ، وجميع التكرارات من 0xFFFFFF إلى 0xFFFF. ترتيب البايت هنا ليس مشكلة ، لأن 0x0000 و 0xFFFF لا يتغيران عند تغيير ترتيب البايت.

يمكن تنزيل الملف الخام من هنا .

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);

أولاً ، نفتح الملف الرئيسي الذي يحتوي على وحدات البايت الخام ونقرأه في المخزن المؤقت. في المستقبل ، سنقوم بتحميل موارد العفريت بشكل مختلف ، ولكن بالنسبة إلى العرض التوضيحي هذا يكفي تمامًا.

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;
}

لرسم نقش متحرك ، نجتاز محتوياته بشكل متكرر. إذا كانت البكسل بيضاء ، فإننا نرسمها باللون الذي تحدده الأزرار. إذا كان أسود ، فنحن نعتبرها خلفية ولا نرسم.


كاميرا هاتفي تشوه الألوان بشكل كبير. وآسف على هزها.

لاختبار تسجيل الصورة ، سننقل المفتاح إلى مكان ما على الشاشة ، ونغير لونه ، ثم نكتب مخزن الإطارات المؤقت على بطاقة SD بحيث يمكن عرضه على الكمبيوتر.

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);
}

يؤدي الضغط على مفتاح القائمة إلى حفظ محتويات المخزن المؤقت للإطارات في ملف يسمى Framebuf . سيكون هذا مخزنًا مؤقتًا للإطار الخام ، لذلك ستظل وحدات البكسل بتنسيق RGB565 مع عكس ترتيب البايت. يمكننا مرة أخرى استخدام ImageMagick لتحويل هذا التنسيق إلى PNG لعرضه على جهاز كمبيوتر.

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

بالطبع ، يمكننا تنفيذ القراءة / الكتابة بتنسيق BMP / PNG والتخلص من كل هذه الضجة مع ImageMagick ، ​​ولكن هذا مجرد رمز تجريبي. حتى الآن لم أقرر تنسيق الملف الذي أريد استخدامه لتخزين النقوش المتحركة.


ها هو! يتم عرض المخزن المؤقت لإطار Odroid Go على كمبيوتر سطح المكتب.

المراجع



الجزء 5: البطارية


يحتوي Odroid Go على بطارية ليثيوم أيون ، حتى نتمكن من إنشاء لعبة يمكنك لعبها أثناء التنقل. هذه فكرة مغرية لشخص لعب أول Gameboy عندما كان طفلاً.

لذلك ، نحتاج إلى طريقة لطلب مستوى بطارية Odroid Go. البطارية متصلة بجهة الاتصال على ESP32 ، حتى نتمكن من قراءة الجهد للحصول على فكرة تقريبية عن وقت التشغيل المتبقي.

مخطط



يوضح الرسم البياني IO36 متصلاً بجهد VBAT بعد سحبه إلى الأرض من خلال المقاوم. مقاومتان ( R21 و R23 ) يشكلان مقسم جهد مشابه لذلك الذي يستخدم على صليب لوحة الألعاب. المقاومات لها نفس المقاومة مرة أخرى بحيث يكون الجهد نصف الأصلي.

بسبب مقسم الجهد ، سوف يقرأ IO36 جهدًا يساوي نصف VBAT . من المحتمل أن يتم ذلك لأن جهات اتصال ADC على ESP32 لا يمكنها قراءة الجهد العالي لبطارية ليثيوم أيون (4.2 فولت عند الشحن الأقصى). مع ذلك ، هذا يعني أنه للحصول على الجهد الحقيقي ، تحتاج إلى مضاعفة الجهد المقروء من ADC (ADC).

عند قراءة قيمة IO36 ، نحصل على قيمة رقمية ، لكننا نفقد القيمة التناظرية التي تمثلها. نحن بحاجة إلى طريقة لتفسير القيمة الرقمية باستخدام ADC في شكل جهد تناظري مادي.

يسمح لك IDF بمعايرة ADC ، الذي يحاول إعطاء مستوى الجهد بناءً على الجهد المرجعي. هذا الجهد المرجعي ( Vref ) هو 1100 mV افتراضيًا ، ولكن نظرًا للخصائص المادية ، يختلف كل جهاز قليلاً. يحتوي ESP32 في Odroid Go على Vref محدد يدويًا ، "flashed" في eFuse ، والذي يمكننا استخدامه ك Vref أكثر دقة.

سيكون الإجراء كما يلي: أولاً ، سنقوم بتكوين معايرة ADC ، وعندما نريد قراءة الجهد ، سنأخذ عددًا معينًا من العينات (على سبيل المثال ، 20) لحساب متوسط ​​القراءات ؛ ثم نستخدم الجيش الإسرائيلي لتحويل هذه القراءات إلى الجهد. حساب المتوسط ​​يزيل الضوضاء ويعطي قراءات أكثر دقة.

لسوء الحظ ، لا يوجد اتصال خطي بين الجهد وشحن البطارية. عندما تنخفض الشحنة ، ينخفض ​​الجهد ، عندما يزيد ، يرتفع ، ولكن بطريقة لا يمكن التنبؤ بها. كل ما يمكن قوله: إذا كان الجهد أقل من حوالي 3.6 فولت ، عندها يتم تفريغ البطارية ، ولكن من الصعب بشكل مدهش تحويل مستوى الجهد بدقة إلى نسبة مئوية من شحن البطارية.

بالنسبة لمشروعنا ، هذا ليس مهمًا بشكل خاص. يمكننا تنفيذ تقريب تقريبي للسماح للاعب بمعرفة الحاجة إلى شحن الجهاز بسرعة ، لكننا لن نعاني ، في محاولة للحصول على النسبة الدقيقة.

LED الحالة



يوجد على اللوحة الأمامية أسفل شاشة Odroid Go مؤشر LED أزرق (LED) ، والذي يمكننا استخدامه لأي غرض. يمكنك أن تبين لهم أن الجهاز قيد التشغيل ويعمل ، ولكن في هذه الحالة ، عندما يلعب في الظلام ، سوف يضيء مؤشر LED أزرق في وجهك. لذلك ، سنستخدمها للإشارة إلى انخفاض شحن البطارية (على الرغم من أنني أفضل اللون الأحمر أو الكهرماني لهذا).

لاستخدام LED ، تحتاج إلى تعيين IO2 كمخرج ، ثم تطبيق إشارة عالية أو منخفضة عليه لتشغيل وإيقاف LED.

أعتقد أن المقاوم 2 كيلو أوم ( المقاوم المحدد الحالي ) سيكون كافيًا حتى لا نحرق LED ونوفر تيارًا كبيرًا جدًا من دبوس GPIO.

يتمتع LED بمقاومة منخفضة إلى حد ما ، لذلك إذا تم تطبيق 3.3 V عليه ، فسوف نحرقه عن طريق تغيير التيار. للحماية من هذا ، عادة ما يتم توصيل المقاوم في سلسلة مع LED.

ومع ذلك ، فإن المقاومات المقيدة الحالية لمصابيح LED عادة ما تكون أقل بكثير من 2 كيلو أوم ، لذلك لا أفهم لماذا يعتبر المقاوم R7 مثل هذه المقاومة.

التهيئة


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");
}

أولاً ، نقوم بتعيين GPIO LED كمخرج حتى نتمكن من تبديله إذا لزم الأمر. ثم نقوم بتكوين دبوس ADC ، كما فعلنا في حالة التقاطع - بعرض بت 12 وحد أدنى من التوهين. يقوم

esp_adc_cal_characterize بإجراء حسابات لنا لوصف ADC حتى نتمكن لاحقًا من تحويل القراءات الرقمية إلى إجهاد مادي.

قراءة البطارية


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;
}

نأخذ عشرين عينة أولية من ADC من اتصال ADC ، ثم نقسمها للحصول على متوسط ​​القيمة. كما ذكر أعلاه ، هذا يساعد على تقليل ضجيج القراءات.

ثم نستخدم esp_adc_cal_raw_to_voltage لتحويل القيمة الخام إلى الجهد الحقيقي. نظرًا لمقسم الجهد المذكور أعلاه ، نقوم بمضاعفة القيمة المرتجعة: ستكون قيمة القراءة نصف جهد البطارية الفعلي.

بدلاً من التوصل إلى طرق صعبة لتحويل هذا الجهد إلى نسبة مئوية من شحن البطارية ، فسنعيد جهدًا بسيطًا. دع وظيفة الاستدعاء تقرر بنفسها ما يجب القيام به مع الجهد - سواء لتحويله إلى نسبة مئوية من الشحنة ، أو ببساطة تفسيرها على أنها قيمة عالية أو منخفضة.

يتم إرجاع القيمة بالمللي فولت ، لذا تحتاج وظيفة الاستدعاء إلى إجراء التحويل المناسب. هذا يمنع تعويم الفائض.

إعداد LED


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

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

هاتان الوظيفتان البسيطتان تكفيان لاستخدام LED. يمكننا إما تشغيل أو إيقاف الضوء. دع وظيفة الاستدعاء تقرر متى تفعل ذلك.

يمكننا إنشاء مهمة من شأنها مراقبة جهد البطارية بشكل دوري وتبديل حالة LED وفقًا لذلك ، ولكن من الأفضل استجواب جهد البطارية في دورتنا الرئيسية ، ومن ثم تحديد كيفية ضبط جهد البطارية من هناك.

تجريبي


uint32_t batteryLevel = Odroid_ReadBatteryLevel();

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

يمكننا ببساطة طلب مستوى البطارية في الدورة الرئيسية ، وإذا كان الجهد أقل من قيمة العتبة ، قم بتشغيل LED ، مشيرًا إلى الحاجة إلى الشحن. استنادًا إلى المواد التي تمت دراستها ، يمكنني القول أن 3600 مللي فولت (3.6 فولت) هي علامة جيدة على انخفاض شحنة بطاريات ليثيوم أيون ، ولكن البطاريات نفسها معقدة.

المراجع



الجزء 6: الصوت


الخطوة الأخيرة للحصول على واجهة كاملة لجميع أجهزة Odroid Go هي كتابة طبقة صوت. بعد الانتهاء من هذا ، يمكننا البدء في الانتقال نحو برمجة أكثر عمومية للعبة ، أقل ارتباطًا ببرمجة Odroid. سيتم تنفيذ جميع التفاعلات مع الأجهزة الطرفية من خلال وظائف Odroid .

بسبب قلة خبرتي في البرمجة الصوتية ونقص التوثيق الجيد من جانب جيش الدفاع الإسرائيلي ، عند العمل على مشروع ، استغرق تنفيذ الصوت معظم الوقت.

في النهاية ، لم يكن مطلوبًا الكثير من التعليمات البرمجية لتشغيل الصوت. تم إنفاق معظم الوقت على كيفية تحويل بيانات الصوت إلى ESP32 المطلوب وكيفية تكوين برنامج تشغيل الصوت ESP32 ليتناسب مع تكوين الأجهزة.

أساسيات الصوت الرقمي


ويتكون الصوت الرقمي من جزأين: تسجيل و تشغيل .

سجل


لتسجيل الصوت على جهاز كمبيوتر ، نحتاج أولاً إلى تحويله من مسافة إشارة (تناظرية) مستمرة إلى مساحة إشارة (رقمية) منفصلة. يتم إنجاز هذه المهمة باستخدام محول تناظري إلى رقمي (ADC) (والذي تحدثنا عنه عندما عملنا مع الصليب في الجزء 2).

يتلقى ADC عينة من الموجة الواردة ويرقم القيمة ، والتي يمكن بعد ذلك حفظها في ملف.

لعب


يمكن إرجاع ملف صوتي رقمي من الفضاء الرقمي إلى التناظري باستخدام محول رقمي إلى تناظري (DAC) . يمكن لـ DAC إعادة إنتاج القيم في نطاق معين فقط. على سبيل المثال ، يمكن لـ DAC 8 بت مع مصدر 3.3 فولت إخراج الفولتية التناظرية في النطاق من 0 إلى 3.3 مللي فولت في خطوات 12.9 مللي فولت (3.3 فولت مقسومة على 256).

تأخذ DAC القيم الرقمية وتحولها مرة أخرى إلى الجهد ، والتي يمكن إرسالها إلى مضخم صوت أو مكبر صوت أو أي جهاز آخر قادر على استقبال إشارة صوتية تناظرية.

معدل أخذ العينات


عند تسجيل الصوت التناظري من خلال ADC ، يتم أخذ العينات بتردد معين ، وكل عينة هي "لقطة" لإشارة الصوت في نقطة زمنية. تسمى هذه المعلمة تردد أخذ العينات وتقاس بالهرتز .

كلما زاد تردد أخذ العينات ، كلما قمنا بإعادة إنشاء ترددات الإشارة الأصلية بدقة أكبر. تنص نظرية Nyquist-Shannon (Kotelnikov) (بعبارات بسيطة) على أن تردد أخذ العينات يجب أن يكون ضعف أعلى تردد إشارة نريد تسجيله.

يمكن للأذن البشرية أن تسمع تقريبًا في النطاق من 20 هرتز إلى 20 كيلو هرتز ، لذلك غالبًا ما يستخدم تردد أخذ العينات 44.1 كيلو هرتز لإعادة إنشاء موسيقى عالية الجودة، وهو أكثر بقليل من ضعف الحد الأقصى للتردد الذي يمكن للأذن البشرية التعرف عليه. وهذا يضمن إعادة إنشاء مجموعة كاملة من ترددات الأجهزة والصوت.

ومع ذلك ، تأخذ كل عينة مساحة في الملف ، لذلك لا يمكننا تحديد الحد الأقصى لمعدل أخذ العينات. ومع ذلك ، إذا لم تتناول عينة سريعة بما فيه الكفاية ، فقد تفقد معلومات مهمة. يجب أن يعتمد تردد أخذ العينات المحدد على الترددات الموجودة في الصوت المعاد إنشاؤه.

يجب أن يتم التشغيل بنفس تردد المصدر ، وإلا سيختلف الصوت ومدته.

افترض أن عشر ثوان من الصوت تم تسجيلها بتردد أخذ العينات يبلغ 16 كيلو هرتز. إذا قمت بتشغيله بتردد 8 كيلو هرتز ، فستكون نغمته أقل ، وستكون المدة عشرين ثانية. إذا قمت بتشغيله بتردد أخذ العينات 32 كيلو هرتز ، فستكون النغمة المسموعة أعلى ، وسيستمر الصوت نفسه خمس ثوانٍ.

يوضح هذا الفيديو الفرق في معدلات العينات مع الأمثلة.

عمق البت


تواتر أخذ العينات هو نصف المعادلة فقط. يحتوي الصوت أيضًا على عمق قليلاً ، أي عدد البتات لكل عينة.

عندما يلتقط ADC عينة من إشارة صوتية ، يجب أن يحول هذه القيمة التناظرية إلى رقمية ، ويعتمد نطاق القيم الملتقطة على عدد البتات المستخدمة. 8 بت (256 قيمة) ، 16 بت (65،526 قيم) ، 32 بت (4،294،967،296 قيم) ، إلخ.

يرتبط عدد البتات لكل عينة بالنطاق الديناميكي للصوت ، أي بأعلى الأجزاء وأكثرها هدوءًا. عمق البت الأكثر شيوعًا للموسيقى هو 16 بت.

أثناء التشغيل ، من الضروري توفير نفس عمق البت مثل المصدر ، وإلا سيتغير الصوت ومدته.

على سبيل المثال ، لديك ملف صوتي بأربع عينات مخزنة على هيئة 8 بت: [0x25 ، 0xAB ، 0x34 ، 0x80]. إذا حاولت تشغيلها كما لو كانت 16 بت ، فستحصل على عينتين فقط: [0x25AB ، 0x3480]. لن يؤدي هذا فقط إلى قيم غير صحيحة للعينات الصوتية ، ولكن أيضًا إلى تقليل عدد العينات إلى النصف ، وبالتالي مدة الصوت.

من المهم أيضًا معرفة تنسيق العينات. 8 بت غير موقعة ، 8 بت غير موقعة ، 16 بت غير موقعة ، 16 بت غير موقعة ، إلخ. عادة ما تكون 8 بت غير موقعة ، و 16 بت موقعة. إذا تم الخلط بينهما ، سيتم تشويه الصوت إلى حد كبير.

يعرض هذا الفيديو اختلاف عمق البت مع الأمثلة.

ملفات Wav


في معظم الأحيان ، يتم تخزين بيانات الصوت الخام على جهاز الكمبيوتر بتنسيق WAV ، والذي يحتوي على رأس بسيط يصف تنسيق الصوت (تردد أخذ العينات ، عمق البت ، الحجم ، إلخ) ، متبوعًا ببيانات الصوت نفسها.

لا يتم ضغط الصوت على الإطلاق (على عكس تنسيقات مثل MP3) ، لذلك يمكننا تشغيله بسهولة دون الحاجة إلى مكتبة ترميز.

المشكلة الرئيسية في ملفات WAV هي أنه بسبب نقص الضغط ، يمكن أن تكون كبيرة جدًا. يرتبط حجم الملف مباشرة بالمدة ومعدل أخذ العينات وعمق البت.

الحجم = المدة (بالثواني) × معدل أخذ العينات (العينات / العينات) × عمق البت (البت / العينة)

يؤثر تكرار أخذ العينات على حجم الملف بشكل أكبر ، لذا فإن أسهل طريقة لتوفير المساحة هي تحديد قيمة منخفضة بما فيه الكفاية. سننشئ صوتًا في المدرسة القديمة ، لذا فإن التردد المنخفض لأخذ العينات يناسبنا.

I2S


يحتوي ESP32 على أجهزة طرفية ، مما يجعل من السهل نسبيًا توفير واجهة بمعدات صوتية: Inter-IC Sound (I2S) .

بروتوكول I2S بسيط للغاية ويتكون من ثلاث إشارات فقط: إشارة ساعة واختيار قنوات (يسار أو يمين) ، وكذلك خط البيانات نفسه.

يعتمد تردد الساعة على تردد أخذ العينات وعمق البت وعدد القنوات. يتم استبدال الإيقاعات لكل بت من البيانات ، وبالتالي ، من أجل إعادة إنتاج الصوت بشكل صحيح ، يجب عليك ضبط تردد الساعة وفقًا لذلك.

تردد الساعة = تردد أخذ العينات (العينات / العينات) × عمق البت (البتات / العينة) × عدد القنوات

يحتوي برنامج تشغيل وحدة التحكم الدقيقة E232 ESP32 على وضعين محتملين: يمكنه إما إخراج البيانات إلى جهات الاتصال المتصلة بجهاز استقبال I2S خارجي ، والتي يمكنها فك تشفير البروتوكول ونقل البيانات إلى مكبر الصوت ، أو يمكنها نقل البيانات إلى ESP32 DAC الداخلي لإخراج إشارة تناظرية يمكن إرسالها إلى المضخم.

لا يحتوي Odroid Go على أي وحدة فك ترميز I2S على اللوحة ، لذلك سيتعين علينا استخدام EAC32 DAC 8 بت الداخلي ، أي يجب علينا استخدام صوت 8 بت. يحتوي الجهاز على جهازي DACs ، أحدهما متصل بـ IO25 والآخر بـ IO26 .

الإجراء يبدو كالتالي:

  1. نقوم بنقل البيانات الصوتية إلى برنامج تشغيل I2S
  2. يرسل برنامج تشغيل I2S بيانات الصوت إلى DAC داخلي 8 بت
  3. تقوم DAC الداخلية بإخراج إشارة تمثيلية
  4. تنتقل الإشارة التناظرية إلى مضخم الصوت



إذا نظرنا إلى دائرة الصوت في دائرة Odroid Go ، فسوف نرى دبابيس GPIO ( IO25 و IO26 ) متصلين بإدخالات مكبر الصوت ( PAM8304A ). يتم
توصيل IO25 أيضًا بإشارة / SD للمكبر ، أي جهة الاتصال التي تقوم بتشغيل مكبر الصوت أو إيقاف تشغيله (إشارة منخفضة تعني إيقاف التشغيل). يتم توصيل مخرجات مكبر الصوت بسماعة واحدة ( P1 ).

تذكر أن IO25 و IO26 عبارة عن مخرجات لـ DACs 8 بت ESP32 ، أي أن إحدى DAC متصلة بـ IN- والأخرى بـ IN + .

IN- و IN + هيالمدخلات التفاضلية لمضخم الصوت. تستخدم المدخلات التفاضلية للحد من الضوضاء الناجمة عن التداخل الكهرومغناطيسي . أي ضجيج موجود في إشارة واحدة سيكون موجودًا أيضًا في إشارة أخرى. يتم طرح إشارة واحدة من أخرى ، مما يلغي الضوضاء.

إذا نظرت إلى مواصفات مضخم الصوت ، فإن لديها دائرة تطبيقات نموذجية ، وهي الطريقة الموصى بها من قبل الشركة المصنعة لاستخدام مكبر الصوت.


ويوصي بتوصيل IN- بالأرض ، IN + بإشارة الإدخال ، و / SD بإشارة التشغيل / الإيقاف. إذا كان هناك ضوضاء 0.005 فولت ، فسيتم قراءة IN- 0V + 0.005V ، ومع IN + - VIN + 0.005V . يجب طرح إشارات الإدخال من بعضها البعض والحصول على قيمة الإشارة الحقيقية ( VIN ) بدون ضوضاء.

ومع ذلك ، لم يستخدم مصممو Odroid Go التكوين الموصى به.

مرة أخرى بالنظر إلى دائرة Odroid Go ، نرى أن المصممين ربطوا خرج DAC بـ IN- وأن نفس إخراج DAC متصل بـ / SD . / SD- هذه إشارة إيقاف تشغيل ذات مستوى منخفض نشط ، لذلك لكي يعمل مكبر الصوت ، تحتاج إلى ضبط إشارة عالية.

هذا يعني أنه لاستخدام مضخم الصوت ، يجب ألا نستخدم IO25 باعتباره DAC ، ولكن كمخرج GPIO مع إشارة عالية دائمًا. ومع ذلك ، في هذه الحالة ، يتم تعيين إشارة عالية إلى IN- ، وهو ما لا توصي به مواصفات مكبر الصوت (يجب أن يتم تأريضه). ثم يجب علينا استخدام DAC المتصل بـ IO26 ، حيث يجب تغذية ناتج I2S إلى IN + . هذا يعني أننا لن نحقق الحد الضروري من الضوضاء ، لأن IN- غير متصل بالأرض. تصدر الضوضاء الناعمة باستمرار من السماعات.

نحتاج إلى ضمان التكوين الصحيح لبرنامج تشغيل I2S ، لأننا نريد استخدام DAC المتصل بـ IO26 فقط . إذا استخدمنا DAC متصل بـ IO25 ، فسيؤدي ذلك باستمرار إلى إيقاف تشغيل إشارة مكبر الصوت ، وسيكون الصوت رهيبًا.

بالإضافة إلى هذا الغرابة ، عند استخدام DAC داخلي 8 بت ، يتطلب برنامج تشغيل I2S في ESP32 إرسال عينات 16 بت إليه ، ولكنه يرسل فقط البايت العالي إلى DAC 8 بت. لذلك ، نحتاج إلى أخذ صوت 8 بت ولصقه في ضعف المخزن المؤقت الكبير ، في حين أن المخزن المؤقت سيكون نصف فارغ. ثم نمرره إلى برنامج تشغيل I2S ويمرر DAC البايت العالي لكل عينة. لسوء الحظ ، هذا يعني أنه يجب علينا "الدفع" مقابل 16 بت ، ولكن لا يمكننا استخدام 8 بت إلا.

تعدد المهام


لسوء الحظ ، لا يمكن أن تعمل اللعبة على نواة واحدة ، كما أردت في الأصل ، لأنه يبدو أن هناك خطأ في برنامج تشغيل I2S.

يجب أن يستخدم برنامج تشغيل I2S DMA (مثل برنامج تشغيل SPI) ، أي يمكننا فقط بدء نقل I2S ، ثم نواصل عملنا أثناء قيام برنامج تشغيل I2S بنقل البيانات الصوتية.

ولكن بدلاً من ذلك ، يتم حظر وحدة المعالجة المركزية طوال مدة الصوت ، وهو غير مناسب تمامًا للعبة. تخيل أنك تضغط على زر الانتقال ، ثم توقف نقش اللاعب مؤقتًا حركته لمدة 100 مللي ثانية أثناء تشغيل صوت القفز.

لحل هذه المشكلة ، يمكننا الاستفادة من حقيقة وجود نواتين على متن ESP32. يمكننا إنشاء مهمة (أي خيط) في المركز الثاني ، والتي ستتعامل مع إعادة إنتاج الصوت. بفضل هذا ، يمكننا نقل المؤشر إلى المخزن المؤقت للصوت من المهمة الرئيسية للعبة إلى المهمة الصوتية ، وتبدأ مهمة الصوت في نقل I2S ويتم حظرها طوال مدة تشغيل الصوت. لكن المهمة الرئيسية في النواة الأولى (مع معالجة المدخلات والعرض) ستستمر في التنفيذ دون حظر.

التهيئة


بمعرفة ذلك ، يمكننا بدء تشغيل برنامج تشغيل I2S بشكل صحيح. للقيام بذلك ، تحتاج فقط إلى بضعة أسطر من التعليمات البرمجية ، ولكن الصعوبة تكمن في معرفة المعلمات التي تحتاج إلى تعيينها لإعادة إنتاج الصوت بشكل صحيح.

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);
	}
}

أولاً ، نقوم بتكوين IO25 (المتصل بإشارة إيقاف تشغيل مكبر الصوت) كإخراج حتى يتمكن من التحكم في مضخم الصوت ، وتطبيق إشارة عالية عليه لتشغيل مكبر الصوت.

بعد ذلك ، نقوم بتكوين وتثبيت برنامج تشغيل I2S نفسه. سأقوم بتحليل كل جزء من سطر التكوين كل سطر ، لأن كل سطر يتطلب شرحًا:

  • الوضع
    • قمنا بتعيين برنامج التشغيل باعتباره برنامجًا رئيسيًا (التحكم في الناقل) ، وجهاز إرسال (لأننا ننقل البيانات إلى المستلمين) ، وقمنا بتكوينه لاستخدام DAC 8 بت المدمج (لأن لوحة Odroid Go لا تحتوي على DAC خارجي).
  • معدل العينة
    • 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. , .

ثم نقوم بإنشاء قائمة انتظار - هذه هي الطريقة التي يرسل بها FreeRTOS البيانات بين المهام. نضع البيانات في قائمة انتظار مهمة واحدة ونستخرجها من قائمة انتظار مهمة أخرى. قم بإنشاء بنية تسمى QueueData تجمع بين المؤشر إلى المخزن المؤقت الصوتي وطول المخزن المؤقت في بنية واحدة يمكن وضعها في قائمة الانتظار.

بعد ذلك ، قم بإنشاء مهمة تعمل على النواة الثانية. نقوم بتوصيلها بوظيفة PlayTask ، التي تقوم بتشغيل الصوت. المهمة نفسها هي حلقة لا نهائية تتحقق باستمرار لمعرفة ما إذا كانت هناك أي بيانات في قائمة الانتظار. إذا كانت كذلك ، ترسلها إلى سائق I2S حتى يمكن لعبها. سيتم حظر مكالمة i2s_writeوهذا يناسبنا ، لأن المهمة تتم على نواة منفصلة عن الخيط الرئيسي للعبة.

يلزم إجراء مكالمة إلى i2s_zero_dma_buffer بحيث لا تكتمل الأصوات من مكبرات الصوت بعد اكتمال التشغيل. لا أعرف ما إذا كان هذا خطأ في برنامج تشغيل I2S أو السلوك المتوقع ، ولكن بدونه ، بعد انتهاء تشغيل المخزن المؤقت للصوت ، تصدر السماعة إشارة القمامة.

تشغيل الصوت


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

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

	xQueueSendToBack(gQueue, &data, portMAX_DELAY);
}

نظرًا لحقيقة أن التكوين بالكامل قد اكتمل بالفعل ، فإن الاستدعاء لوظيفة تشغيل المخزن المؤقت للصوت نفسه بسيط للغاية ، لأن العمل الرئيسي يتم في مهمة أخرى. نضع المؤشر في المخزن المؤقت وطول المخزن المؤقت في بنية QueueData ، ثم نضعه في قائمة الانتظار التي تستخدمها وظيفة PlayTask .

بسبب هذا النمط من العملية ، يجب أن يقوم المخزن المؤقت الصوتي بإكمال التشغيل قبل أن يتمكن من بدء المخزن المؤقت الثاني. لذلك ، إذا حدثت قفزة وإطلاق نار في وقت واحد ، فسيتم تشغيل الصوت الأول قبل الثاني ، وليس في نفس الوقت.

على الأرجح ، سأقوم في المستقبل بخلط أصوات الإطارات المختلفة في المخزن المؤقت الصوتي الذي يتم نقله إلى برنامج تشغيل I2S. سيسمح لك ذلك بتشغيل عدة أصوات في نفس الوقت.

تجريبي


سننشئ مؤثرات صوتية خاصة بنا باستخدام jsfxr ، وهي أداة مصممة خصيصًا لتوليد نوع أصوات الألعاب التي نحتاجها. يمكننا ضبط تردد أخذ العينات وعمق البتات مباشرة ، ثم إخراج ملف WAV.

لقد قمت بإنشاء تأثير صوت قفزة بسيط يشبه صوت قفزة ماريو. يحتوي على تردد أخذ عينات 5012 (كما تم تكوينه أثناء التهيئة) وعمق بت 8 (لأن DAC هو 8 بت).


بدلاً من تحليل ملف WAV في الشفرة مباشرة ، سنفعل شيئًا مشابهًا لما فعلناه لتحميل العفريت في العرض التوضيحي للجزء 4: سنزيل رأس WAV من الملف باستخدام محرر hex. وبفضل هذا ، سيكون الملف المقروء من بطاقة SD بيانات أولية فقط. أيضا ، لن نقرأ مدة الصوت ، سنكتبه في الرمز. في المستقبل ، سنقوم بتحميل الموارد الصوتية بشكل مختلف ، ولكن هذا يكفي للعرض التوضيحي.

يمكن تنزيل الملف الخام من هنا .

// 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);
    }
}

نقوم بتحميل بيانات 8 بت في المخزن المؤقت soundEffect 8 بت ، ثم نسخ هذه البيانات إلى المخزن المؤقت soundBuffer 16 بت ، حيث سيتم تخزين البيانات في الثماني بتات العالية. أكرر - هذا ضروري بسبب ميزات تنفيذ الجيش الإسرائيلي.

بعد إنشاء مخزن مؤقت 16 بت ، يمكننا تشغيل صوت نقرة زر. سيكون من المنطقي استخدام زر مستوى الصوت لهذا.

int lastState = 0;

for (;;)
{
	[...]

	int thisState = input.volume;

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

	lastState = thisState;

	[...]
}

نحن نراقب حالة الزر بحيث أنه عن طريق الخطأ ، بنقرة واحدة على الزر ، لا يتم استدعاء Odroid_PlayAudio عن طريق الخطأ عدة مرات.


مصدر


كل رمز المصدر هنا .

المراجع



All Articles