
الجوهر
لقد قمت بالفعل بإنشاء عدد من الأجهزة الإلكترونية المختلفة للهوايات ، ولدي ميزة غريبة: إذا كان هناك باعث صوت كهرضغطية (جرس) على اللوحة ، فأنا ، بعد الانتهاء من العمل الرئيسي في المشروع ، أبدأ في المعاناة من الهراء وجعله يلعب ألحانًا مختلفة (قدر الإمكان) ) من المفيد بشكل خاص تضمين لحن في نهاية عملية طويلة لجذب الانتباه. على سبيل المثال ، استخدمته عندما بنيت كاميرا تعريض ضوئي مؤقتة لإضاءة مقاوم الضوء ، إلخ.
ولكن عندما بدأت في البحث عن أمثلة لتوليد الترددات لـ AVR على الشبكة ، لسبب ما ، صادفت مشاريع وحشية أو غير موجزة بما يكفي لتوليد توليد الترددات الصوتية بطريقة مبرمجة بحتة. ثم قررت معرفة ذلك بنفسي ...
الاستطراد الغنائي
تشمل هوايتي إنشاء أجهزة مختلفة على وحدات التحكم الدقيقة ، لأن هذا لا يتقاطع مع أستاذي. نشاط (تطوير البرمجيات) ، أعتبر نفسي من العصاميين المطلقين ، وفي الإلكترونيات ليست قوية جدًا. في الواقع ، أنا أفضل الميكروكونترولر PIC ، ولكن حدث أن تراكمت عددًا معينًا من الميكروكونترولر Atmel AVR (الآن Microchip). قم بإجراء حجز على الفور لم يكن لدي AVR في يدي ، أي هذا هو أول مشروع لي على Atmel MCU ، وهو Atmega48pa. ينفذ المشروع نفسه بعض الحمولة ، ولكن هنا سأصف جزءًا منه يتعلق بتوليد ترددات صوتية. اختبار توليد الترددات الذي أسميته "buzic" ، كاختصار لموسيقى الجرس. نعم ، لقد نسيت تقريباً: في هبر ، هناك مستخدم يحمل لقب
buzic ، أردت أن أحذر على الفور من أن هذه المذكرة لا تنطبق عليه على الإطلاق ، وفي حالة اعتذاره فورًا عن استخدام تركيبة الحروف "Buzic".
لذا دعنا نذهب
تعرفت على عدد كبير من الأمثلة من الشبكة - كلها مبنية إما على أبسط دورة في الجسم الرئيسي للبرامج الثابتة ، أو في انقطاع المؤقت. لكنهم جميعًا يستخدمون نفس النهج لتوليد التردد:
- إطعام مستوى عال لقدم متحكم
- جعل التأخير
- تغذية منخفضة إلى قدم متحكم
تغيير التأخير وإعدادات المؤقت - ضبط التردد.
هذا النهج لم يناسبني كثيرًا ، لأنه لم يكن لدي رغبة في كتابة كود للتحكم اليدوي في قدم متحكم. أود أن يقوم "الحجر" بتوليد تردد الصوت بالنسبة لي ، وقد قمت للتو بتعيين قيم بعض التسجيلات ، وبالتالي تغييرها (التردد).
عند دراسة ورقة البيانات (المشار إليها فيما يلي باسم DS) ، ما زلت أجد وضع الموقت الذي احتاجه - وهذا الوضع ، كما قد توقعت ، هو وضع CTC (Clear Timer on Compare Match). نظرًا لأن وظيفة تشغيل الموسيقى هي ، بعبارة ملطفة ، وليس الوظيفة الرئيسية ، فقد فضلت اختيار المؤقت 2 لها (الفقرة 22 من SD).
يعلم الجميع أن أي متحكم تقريبًا لديه وضع توليد إشارة PWM يتم تنفيذه على المؤقتات وهو جهاز بالكامل. ولكن في هذه المهمة ، PWM ليست مناسبة لأن سيتم إنشاء تردد واحد فقط في الأجهزة. لذلك ، نحن بحاجة إلى PFM (تعديل تردد النبض). بعض التشابه بين PFM هو وضع مؤقت CTC (البند 22.7.2 LH).
وضع CTC
المؤقت 2 في متحكم Atmega48pa هو 8 بت ، أي أنه "يدق" من 0 إلى 255 ثم يذهب في دائرة. بالمناسبة ، يمكن أن يذهب الموقت في اتجاه مختلف ، ولكن ليس في حالتنا. المكون المطلوب التالي هو وحدة المقارنة. تحدث بشكل فظ جدًا ، هذه الوحدة هي البادئ بأي أحداث تتعلق بالمؤقت. يمكن أن تكون الأحداث مختلفة - مثل الانقطاعات ، والتغيرات في مستوى أرجل معينة من وحدة التحكم الدقيقة ، وما إلى ذلك (من الواضح أننا مهتمون بالثاني). كما قد تخمن ، لم تتم تسمية وحدة المقارنة فقط - بل تقارن قيمة محددة تم تحديدها بواسطة مطور البرامج الثابتة بقيمة المؤقت الحالية. إذا وصلت قيمة المؤقت إلى القيمة التي حددناها ، فسيحدث حدث. يمكن أن تحدث الأحداث أيضًا عند تجاوز المؤقت أو أثناء إعادة التعيين.
حسنًا ، لقد توصلنا إلى استنتاج مفاده أنه من الملائم بالنسبة لنا في أوقات معينة أن المؤقت ، مع وحدة المقارنة ، يغيران بشكل مستقل مستوى سفح وحدة التحكم الدقيقة إلى الاتجاه المعاكس - وبالتالي يولد نبضات.المهمة الثانية هي تحديد الفترات الفاصلة بين هذه النبضات - أي السيطرة على وتيرة الجيل. يكمن التفرد الكامل لوضع CTC في حقيقة أنه في هذا الوضع لا ينتقل المؤقت إلى النهاية (255) ، ولكن يتم إعادة تعيينه عند الوصول إلى القيمة المحددة. وفقًا لذلك ، من خلال تغيير هذه القيمة ، يمكننا بالفعل التحكم في التردد. على سبيل المثال ، إذا قمنا بتعيين قيمة وحدة المقارنة إلى 10 ، فسيحدث تغير المستوى على قدم وحدة التحكم الدقيقة 20 مرة أكثر مما لو قمنا بتعيينها (قيمة وحدة المقارنة) على 200.
الآن يمكننا التحكم في التردد!
حديد

