با شما هستیم در قسمت هشتم آموزش میکروکنترلرهای AVR. در این قسمت، مبحث بسیار مهمی تحت عنوان وقفه یا اینتراپت را به شما همراهان رزدینو آموزش میدهیم. ابتدا مفهموم کلی وقفه را بیان میکنیم و سپس اولین مورد از وقفهها یعنی وقفه خارجی را به طور کامل تشریح خواهیم کرد. همراه ما باشید.
اینتراپت یا وقفه چیست؟
فرض کنید شما در خانه در حال مطالعه هستید و مطلع میشوید که قرار است نیم ساعت دیگر دایی شما شرفیاب شود. خب قطعا شما در طول این نیم ساعت، به مطالعه خود ادامه میدهید تا اینکه دایی شما از راه برسد. در این صورت مطالعه را رها و به استقبال دایی میروید. این حالت اول بود. در حالت دوم، مطالعه را رها کرده و نیم ساعت منتظر میمانید تا دایی شما از راه برسد. در این صورت تمام نیم ساعت شما صرف انتظار شده است و کار مفیدی انجام ندادهاید. انتخاب شما کدام مورد است؟ قطعا حالت اول.
حال دقیقا همین قضیه در میکروکنترلرهای avr (یا حتی میکروکنترلرهایی نظیر PIC یا ARM) صادق است. فرض را بر این میگذاریم که قرار است عملیات شماره 1 بصورت مداوم اجرا شود و همچنین با زدن یک کلید عملیات شماره 2 انجام گردد. در این صورت شما نمیتوانید بصورت مداوم CPU را در حال چک کردن پایهای که کلید به آن متصل است رها کنید. چون باعث میشود عملیات شماره 1 به حالت توقف درآید. اینجاست که وقفه به کمک ما میآید.
روند اجرایی یک وقفه
با راهاندازی وقفه (حال هر وقفهای که متناسب با کار ما باشد)، در حالت پیشفرض CPU در حال انجام عملیات شماره 1 است. ناگهان وقفه اتفاق میافتد. CPU عملیات شماره 1 را هرجایی که هست، رها کرده، عملیات شماره 2 را انجام داده و سرانجام به ادامه اجرای عملیات شماره 1 باز میگردد. تمام وقفهها از این روند تبعیت میکنند.
شکل بالا به خوبی روند اجرایی یک وقفه توسط CPU را نشان میدهد.
بیت وقفه عمومی
در تمام میکروکنترلرهای avr اگر بخواهیم از هرکدام از وقفهها استفاده کنیم، اول باید بیت وقفه عمومی روشن شود. بیت وقفه عمومی الزام تمام وقفههاست و در رجیستر SREG میکرو قرار دارد و با علامت I مشخص شده است.
بیت وقفه عمومی به دو صورت فعال میشود؛ اولین حالت فعال سازی بصورت زیر است.
1 2 |
SREG = SREG | 0x80; |
چون بیت I بیت شماره 7 رجیستر SREG است، باید مقدار 0x80 را با خود آن OR کرد و داخل خودش ریخت👆 راه دوم فعال سازی استفاده از دستور اسمبلی است.
1 2 |
#asm ("sei") |
با استفاده از asm# میتوان یک خط اسمبلی میان کدهای C نوشت. دستور فوق بیت I را set مینماید (یعنی 1 میکند).
انواع وقفهها
در میکروهایی نظیر mega8 یا mega16/32 ما به طور کل 21 وقفه داریم که هرکدام مربوط به یک بخش هستند. شکل زیر تمام وقفهها و دلیل وقوع آنها را شرح داده است.
در شکل بالا ستون Source نام وقفههای میکروکنترلر است و ستون Vector NO شماره وقفه است. این شماره از اهمیت بالایی برخوردار بوده و برای راهاندازی هرکدام از این وقفهها، به این شماره احتیاج است.
سرویس روتین وقفه
همانطور که گفتیم، هدف ما اجرای یک سری دستورات در صورت وقوع وقفه است. دستوراتی که قرار است اجرا شوند، داخل سرویس روتین وقفه نوشته میشوند. سرویس روتین وقفه با کلمه کلیدی interrupt مشخص شده و فرم آن به شکل زیر است.
1 2 3 4 |
interrupt[Vector شماره]void اسم دلخواه(void){ دستوراتی که با وقوع وقفه باید اجرا شوند } |
پس از نوشتن کلمه interrupt، داخل براکت 👈[ ] شماره Vector که شناسه منحصر به فرد وقفه است، نوشته میشود. با این کار معلوم میکنیم که این سرویس روتین مربوط به کدام وقفه است. کلمات کلیدی void ثابت هستند و فقط یک اسم برای سرویس روتین نیاز است (اسم دلخواه). در ادامه به بررسی مفهوم وقفه خارجی خواهیم پرداخت؛ با توجه به اهمیت این بحث، خواهشمندیم که این قسمت را با دقت بخوانید!
وقفه خارجی
وقفه خارجی به منظور تشخیص لبه یک سیگنال یا بصورت متداولتر برای خواندن کلیدها استفاده میشود. در تصویر قسمت بالا، وقفههای خارجی، INT1 ، INT0 و INT2 هستند و شماره Vector آنان به ترتیب 2 , 3 و 19 است. در شکل زیر پایههای مربوط به وقفه خارجی مشخص شدهاند.
برای راه اندازی این وقفهها 4 رجیستر در فضای SFR تعبیه شده است که عبارتاند از: MCUCR , GIFR , GICR و MCUCSR.
نکته: رجیسترهای نام برده دارای بیتهای غیر مرتبط با وقفه خارجی نیز هستند که ما در این آموزش با آنها کاری نداریم.
رجیستر General Interrupt Control Register) GICR)
برای فعالسازی هرکدام از وقفههای 0 ، 1 یا 2 باید از این رجیستر استفاده کرد.
در انتهای این رجیستر سه بیت INT1 , INT0 و INT2 قرار دارند که با یک کردن هرکدام از آنها وقفه مربوط فعال میشود. البته باید دقت کرد که بیت وقفه عمومی که قبل تر راجب آن توضیح دادیم هم فعال باشد.
رجیستر General Interrupt Flag Register) GIFR)
هرگاه وقفه خارجی رخ دهد، بیت متناظر با آن وقفه یک شده و CPU از این طریق متوجه رخ دادن وقفه میشود.
همانطور که در شکل بالا میبینید، سه بیت INTF1 , INTF0 و INTF2 که به آنها بیتهای Flag یا پرچم نیز گفته میشود، با رخ دادن وقفه 1 شده و CPU را مطلع میسازند. سپس CPU عملیات تعیین شده را انجام داده و در پایان به صورت اتوماتیک، همان بیت را 0 میکند.
رجیستر MCU Control Register) MCUCR)
این رجیستر نحوه چگونگی رخ دادن وقفه را معلوم میکند. مثلا وقفه با لبه بالارونده باشد یا لبه پایین رونده و … .
4 بیت کم ارزش این رجیستر متعلق به وقفههای خارجی 0 و 1 هستند. برای تنظیم این 4 بیت به شکل زیر توجه کنید.
دو بیت اول مربوط به وقفه شماره 0 و دو بیت دوم مربوط به وقفه شماره 1 است. هرکدام چهار حالت میتوانند داشته باشند:
- اگر 00 مقداردهی شوند، سطح 0 منطقی باعث ایجاد وقفه میشود.
- اگر 01 مقداردهی شوند، هر دو لبهی پایین رونده و بالارونده باعث ایجاد وقفه میشود.
- اگر 10 مقداردهی شوند، لبه پایین رونده وقفه ایجاد میکند.
- اگر 11 مقداردهی شوند، لبه بالا رونده وقفه ایجاد میکند.
رجیستر MCU Control and Status Register) MCUCSR)
همانند رجیستر MCUCR است. با این تفاوت که نحوه رخ دادن وقفه خارجی 2 را تنظیم میکند.
وقفه شماره 2 تنها یک بیت تنظیم به نام ISC2 دارد. اگر این بیت 0 شود، وقفه بصورت پایین رونده و اگر 1 شود وقفه بصورت بالارونده عمل میکند.
نکته: توصیه میشود رجیستر MCUCR و MCUCSR قبل از GICR و GIFR مقداردهی شود.
مثال اول
به عنوان اولین مثال میخواهیم برنامهای بنویسیم که با فشردن یک کلید (لبه بالا رونده وقفه شماره 0)، Led های متصل به پورت A به ترتیب روشن و خاموش شوند و در انتها همهی آنها خاموش گردد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <mega16.h> #include <delay.h> unsigned char i; void main(void){ DDRA = 0xFF; PORTA = 0x00; MCUCR = 0x03; GICR = 0x40; GIFR = 0x40; #asm ("sei") while(1); } interrupt[2] void ext0(void){ for(i=0;i<=7;i++){ PORTA = 0x01 << i; delay_ms(300); } PORTA = 0x00; } |
از main شروع میکنیم. پورت A را تماما خروجی کرده و مقدار آن را 0x00 میدهیم. سپس MCUCR را برابر 0x03 میکنیم. با اینکار مطابق جدول گفته شده، دو بیت ISC00 و ISC01 برابر 1 میشوند و وقفه در لبه بالا رونده رخ میدهد. حال باید خود وقفه فعال شود. با توجه به رجیستر GICR، باید بیت INT0 را یک کرد. پس مقدار 0x40 را اعمال میکنیم. همین مقدار را هم به GIFR میدهیم. در آخر هم بیت وقفه عمومی فعال شده و CPU وارد یک حلقه بدون دستور و بی پایان while میشود.
تا اینجای کار تنها وقفه فعال شد. در بیرون از main با کلمه interrupt که در بالا توضیح دادیم، سرویس روتین وقفه را مینویسیم. داخل براکت باید شماره Vector وقفه شماره 0 که عدد 2 است نوشته شود. نام دلخواه را هم ext0 نوشتیم. حال به عنوان دستورات اجرایی با نوشتن حلقه for تمام Led های پورت A یکی یکی روشن میشوند. در آخر هم پورت A را صفر میکنیم.
نکته: چون وقفه در لبه بالا رونده تنظیم شده است، با یک مقاومت 10K پین INT0 را pulldown کردیم تا در حالت عادی مقدار این پین 0 باشد.
مثال دوم
در دومین مثال برنامهای مینویسیم که رقص نور بصورت دائم در حال انجام باشد. اما کاری میکنیم که با هر بار رخ دادن وقفه، جهت آن برعکس شود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <mega16.h> #include <delay.h> unsigned char i; bit direct; void main(void){ DDRA = 0xFF; PORTA = 0x00; MCUCR = 0x03; GICR = 0x40; GIFR = 0x40; #asm ("sei") while(1){ for(i=0;i<=7;i++){ PORTA = (direct)? (0x01 << i) : (0x80 >> i); delay_ms(300); } } } interrupt[2]void ext(void){ direct = !direct; i = 7 - i; } |
دستورات اولیه main یکسان هستند. اما اینبار داخل while یک حلقه for نوشتیم که در هربار اجرا بسته به مقدار direct یکی از مقادیر را به پورت A نسبت میدهد. اگر direct یک باشد، مقدار (0x01 << i) به پورت ریخته میشود و اگر صفر باشد، مقدار (0x80 >> i) به پورت ریخته خواهد شد. در اولین حالت نمایش Led ها بصورت بالا رو و در دومین حالت بصورت پایین رو است.
اما برنامه سرویس روتین ساده است؛ کافی است تنها مقدار direct را برعکس کنیم. همینطور باید مقدار i هم عوض شود تا هنگام تعویض جهت حرکت، پرشی دیده نشود (دلیل اینکار پیچیده است! خودتان آن را تحلیل کنید😡).
خب دوستان این بخش هم هرچند نسبت به دیگر بخشها طولانیتر بود به اتمام رسید. مطمئنم هنوز سؤالات زیادی در مورد مبحث وقفه (و به خصوص وقفه خارجی) باقی مانده است؛ میتوانید در آخر همین پست سؤالات و یا مشکلات خود را مطرح کرده تا تیم ما در کوتاهترین زمان پاسخگوی شما عزیزان باشد.
برای آموزش بعدی مبحث تایمر کانتر کلیک نمایید.
14 پاسخ
چرا تو مثال ها از اول به رجیستر GIFR مقدار 0x40 دادید؟ مگه همون طور که توضیح دادید نباید هنگام وقوع وقفه تازه مقدار متناظرش ۱ بشه که CPU مطلع بشه؟
سلام. سوال خوبی بود؟ CPU به مقدار رجیستر GIFR همواره گوش میده. به محض اینکه یکی از بیت ها 1 شد تشخیص میده که وقفه اتفاق افتاده. حالا 2 راه واسه صفر کردن این بیت 1 شده هست؛ یا اینکه یدونه 1 روی همون بیت نوشته بشه و یا اینکه اگه سرویس روتین وقفه اجرا بشه، این بیت به صورت اتوماتیک 0 میشه. ولی بار اول که قراره رجیسترها رو کانفیگ کنیم، چون به صورت پیشفرض رجیستر GIFR تمامی بیت هاش برابر 1 هست، یه وقفه ناخواسته اتفاق میفته که با نوشتن روی GIFR از وقوع اون وقفه جلوگیری بشه.
ممنونم از پاسختون.
یه چیزی رو فقط متوجه نشدم؛ چطوری یدونه ۱ روی اون بیت بنویسیم اون بیت صفر میشه؟
چرا تمام بیت های رجیستر GIFR رو صفر نذاشتیم از اول، که حالا اگر وقفه ای رخ داد فقط اون بیت وقفه ۱ بشه و CPU مطلع بشه؟
دلیل اینکه 1 مینویسم که 0 بشه به خاطر ساختار منطقی میکروکنترلر هست. نوشتن 0 روی هر نوع بیتی که حالت Flag داشته باشه، هیچ تاثیری نداره.
اینکه میتونستم تمام بیت ها رو 0 کنیم ایرادی نداره. شما واسه 0 کردن تمام بیت کافیه روی تمام بیت ها 1 بنویسی.
خیلی ممنون از پاسختون و مطالب خوبی که گذاشتید.
خواهش میکنم. ویدئوهای آموزشی پیشرفته در آینده روی سایت قراره میگیره و شاید کامل ترین ویدئوهای آموزش AVR باشه که برای اولین بار توی ایران تدریس میشه.
من از سال 94 تا الان دنبال یه آموزش خوب وقفه و تایمر کانتر بودم به خوبی آموزش شما ندیدم ممنونت
ممنون از انرژی خوبتون. سعی ما اینه مطالب کامل و جامع باشن.
سلام
سپاس از مطلب خوب و کاملتون
فوق العاده بود. خیلی مختصر و مفید و کاربردی. ممنون
سلام و درود بر استاد نصر که علم و تجربه اش متناسب با نیاز در اختیار ما میگذارد ممنون شما هستیم
سلام. ممنون از لطفتون 🌹🌹🌹🌹
خدا قوت ،خیلی perfect
میشه یک برنامه ای بنویسیم :
تا زمانی که کلید رو زده و نگه داشتیم چراغ چشمک بزنه ، وقتی کلید رها شد چراغ خاموش باشه ؟؟