در قسمت ششم از آموزش STM32 با توابع LL، در رابطه با GPIO در حالت ورودی صحبت کردیم. نحوهی کار به این صورت بود که مقدار رجیستری که مربوط به پینهای میکروکنترلر در حالت ورودی بود را میخواندیم و پس از آن، متناسب با آن مقدار، متغییری را افزایش و بر روی سون سگمنت نشان میدادیم.
در این قسمت میخواهیم در رابطه با Interrupt یا وقفه صحبت کنیم.
ابتدا در مورد اینکه Interrupt چیست صحبت میکنیم، سپس توضیح میدهیم که در میکروکنترلرهای STM32، وقفهها به چه صورتی عمل میکنند و به چه نحوی در میکروکنترلر جاسازی شدهاند و در نهایت این واحد را به صورت عملی راهاندازی خواهیم کرد.
Interrupt
همانطور که از واژهی Interrupt مشخص است، در روند یا انجام کاری، وقفه ایجاد میکند. در دیجیتال و مشخصا در میکروکنترلرها نیز، Interrupt به همین معناست و در روند کار CPU وقفه ایجاد میکند.
زمانی که CPU در حال انجام کار و روال عادی خود است با وقوع وقفه، CPU برای مدتی روال عادی که در حال اجرای آن بود را متوقف کرده و زیر روال مربوط به وقفهای که اتفاق افتاد را اجرا میکند. (البته توضیحات بالا به صورت کلی بیان شده است، جزئیات این کار را در ادامه به صورت دقیقتر بررسی میکنیم)
اجازه بدهید با یک تشبیه وقفه را تشریح بکنم، تا هم مفهموش را بهتر متوجه شوید و هم کاربرد وجود وقفه را لمس کنید.
فرض کنید در خانه نشستید و در حال انجام کارهایتان هستید، در عین حال منتظر دوستتان هستید که میخواهد پیش شما بیاید. یک راه این است که چشم از پنجره برندارید، و منتظر بمانید تا دوستتان بیاید و وقتی رسید در را بر روی او باز کنید، راه دیگر این است که با خیال راحت کارهایتان را انجام بدهید و هر موقع صدای زنگ خانه را شنیدید در را بر روی دوستتان باز کنید. در اینجا زنگ خانه نقش وقفه را دارد و با به صدا در آمدن زنگ، وقفهای به شما میرسد که باید کار در حال انجام را برای مدتی رها کنید و به وقفه رسیده شده، رسیدگی کنید.
در میکروکنترلر هم همچین اتفاقی رخ میدهد، و با وقوع وقفه، کار در حال انجام رها شده و به وقفه رسیده شده، رسیدگی میشود. فکر میکنم با توضیحات بالا به خوبی متوجه شده باشید که ذات وقفه چیست و چرا باید وجود داشته باشد.
خب تا الان کلیات وقفه را بررسی کردیم و با دلیل وجود و کاربرد وقفه یا همان Interrupt آشنا شدیم. اکنون میخواهیم ساختار Interrupt را به صورت دقیقتر در پردازندههای Cortex-M، و مشخصا در میکروکنترلرهای STM32 بررسی کنیم.
وقفه یا Interrupt در میکروکنترلرهای STM32 سری F1
ما در میکروکنترلرهای STM32 انواع وقفهها را داریم، مثل وقفههای خارجی، وقفهی تایمر، وقفهی مبدل آنالوگ به دیجیتال (ADC)، وقفههای پروتکلها و ارتباطات مثل پروتکل UART و تعداد زیادی وقفه دیگر وجود دارد که مربوط به مدیریت سختافزار درون خود میکروکنترلر میشود.
وقفههای این دسته از میکروکنترلرها دارای یک سری قابلیت ویژه هستند که در ادامه، این قابلیتهای ویژه را با هم بررسی میکنیم.
NVIC (Nested vectored interrupt controller )
NVIC به معنای کنترلکننده بردار وقفههای تو در تو. در میکروکنترلرهای ARM برخلاف میکروکنترلرهای AVR، وقفهها دارای اولویت هستند. اجازه بدهید کمی بیشتر در رابطه با وقفههای تو در تو صحبت کنیم تا موضوع به خوبی تفهیم شود.
در میکروکنترلرهای AVR نحوهی کار به این صورت است که اگر وقفهای رخ بدهد، تابع سرویسدهنده مربوط به این وقفه اجرا میشود، حال اگر در حین اجرای این تابع، وقفهی دومی رخ بدهد هیچ اتفاقی نمیافتد، در واقع موقعی به وقفهی دوم رسیدگی میشود که کدهای درون تابع سرویسدهنده به وقفهی اول به طور کامل اجرا شده باشند.
در میکروکنترلرهای ARM هم روند کار مانند AVR است اما با یک تفاوت بسیار مهم و اساسی. فرض کنید وقفهی اول رخ داده است و میکروکنترلر در حال رسیدگی به این وقفه است، در همین حین وقفهی دومی رخ میدهد، اکنون میکروکنترلر بر اساس شرایطی تصمیم میگیرد که به وقفهی دوم پاسخ بدهد، یا رسیدگی به همان وقفهی اول را ادامه بدهد.
این شرایط چیست؟
در میکروکنترلرهای ARM هر وقفه دارای یک اولویت است و واحد NVIC با توجه به این اولویتها به وقفهها رسیدگی میکند. پس زمانی که وقفهی اول در حال انجام است اگر وقفهی دومی رخ بدهد، تنها در صورتی به وقفهی دوم رسیدگی میشود که اولویت بالاتری داشته باشد، در غیر این صورت آن را نادیده میگیرد تا عملیات مربوط به وقفهی اول به پایان برسد.
حال فرض کنیم که عملیات مربوط به وقفهی اول در حال اجرا است و وقفهی دومی که اولویت بالاتری دارد رخ میدهد، چه اتفاقی میافتد؟ عملیات مربوط به وقفهی اول که در حال اجرا بود برای مدتی رها میشود و به وقفهی دوم رسیدگی میشود. پس از اینکه عملیات مربوط به وقفهی دوم به صورت کامل انجام گرفت، به ادامهی عملیات مربوط به وقفهی اول رسیدگی خواهد شد.
برای اینکه مفاهیم توضیح داده شده در بالا را به خوبی متوجه بشوید، به تصویر زیر دقت بکنید.
در میکروکنترلرهای سری F1 ما میتوانیم 16 اولویت وقفه تعیین کنیم. تنظیم این اولویتها با استفاده از 4 بیت که در مجموع شامل 16 حالت مختلف را شامل میشود، صورت میگیرد.
همانطور که گفتیم تعداد وقفهها بسیار زیاد است و پرداختن به تمامی این وقفهها در یک مقاله ممکن نیست، پس در این مقاله فقط به وقفههای خارجی میپردازیم و سایر وقفهها را در قسمتهای مربوطه توضیح خواهیم داد.
External interrupt
در میکروکنترلرهای سری F1 در مجموع 16 وقفه خارجی بر روی پینهای GPIO وجود دارد.
ابتدا به تصویر زیر که جانمایی و چگونگی این وقفهها را نشان میدهد توجه کنید.
همانطور که از تصویر بالا مشخص است هر شماره از خط یا لاین وقفه خارجی با استفاده از یک مالتی پلکسر به چندین پین از GPIO با همان شماره متصل است.
از تصویر بالا میتوان استنتاج کرد که اگر به عنوان مثال وقفهای بر روی لاین شماره 1 یا همان EXTI1 رخ داد، این وقفه مربوط به یکی از پینهای PA1 تا PG1 میشود. اینکه دقیقا کدام یک از این پینها عامل وقفه بوده است را ما خودمان از قبل تعیین کردیم. ما در یک رجیستر تعیین میکنیم که خروجی مالتی پلکسر کدام یک از ورودیها باشد.
به این نکته دقت کنید که ما به صورت همزمان نمیتوانیم دو پین هم شماره از دو پورت مختلف مثل PA1 و PB1 را به عنوان وقفه خارجی تعریف کنیم، دلیلش هم در دل تصویر و توضیحات بالا نهفته است، اگر دلیل این موضوع را متوجه نشدید دوباره برگردید تصویر بالا را ببینید و توضیحات را یک بار دیگر به دقت بخوانید.
ISR (Interrupt service routine)
زمانی که یک وقفه رخ میدهد، زمان رسیدگی به آن وقفه فرا میرسد. حال سوال اینجاست که چگونه و به چه نحوی باید به این وقفه رسیدگی شود و اصلا ما از کجا میتوانیم متوجه بشویم که وقفه رخ داده است.
برای شرح دقیق اینکه هنگام وقوع وقفه دقیقا چه اتفاقاتی رخ میدهد، باید وارد جزئیات پردازنده Cortex-M3 بشویم. ذکر این جزئیات از حوصله این مقاله خارج است و به علاوه میتواند باعث سردرگمی شما شود. اما نگران نباشید من در ادامه به صورت خیلی مختصر و ساده و البته به طوری که قابل فهم باشد این موضوع را به شما توضیح خواهم داد.
اینکه چگونه باید متوجه بشویم که وقفه رخ داده است بسیار ساده است. با بررسی بیت متناظر با هر وقفه در رجیستر Pending register متوجه خواهیم شده که وقفه رخ داده است یا خیر.
اما موضوع اصلی اینجاست که وقتی متوجه شدیم که وقفه رخ داده است چگونه باید به آن رسیدگی کنیم. وظیفه رسیدگی به وقفه با ISR میباشد. ISR یا ترجمه آن، “روال سرویس وقفه” در زمان وقوع وقفه به صورت سختافزاری فعال میشود و کدهایی که ما نیاز داریم در زمان وقفه اجرا شود را اجرا میکند. دقت کنید که فعال شدن ISR به صورت سختافزاری میباشد و اصلا نیازی نیست که به صورت نرمافزاری کاری انجام بدهیم. فقط باید کدهایی که میخواهیم در زمان فعال شدن ISR اجرا شود را درون تابع مربوط به آن بنویسیم.
پس نتیجه میگیریم که هنگام وقوع یک وقفه خودکار و به صورت سختافزاری ISR مربوط به آن وقفه فعال میشود و ما باید کدهایمان را در قسمت مشخص شده در درون یک تابع بنویسیم تا در زمان وقوع وقفه اجرا شوند. اینکه محل نوشتن این کدها در کجای برنامه و در درون چه تابعی باشد را در ادامه، زمانی که وارد نرمافزار Keil و محیط برنامهنویسی شدیم به شما خواهم گفت.
همانطور که گفتیم ما در مجموع 16 وقفه خارجی که بر روی پینهای GPIO جانمایی شدهاند، داریم. آیا هر کدام از این وقفههای خارجی، ISR مربوط به خودشان را دارند؟ خیر، ممکن است چندین وقفه خارجی، همگی با هم فقط یک ISR داشته باشند.
در میکروکنترلرهای سری F1 که پردازنده آنها Cortex-M3 است، وقفههای خارجی شماره 0 تا 4 هر کدام به صورت جداگانه یک ISR دارند، وقفههای شماره 5 تا 9 همگی یک ISR و وقفههای شماره 10 تا 15 هم همگی یک ISR دارند.
تا اینجا به خوبی با مفاهیم اولیه آشنا شدیم و جزئیات مربوط به وقفهها را به خوبی میدانیم، اکنون وقت آن است که وارد نرمافزار بشویم و تمامی این مفاهیم را به صورت عملی پیادهسازی بکنیم.
ما قصد داریم که وقفه خارجی را بر روی پین PA1 فعال و این پین را به یک کلید خارجی متصل بکنیم. حال اگر این کلید 5 بار فشرده شد، یک LED بر روی پین PA0 روشن و اگر 5 بار دیگر فشرده شد همین LED خاموش شود.
البته یک مدار خارجی که در زیر مشاهده میکنید بر روی پین PA1 قرار میدهیم تا دیبانس (اگر در مورد دیبانس نمیدانید در انتهای همین مقاله کامنت بگذارید) کلید از بین برود و با هر بار فشردن کلید تنها یک بار وقفه رخ بدهد.
کلاک و دیباگ را مانند قسمتهای گذشته تنظیم میکنیم. پین PA0 را بر روی GPIO_Output و پین PA1 را بر روی GPIO_EXTI1 قرار میدهیم:
وقفه را نیز بر روی لبهی پایین رونده با اولویت 0 تعریف میکنیم:
پس از انجام مراحل بالا از پروژه خروجی میگیریم و وارد محیط برنامهنویسی میشویم.
ما ابتدا یک متغیر تعریف میکنیم و با وقوع وقفه این متغیر را یک واحد افزایش میدهیم. چگونه باید با وقوع وقفه متغیر را یک واحد افزایش داد؟ همانطور که قبلا گفتیم با وقوع وقفه، ISR مربوط به آن وقفه فعال میشود. پس از فعال شدن ISR، تابعی فراخوانی میشود و هر کدی درون آن تابع باشد اجرا میشود. حال ما باید این تابع را پیدا کنیم و کدمان که همان افزایش متغیر است را درون این تابع بنویسیم.
توابع مربوط به وقفهها درون فایل stm32f1xx_it.c وجود دارند، ما باید به این فایل برویم و تابع مربوط به وقفهی خارجی که بر روی پین PA1 رخ میدهد را پیدا کنیم. اسم تابع مربوط به این وقفه EXTI1_IRQHandler است. ما باید طبق کد زیر، درون تابع متغیر را افزایش بدهیم:
void EXTI1_IRQHandler(void) { /* USER CODE BEGIN EXTI1_IRQn 0 */ /* USER CODE END EXTI1_IRQn 0 */ if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_1) != RESET) { LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_1); /* USER CODE BEGIN LL_EXTI_LINE_1 */ i++; /* USER CODE END LL_EXTI_LINE_1 */ } /* USER CODE BEGIN EXTI1_IRQn 1 */ /* USER CODE END EXTI1_IRQn 1 */ }
درون تابع و در کد بالا، ما با یک شرط چک کردیم که وقفهای بر روی لاین 1 یا همان پین PA1 اتفاق افتاده است یا نه، و اگر وقفه اتفاق افتاد، ابتدا بیت مربوط به این وقفه را پاک کن و سپس به متغیر i، یک واحد اضافه کن. از این به بعد هر موقع وقفهای بر روی پین PA1 رخ داد؛ ISR به صورت خودکار فعال و تایع EXTI1_IRQHandler فراخوانی میشود و متغیر یک واحد افزایش مییابد.
در اینجا ممکن است بپرسید که دلیل گذاشتن شرط و همچنین پاک کردن بیت مربوط به وقفه چیست!
خب همانطور که گفتیم بعضی از وقفهها همگی با هم تنها یک ISR و یک تابع دارند و ما باید حتما درون تابع، با استفاده از شرط بررسی کنیم که کدام یک از وقفهها رخ داده است تا دستورات هر وقفه متناظر با همان وقفه اجرا بشوند.
بیت مربوط به وقفه را هم به این دلیل پاک میکنیم که با هر بار فراخوانی تابع، مدهای درون شرط وقفهای که رخ نداده است، اجرا نشود.
شاید توضیحات بالا کمی برایتان گنگ باشد، اجازه بدهید با یک مثال این موضوع را برای شما بیشتر توضیح بدهم.
همانطور که میدانید وقفههای خارجی شماره 5 تا 9 همگی با هم یک ISR و یک تابع دارند. پس هر کدام از وقفههای 5 تا 9 رخ بدهد، تنها یک تابع فراخوانی میشود. حال فرض کنید ما میخواهیم اگر وقفه شماره 5 رخ داد، متغیر A و اگر وقفه شماره 6 رخ داد، متغیر B یک واحد افزایش پیدا کند. اگر درون تابع، بدون هیچ شرطی متغیر A و B را افزایش بدهیم، با هر کدام از وقفههای شماره 5 و 6 هم متغیر A افزایش پیدا میکند و هم متغیر B. این عملکرد مدنظر ما نیست و هیچ کنترلی روی وقفهها هم نداریم، پس با این تفاسیر وجود شرط الزامی است.
اکنون در نظر بگیرید که ما برای هر وقفه درون تابع از یک شرط استفاده کردیم، اما بیتهای مربوط به وقفه را پاک نکردیم، چه اتفاقی رخ میدهد؟
فرض کنید وقفه شماره 5 رخ میدهد و تابع فراخوانی میشود و درون شرط مربوط به این وقفه، فقط متغیر A یک واحد افزایش پیدا میکند و بیت مربوط به این وقفه را پاک نمیکنیم.
در دفعه بعد فرض کنید وقفه شماره 6 رخ میدهد و تابع فراخوانی میشود، چه اتفاقی رخ میدهد؟ چون درون شرط مربوط به وقفه شماره 5، بیت مربوط به وقفه را پاک نکردیم، شرط مربوط به وقفه 5 همچنان برقرار است و با وجود اینکه فقط وقفه شماره 6 رخ داده است متغیر A دوباره افزایش پیدا میکند که مطلوب ما نیست. پس الزامی است که درون هر شرط بیت مربوط به وقفه را پاک کنیم.
حال درون main برنامه و در حلقه while باید کد زیر را قرار بدهیم:
if (i > 5 && State == 0) { LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); i = 0; State = 1; } if (i > 5 && State == 1) { LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0); i = 0; State = 0; }
خب همانطور که گفتیم درون فایل stm32f1xx_it.c و در تابع EXTI1_IRQHandler مشخص کردیم که با هر بار وقوع وقفه، متغیر i یک واحد افزایش پیدا کند. درون main برنامه هم که در بالا مشاهده میکنید کارهای کنترلی بر روی متغیر i که منجر به خاموش و روشن شدن LED میشود را انجام میدهیم.
تا ابنجا هر آن چیزی که در رابطه با وقفه خارجی نیاز بود را مفصلا شرح دادم. سایر وقفهها را نیز در قسمتهای مربوطه به صورت کامل توضیح خواهم داد.
در قسمت هشتم در رابطه با UART-Transmit صحبت خواهیم کرد.
منبع:سیسوگ