يظهر دبوس التحكم الدقيق أننا بحاجة إلى ربط الجرس الخاص بنا إما بساق PB3 (OC2A) أو ساق PD3 (OC2B) ، لأن OC2A و OC2B يعنيان أنه على هذه الأرجل ، يمكن للمؤقت 2 أن يولد إشارات.
المخطط الذي أستخدمه عادة لتوصيل الجرس هو:
وهكذا قمنا بتجميع الجهاز.يسجل
في الفقرة السابقة ، قررنا اختيار الساق - هذا هو PB3 (OC2A) ، وسنعمل معه. إذا كنت بحاجة إلى PD3 ، فسيكون كل شيء بالنسبة لها هو نفسه ، والذي سيكون مرئيًا بوضوح من القصة.
سنقوم بتكوين المؤقت 2 الخاص بنا عن طريق تغيير 3 سجلات:
- TCCR2A - إعدادات الوضع واختيار السلوك
- TCCR2B - إعدادات الوضع ومقسم تردد المؤقت (أيضًا بتات FOC - لا نستخدمها)
- OCR2A (OCR2B لحالة ساق PD3) - قيمة وحدة المقارنة
النظر أولاً في تسجيلات TCCR2A و TCCR2B

كما ترون ، لدينا 3 مجموعات من البتات المهمة بالنسبة لنا - هذه هي البتات من سلسلة COM2xx و WGM2x و CS2x
أول شيء نحتاج إلى تغييره هو WGM2x - هذا هو الشيء الرئيسي لاختيار وضع الإنشاء - يتم استخدام هذه البتات لتحديد وضع CTC.
ملحوظة: من الواضح في LH أن الخطأ المطبعي في "تحديث OCR0x at" يجب أن يكون OCR2xعلى سبيل المثال سيكون الرمز على النحو التالي:
TCCR2A = _BV(WGM21) ;
كما ترون ، لم يتم استخدام TCCR2B حتى الآن. يجب أن يكون WGM22 صفرًا ، ولكنه بالفعل صفر.
الخطوة التالية هي تكوين بت COM2xx ، بشكل أكثر دقة COM2Ax - لأنه نحن نعمل مع الساق PB3 (تستخدم PD3 COM2Bx بنفس الطريقة). ما سيحدث لساق PB3 الخاصة بنا يعتمد عليهم.
تعتمد بتات COM2xx على الوضع الذي اخترناه مع بتات WGM2x ، لذا سيتعين علينا العثور على القسم المقابل في LH. لأن لدينا وضع CTC ، أي ليس PWM ، ثم ابحث عن لوحة "مقارنة وضع الإخراج ، غير PWM" ، ومن هنا:

هنا تحتاج إلى تحديد "تبديل" - بحيث يتغير المستوى على الساق إلى العكس عندما يصل المؤقت إلى القيمة المحددة. تغيير المستوى المستمر وتنفيذ توليد التردد الذي نحتاجه.
لأن بتات COM2xx موجودة أيضًا في سجل TCCR2A - فقط تتغير:
TCCR2A = _BV(COM2A0) | _BV(WGM21) ;
بطبيعة الحال ، تحتاج أيضًا إلى تحديد مقسم التردد باستخدام بتات CS2x ، وبالطبع ، قم بتعيين قدم PB3 على الإخراج ... ولكننا لن نفعل ذلك حتى الآن عندما ندير MK ، لن نحصل على صرخة خارقة بتردد غير مفهوم ، ولكن عندما نقوم بكل الإعدادات الأخرى و بدوره على القدم للخروج - سيتم وصفه أدناه.
لذلك دعونا نلقي نظرة على التهيئة الكاملة:
#include <avr/io.h> //set bit - using bitwise OR operator #define sbi(x,y) x |= _BV(y) //clear bit - using bitwise AND operator #define cbi(x,y) x &= ~(_BV(y)) #define BUZ_PIN PB3 void timer2_buzzer_init() { // PB3 cbi(PORTB, BUZ_PIN); // PB3 , cbi(DDRB, BUZ_PIN); // TCCR2A = _BV(COM2A0) | _BV(WGM21) ; // ( ) OCR2A = 0; }
استخدمت وحدات ماكرو cbi و sbi (تجسست في مكان ما على الشبكة) لتعيين البتات الفردية ، وتركتها بهذه الطريقة. تم وضع وحدات الماكرو هذه بالطبع في ملف الرأس ، ولكن من أجل الوضوح ، أضعها هنا.
حساب تكرار ومدة الملاحظات
الآن نأتي إلى جوهر المسألة. منذ بعض الوقت ، حاول معارف الموسيقيين أن يدفعوا إلى دماغي مبرمجًا بعض المعلومات حول الطاقم الموسيقي ، كان دماغي يغلي تقريبًا ، لكنني ما زلت أحضر حبة مفيدة من هذه المحادثات.
أحذرك على الفور - من الممكن وجود أخطاء كبيرة.
- يتكون كل مقياس من 4 أرباع
- كل لحن له إيقاع - أي عدد هذه الأرباع في الدقيقة
- يمكن عزف كل نغمة كمقياس كامل ، وكذلك جزءه 1/2 ، 1/3 ، 1/4 ، إلخ.
- كل ملاحظة ، بالطبع ، لها تردد معين
لقد درسنا الحالة الأكثر شيوعًا ، في الواقع ، كل شيء أكثر تعقيدًا هناك ، على الأقل بالنسبة لي ، لذلك لن أناقش هذا الموضوع في إطار هذه القصة.
حسنًا ، حسنًا ، سنعمل مع ما لدينا. الشيء الأكثر أهمية بالنسبة لنا هو الحصول في نهاية المطاف على تكرار المذكرة (في الواقع ، قيمة سجل OCR2A) ومدتها ، على سبيل المثال ، بالمللي ثانية. وفقًا لذلك ، من الضروري إجراء بعض الحسابات.
لأن نحن في إطار لغة برمجة ، الألحان أسهل في التخزين في مصفوفة. الطريقة الأكثر منطقية لتعيين كل عنصر من عناصر الصفيف في التنسيق هي ملاحظة + المدة. من الضروري حساب حجم العنصر بالبايت ، لأننا نكتب تحت وحدة التحكم الدقيقة ومع وجود الموارد هنا فهو ضيق - وهذا يعني أن حجم العنصر بالبايت يجب أن يكون كافيًا.
التردد
لنبدأ بالتردد. لأن لدينا مؤقت 8 بت 2 ، سجل المقارنة OCR2A هو أيضا 8 بت. أي أن عنصر مصفوفة اللحن الخاص بنا سيكون بالفعل 2 بايت على الأقل ، لأنك ما زلت بحاجة إلى حفظ المدة. في الواقع ، 2 بايت هو الحد الأقصى لهذا النوع من الحرف. ما زلنا لا نحصل على صوت جيد ، بصراحة ، وإنفاق المزيد من وحدات البايت أمر غير معقول.
لذا ، توقفنا عند 2 بايت.عند حساب التردد ، في الواقع ، تظهر مشكلة كبيرة أخرى.إذا نظرت إلى ترددات الملاحظات ، فسوف نرى أنها مقسمة إلى أوكتاف.

بالنسبة إلى معظم الألحان البسيطة ، يكفي 3 أوكتاف ، لكنني قررت المراوغة وتنفيذ 6: كبيرة وصغيرة والأربع التالية.
الآن دعنا نتطرق إلى الموسيقى ونغرق في عالم برمجة وحدة التحكم الدقيقة.
أي جهاز توقيت في AVR (والغالبية العظمى من MK الآخر) مرتبط بتردد MK نفسه. تردد الكوارتز في دائرتي هو 16 ميجا هرتز. يتم تحديد نفس التردد بواسطة F_CPU "تعريف" ليكون مساويًا لـ 16000000 في حالتي. في سجل TCCR2B ، يمكننا تحديد مقسمات التردد بحيث لا يقوم "المؤقت" 2 الخاص بنا "بتحديد" بسرعة محمومة تبلغ 16000000 مرة في الثانية ، ولكن أبطأ قليلاً. يتم اختيار مقسم التردد بواسطة بتات CS2x ، كما هو مذكور أعلاه.
ملحوظة: من الواضح أنه في LH يجب أن يكون الخطأ المطبعي بدلاً من "CA2x" هو CS2xالسؤال الذي يطرح نفسه - كيفية تكوين الفاصل؟
للقيام بذلك ، تحتاج إلى فهم كيفية حساب قيم سجل OCR2A. وحسابه بسيط للغاية:
OCR2A = F_CPU / (مقسم تردد الكوارتز * 2) / تردد الملاحظةعلى سبيل المثال ، قم بتدوين الملاحظة قبل الأوكتاف الأول والمقسوم 256 (CS22 = 1 ، CS21 = 1 ، CS20 = 0):
OCR2A = 16000000 / (256 * 2) / 261 = 119
سأشرح على الفور من أين جاء الضرب في 2. والحقيقة هي أننا حددنا وضع "تبديل" مع تسجيلات COM2Ax ، مما يعني أن تغيير المستويات في القدم من الأدنى إلى الأعلى (أو العكس) والعكس سيحدث في مرورين من المؤقت: أولاً يصل الموقت إلى قيمة OCR2A ويغير قدم وحدة التحكم الدقيقة ، على سبيل المثال ، من 1 إلى 0 ، ويتم إعادة تعيينه فقط في اللفة الثانية يتغير من 0 إلى 1. لذلك ، فإن لفتين من الموقت تذهب لكل موجة كاملة ، على التوالي ، يجب ضرب المقسوم على 2 ، وإلا فإننا نحصل فقط نصف تواتر ملاحظتنا.
ومن هنا المصيبة المذكورة أعلاه ...
إذا أخذنا الملاحظة قبل الأوكتاف الكبير وتركنا المقسوم عليه 256:
OCR2A = 16000000 / (256 * 2) / 65 = 480 !!!
480 - من الواضح أن هذا الرقم أكثر من 255 ولا يتناسب فعليًا مع سجل OCR2A 8 بت.ماذا تفعل؟ من الواضح أن تغيير المقسم ، ولكن إذا وضعنا المقسم 1024 ، فسيكون كل شيء على ما يرام مع اوكتاف كبير. ستبدأ المشاكل بالأوكتافات العليا:
لوس الرابع أوكتاف - OCR2A = 16000000 / (1024 * 2) / 3520 = 4
اوكتاف رابع حاد - OCR2A = 16000000 / (1024 * 2) / 3729 = 4
لم تعد قيم OCR2A مختلفة ، مما يعني أن الصوت سيتوقف أيضًا عن الاختلاف.هناك طريقة واحدة للخروج: لتكرار الملاحظات ، تحتاج إلى تخزين ليس فقط قيم سجل OCR2A ، ولكن أيضًا بتات من مقسم تردد الكوارتز. لأن بالنسبة لأوكتافات مختلفة ، ستكون هناك قيمة مختلفة لمقسم تردد الكوارتز ، والذي سنحتاج إلى تعيينه في سجل TCCR2B!الآن كل شيء يسير في مكانه - وشرحت أخيرًا لماذا لم نتمكن من ملء قيمة المقسوم على الفور في الدالة timer2_buzzer_init ().
لسوء الحظ ، فإن مقسم التردد هو 3 بتات أخرى. ويجب أن تؤخذ في البايت الثاني من عنصر مجموعة اللحن.
تحيا وحدات الماكرو #define DIV_MASK (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_1024 (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_256 (_BV(CS21) | _BV(CS22)) #define DIV_128 (_BV(CS20) | _BV(CS22)) #define DIV_64 _BV(CS22) #define DIV_32 (_BV(CS20) | _BV(CS21)) #define NOTE_1024( x ) ((F_CPU / (1024 * 2) / x) | (DIV_1024 << 8)) #define NOTE_256( x ) ((F_CPU / (256 * 2) / x) | (DIV_256 << 8)) #define NOTE_128( x ) ((F_CPU / (128 * 2) / x) | (DIV_128 << 8)) #define NOTE_64( x ) ((F_CPU / (64 * 2) / x) | (DIV_64 << 8)) #define NOTE_32( x ) ((F_CPU / (32 * 2) / x) | (DIV_32 << 8))
ولمدة الملاحظة ، لم يتبق لدينا سوى 5 بتات ، لذا دعنا نحسب المدة.
المدة
تحتاج أولاً إلى ترجمة قيمة الإيقاع إلى وحدات مؤقتة (على سبيل المثال ، بالمللي ثانية) - لقد فعلت ذلك على النحو التالي:
مدة التدبير الموسيقي بالقيمة ms = (60،000 ms * 4 أرباع) / قيمة الإيقاع.وفقًا لذلك ، إذا كنا نتحدث عن أجزاء الضرب ، فيجب تقسيم هذه القيمة ، وفي البداية اعتقدت أن التحول الأيسر المعتاد للفواصل سيكون كافيًا. على سبيل المثال الرمز كان هذا:
uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { return (precalced_tempo / _BV((note >> 11) & 0b00111)); }
على سبيل المثال استخدمت 3 بتات (من الخمس المتبقية) وحصلت على أجزاء من الإيقاع الموسيقي من درجات 2 إلى 1/128. ولكن عندما أعطيت صديقًا يطلب مني أن أكتب نوعًا من نغمة الرنين إلى قطعة الحديد الخاصة بي ، كانت هناك أسئلة لماذا لا يوجد 1/3 أو 1/6 وبدأت أفكر ...
في النهاية ، قمت بعمل نظام صعب للحصول على مثل هذه الفترات. بت واحد من 2x المتبقية - قضيت على علامة الضرب بمقدار 3 لمقسم الساعة الذي تم الحصول عليه بعد التحول. والبت الأخير هو توضيح ما إذا كان من الضروري طرح 1. هذا من الصعب وصفه ، فمن الأسهل رؤية الرمز:
uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { note >>= 11; uint8_t divider = _BV(note & 0b00111); note >>= 3; divider *= ((note & 0b01) ? 3 : 1); divider -= (note >> 1); return (precalced_tempo / divider); }
ثم قمت "بتعريف" جميع الملاحظات الممكنة (باستثناء تلك التي هي أقل من 1/128).
ها هم #define DEL_MINUS_1 0b10000 #define DEL_MUL_3 0b01000 #define DEL_1 0 #define DEL_1N2 1 #define DEL_1N3 (2 | DEL_MINUS_1) #define DEL_1N4 2 #define DEL_1N5 (1 | DEL_MINUS_1 | DEL_MUL_3) #define DEL_1N6 (1 | DEL_MUL_3) #define DEL_1N7 (3 | DEL_MINUS_1) #define DEL_1N8 3 #define DEL_1N11 (2 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N12 (2 | DEL_MUL_3) #define DEL_1N15 (4 | DEL_MINUS_1) #define DEL_1N16 4 #define DEL_1N23 (3 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N24 (3 | DEL_MUL_3) #define DEL_1N31 (5 | DEL_MINUS_1) #define DEL_1N32 5 #define DEL_1N47 (4 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N48 (4 | DEL_MUL_3) #define DEL_1N63 (6 | DEL_MINUS_1) #define DEL_1N64 6 #define DEL_1N95 (5 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N96 (5 | DEL_MUL_3) #define DEL_1N127 (7 | DEL_MINUS_1) #define DEL_1N128 7
ضع كل ذلك معًا
الإجمالي ، لدينا التنسيق التالي لعنصر مجموعة نغمات الرنين.
- 1 بت: مقسم التأخير - 1
- 1 بت: مقسم التأخير * 3
- 3bit: تغيير مقسم التأخير
- 3bit: مقسم ساعة المعالج
- 8 بت: قيمة OCR2A
فقط 16 بت.
عزيزي القارئ ، إذا كنت ترغب في ذلك ، يمكنك أن تحلم بالتنسيق بنفسك ، ربما سيولد شيء أكثر قوة من ملكي.
نسينا أن نضيف ملاحظة فارغة ، أي الصمت. وأخيرًا ، شرحت لماذا في البداية ، في وظيفة timer2_buzzer_init () ، قمنا بتعيين المحطة PB3 للإدخال وليس الإخراج. عند تغيير التسجيل DDRB ، سنقوم بتشغيل وإيقاف تشغيل "الصمت" أو التكوين ككل. لأن لا يمكن أن يكون لدينا ملاحظات بقيمة 0 - ستكون ملاحظة "فارغة".
حدد وحدات الماكرو المفقودة والوظيفة لتمكين توليد الصوت:
#define EMPTY_NOTE 0 #define NOTE(delay, note) (uint16_t)((delay << 11) | note) ........ ........ ........ void play_music_note(uint16_t note) { if (note) { TCCR2B = (note >> 8) & DIV_MASK; OCR2A = note & 0xff; sbi(DDRB, BUZ_PIN); } else cbi(DDRB, BUZ_PIN); }
الآن سأريكم كيف تبدو نغمة رنين مكتوبة وفقًا لهذا المبدأ:
const uint16_t king[] PROGMEM = { NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N2, SI3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, EMPTY_NOTE), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, FA3), NOTE(DEL_1N2, LA3), NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, MI4), NOTE(DEL_1N4, RE4), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N2, RE4), NOTE(DEL_1N2, EMPTY_NOTE), };
تشغيل نغمة الرنين
لا يزال أمامنا مهمة واحدة وهي عزف اللحن. للقيام بذلك ، نحتاج إلى "تشغيل" من خلال مجموعة نغمات الرنين ، مع تحمل الإيقاف المؤقت المناسب وتبديل ترددات الملاحظات. من الواضح أننا بحاجة إلى مؤقت آخر ، والذي ، بالمناسبة ، يمكن استخدامه لمهام عامة أخرى ، كما أفعل عادةً. علاوة على ذلك ، يمكنك التبديل بين عناصر المصفوفة إما في مقاطعة هذا المؤقت أو في الحلقة الرئيسية ، واستخدام المؤقت لحساب الوقت. في هذا المثال ، استخدمت الخيار الثاني.
كما تعلم ، فإن نص أي برنامج لـ MK يتضمن حلقة لا نهائية:
int main(void) { for(;;) {
في ذلك سوف "نركض" على طول مجموعتنا. ولكننا بحاجة إلى وظيفة مشابهة لـ GetTickCount من WinApi ، والتي تُرجع عدد المللي ثانية على أنظمة تشغيل Windows. ولكن بطبيعة الحال ، في عالم MK لا توجد مثل هذه الوظائف "خارج الصندوق" ، لذا يجب علينا كتابتها بأنفسنا.
المؤقت 1
لحساب الفترات الزمنية (لا أكتب عمداً ميلي ثانية ، ستفهم فيما بعد السبب) لقد استخدمت المؤقت 1 بالاقتران مع وضع CTC المعروف بالفعل. المؤقت 1 هو جهاز ضبط الوقت 16 بت ، مما يعني أن قيمة وحدة المقارنة الخاصة بها مُشار إليها بالفعل بواسطة 2 8 بت يسجل OCR1AH و OCR1AL - للبايتات العالية والمنخفضة ، على التوالي. لا أريد أن أصف بالتفصيل العمل مع المؤقت 1 ، لأن هذا لا ينطبق على الموضوع الرئيسي لهذه المذكرة. لذلك ، سأقول لك فقط في كلمتين.
نحن في الواقع بحاجة إلى 3 وظائف:
- تهيئة الموقت
- معالج مقاطعة الموقت
- دالة تقوم بإرجاع عدد الفواصل الزمنية.
ملف الرمز C #include <avr/io.h> #include <avr/interrupt.h> #include <util/atomic.h> #include "timer1_ticks.h" volatile unsigned long timer1_ticks; // ISR (TIMER1_COMPA_vect) { timer1_ticks++; } void timer1_ticks_init() { // // CTC , 8 TCCR1B |= (1 << WGM12) | (1 << CS11); // OCR1AH = (uint8_t)(CTC_MATCH_OVERFLOW >> 8); OCR1AL = (uint8_t) CTC_MATCH_OVERFLOW; // TIMSK1 |= (1 << OCIE1A); } unsigned long ticks() { unsigned long ticks_return; // , ticks_return // ATOMIC_BLOCK(ATOMIC_FORCEON) { ticks_return = timer1_ticks; } return ticks_return; }
قبل أن أعرض ملف الرأس مع CTC_MATCH_OVERFLOW ثابت معين ، نحتاج إلى العودة في الوقت المناسب إلى قسم
"المدة" وتحديد الماكرو الأكثر أهمية للحن ، والذي يحسب إيقاع اللحن. لقد انتظرت وقتًا طويلاً لتحديده ، لأنه مرتبط مباشرة باللاعب ، وبالتالي بالمؤقت 1.
في التقريب الأول ، بدا الأمر على هذا النحو (انظر الحسابات في قسم "المدة"):
#define TEMPO( x ) (60000 * 4 / x)
القيمة التي نحصل عليها في الإخراج يجب أن
نستبدل الوسيطة الأولى في الدالة
calc_note_delay . الآن نلقي نظرة فاحصة على دالة calc_note_delay ، وهي السطر:
return (precalced_tempo / divider);
نرى أن القيمة التي تم الحصول عليها عن طريق حساب ماكرو TEMPO مقسومة على مقسوم معين. تذكر أن الحد الأقصى
للمقسوم الذي حددناه هو
DEL_1N128 ، أي المقسوم عليه 128.
الآن لنأخذ قيمة الإيقاع المشتركة التي تساوي 240 ونقوم ببعض الحسابات البسيطة:
60000 * 4/240 = 1000يا رعب! حصلنا على 1000 فقط ، نظرًا لأن هذه القيمة ستظل مقسمة على 128 ، فإننا نخاطر بخطر الانزلاق إلى 0 بمعدلات عالية.
هذا هو العدد الثاني من المدة.كيفية حلها؟ من الواضح ، من أجل توسيع نطاق قيم الإيقاع ، نحتاج بطريقة ما إلى زيادة العدد الذي تم الحصول عليه عن طريق حساب ماكرو TEMPO. يمكن القيام بذلك بطريقة واحدة فقط - للابتعاد عن المللي ثانية وحساب الوقت في فترات زمنية معينة. الآن أنت تفهم لماذا طوال هذا الوقت تجنبت ذكر "مللي ثانية" في القصة. دعونا نحدد ماكرو آخر:
#define MS_DIVIDER 4
فليكن مقسومنا على المللي ثانية - قسّم المللي ثانية ، على سبيل المثال ، على 4 (250 μs).
ثم تحتاج إلى تغيير ماكرو TEMPO:
#define TEMPO( x ) (60000 * MS_DIVIDER * 4 / x)
الآن ، بضمير مرتاح ، سأعطي ملف الرأس للعمل مع المؤقت 1:
#ifndef TIMER1_TICKS_H_INCLUDED #define TIMER1_TICKS_H_INCLUDED #define MS_DIVIDER 4 #define CTC_MATCH_OVERFLOW ((F_CPU / 1000) / (8 * MS_DIVIDER)) void timer1_ticks_init(); unsigned long ticks(); #endif
يمكننا الآن تغيير MS_DIVIDER ، ضبط النطاق لمهامنا - لدي 4 في الكود الخاص بي - كان هذا كافيًا لمهامي.
تنبيه: إذا كان لا يزال لديك أي مهام "مرتبطة" بالمؤقت 1 ، فلا تنس أن تضرب / تقسم قيم التحكم في الوقت لها في MS_DIVIDER.القرص الدوار
الآن دعنا نكتب لاعبنا. أعتقد أن كل شيء سيكون واضحًا من الرمز والتعليقات.
int main(void) { timer1_ticks_init();
الخلاصة
آمل أن تكون هذه المذكرة مفيدة للقارئ المحترم ولنفسي ، حتى لا أنسى كل الفروق الدقيقة في تشغيل الألحان ، في حالة التقاط الميكروكونترولر AVR مرة أخرى.
حسنًا ، تقليديًا الفيديو وشفرة المصدر (طورته في بيئة Code Blocks ، لذلك لا تخف من الملفات الغامضة):
كود المصدر