دو روش اصلی برای مدیریت ورودی/خروجی (I/O) وجود دارد:
در روش بررسی مداوم، برنامه به طور مکرر از دستگاه میپرسد که آیا دادهای برای خواندن آماده است یا خیر. در مقابل، در روش وقفهها، دستگاه زمانی که دادهای برای خواندن آماده میشود، جریان عادی برنامه را قطع میکند تا به برنامه اطلاع دهد.
این فصل به آنالیز تفاوت بین بررسی مداوم و وقفهها میپردازد و نحوه عملکرد وقفهها را شرح میدهد تا بتوانید از آنها برای نوشتن رشتهای روی پورت سریال به طور کارآمدتر (بله، دوباره “Hello World!”) استفاده کنید.
بیایید ببینیم بررسی مداوم (Polling) و وقفهها (Interrupts) در مورد تلفن چگونه کار میکنند. با بررسی مداوم، زنگ تلفن خاموش است و شما باید هر 10 ثانیه یکبار تلفن را چک کنید تا ببینید آیا تماسی برقرار شده است یا خیر. شما باید کنار تلفن بنشینید و به هیچوجه خسته نشوید. این روشی است که در برنامه سریال قبلی خود در فصل 9 استفاده کردیم و اساساً شبیه گفتگوی زیر است:
«آیا مشغول هستی؟» «بله.»
«آیا مشغول هستی؟» «بله.»
«آیا مشغول هستی؟» «بله.»
«آیا مشغول هستی؟» «نه.» «این کاراکتر بعدی است.»
کامپیوتر در یک حلقه بررسی مداوم گیرکرده و منتظر میماند تا رجیستر وضعیت UART نشان دهد که UART برای کاراکتر بعدی آماده است. در این مرحله، کامپیوتر کار دیگری ندارد و خسته نمیشود. مزیت اصلی بررسی مداوم، درک و پیادهسازی آسان آن است.
بیایید دوباره به تلفن برگردیم، اما این بار از روش وقفه استفاده میکنیم. شما دائماً کنار تلفن نمینشینید تا ببینید آیا تماسی برقرار شده است یا خیر. در عوض، به کارهای عادی خود میپردازید تا زمانی که تلفن زنگ میخورد (وقفهای رخ میدهد). سپس همه چیز را رها میکنید، به سمت تلفن میدوید و گوشی را برمیدارید – فقط برای اینکه متوجه شوید تماس تبلیغاتی دیگری است، برای محصولی که بههیچوجه حاضر به خرید آن نیستید.
توالی کلیدی رویدادها در سناریوی وقفه به شرح زیر است:
به کارهای عادی خود میپردازیم. یک وقفه دریافت میکنیم (تلفن زنگ میخورد). گوشی را برمیداریم (اجرای سرویس وقفه)، فریاد میزنیم «نه، من نمیخواهم یک ست ریشتراش و خودنویس ترکیبی بخرم» و گوشی را قطع میکنیم. از همان جایی که کارمان را متوقف کرده بودیم، کارهای عادی خود را از سر میگیریم.
همانطور که در قسمت قبل فهمیدیم تنها زمانی میتوانیم کاراکترهایی را به UART ارسال کنیم که رجیستر دادهی ارسالی (TDR) خالی باشد. شکل 10-1 برای نشاندادن نحوه کار TDR، نمودار بلوکی بخشی از UART را نشان میدهد.
شکل 10-1: سختافزار ارسال UART
هنگامی که میخواهیم یک کاراکتر ارسال کنیم، آن را در TDR که 8 بیت ظرفیت دارد، قرار میدهیم. سپس کاراکتر به شیفت رجیستر ارسال (transmit shift register (TSR)) که 10 بیت ظرفیت دارد، منتقل میشود. 2 بیت اضافی، بیت شروع در ابتدای کاراکتر و بیت توقف در انتهای کاراکتر هستند. سپس TSR دادهها را به صورت تکبیتی از خط ارسال سریال (transmit serial (TX)) خارج میکند.
هنگامی که دادهها از TDR به TSR منتقل میشوند، TDR خالی میشود و آماده دریافت کاراکتر دیگری است.
حلقه بررسی مداومی که تابهحال استفاده کردهایم به شکل زیر است:
منتظر بمان تا پرچم خالی بودن ارسال (TXE) تنظیم شود//
1 2 3 | while ((uartHandle.Instance->ISR & UART_FLAG_TXE) == 0) continue; |
به زبان ساده، این کد میگوید: «سرت خلوت شد؟ سرت خلوت شد؟ سرت خلوت شد؟ و تکرار این جمله در زبان C همانند زبان فارسی آزاردهنده است؛ اما سادهترین کاریست که میشود انجام داد که البته همانطور که گفتیم مزیت اصلی “بررسی مداوم” سادگی آن است.
راه دیگر برای ارسال کاراکترها این است که به سیستم بگوییم میخواهیم یک وقفه، زمانی که UART آماده دریافت کاراکتر دیگری است، رخ دهد. یک تابع وقفه بهصورت اتوماتیک زمانی که رویدادی رخ میدهد صدا زده میشود. در پروژهی ما این رویداد همان خالیشدن TDR است.
با وقفهها، به پردازنده میگوییم: «میخواهم بروم و کارهای مفیدی انجام دهم. وقتی TDR خالی شد، میخواهم جریان عادی را قطع کنی و یک تابع روتین وقفه را صدا بزنی تا بتوانم کاراکتر بعدی را به تو بدهم.»
هنگامی که یک وقفه رخ میدهد، CPU یک تابع روتین وقفه را فراخوانی میکند که در آدرس ثابتی قرار دارد و توسط طراحی CPU تعیین میشود. CPUهای اولیه یک آدرس برای تمام وقفهها داشتند، بنابراین کد باید تعدادی بررسی را برای تشخیص منشأ وقفه انجام میداد:
1 2 3 4 5 6 7 8 9 | if (diskInterrupt) { handleDiskInterrupt(); return;} if (serialInterrupt) { handleSerialInterrupt(); return;} if (keyboardInterrupt) { handleKeyboardInterrupt(); return;} if (mouseInterrupt) { handleMouseInterrupt(); return;} logUnknownInterrupt(); |
امروزه حتی یک تراشهی ساده میتواند دستگاههای مختلف زیادی داشته باشد. بررسی همه آنها برای پیداکردن دلیل وقفه، فرایندی زمانبر است. در نتیجه، تراشهها (از جمله تراشهی ARM ما) اکنون از وقفههای برداری استفاده میکنند، به این معنی که هر دستگاه جانبی آدرس وقفهی خاص خود را دارد. وقفههای مربوط به UART1 یک تابع روتین وقفه را در آدرسی فراخوانی میکنند، درحالیکه وقفههای مربوط به UART2 به آدرس دیگری (به طور خاص USART2_IRQHandler) میروند و همینطور برای سایر دستگاههای جانبی.
برای مثال در میکروی stm32f030 بردار وقفه در فایل startup/startup_stm32f030x8.S تعریف شده است:
1 2 3 4 5 6 7 | g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler |
بسیاری از هندلرهای دیگر:
1 2 3 | .word USART1_IRQHandler /* USART1 */ .word USART2_IRQHandler /* USART2 */ |
بعداً، کد نماد USART2_IRQHandler را تعریف میکند:
1 2 3 | .weak USART2_IRQHandler .thumb_set USART2_IRQHandler,Default_Handler |
دستورالعمل دوم (.thumb_set) رویه USART2_IRQHandler را همان Default_Handler تعریف میکند.
اولین دستورالعمل (.weak) آن را بهعنوان یک نماد ضعیف تعریف میکند. اگر یک نماد معمولی بود و ما سعی میکردیم USART2_IRQHandler خود را تعریف کنیم، لینکر با پیام خطای “(نماد تکراری)Duplicate symbol ” کار را متوقف میکرد. بااینحال، ازآنجاییکه نماد ضعیف است، لینکر تعریف ضعیف را دور میاندازد و از تعریفی که ما ارائه میدهیم استفاده میکند. به عبارتی در حالتی که تعریف تابع بهصورت weak باشد ما میتوانیم تابع دیگری همنام با آن تابع تعریف کنیم بدون اینکه کامپایلر خطایی بگیرد. در این حالت تابعی که ما تعریف کردهایم اجرا میشود؛ اما اگر ما چیزی تعریف نکنیم همان تابع weak اجرا میشود.
گاهی اوقات در مستندات STM، پورت سریال I/O ما هم بهعنوان پورت UART (ورودی/خروجی آسنکرون یا ناهمزمان) و هم بهعنوان پورت USART (ورودی/خروجی سنکرون یا همزمان) برچسبگذاری میشود. این پورت بسته به پیکربندی میتواند هر کدام از این دو حالت را داشته باشد؛ بنابراین، ما یک پورت سریال I/O داریم که دو نام دارد: USART و UART. البته خیلی بهتر میبود که شرکت STM از یک نام مشترک استفاده کند.
فایل startup/startup_stm32f030x8.S بعداً Default_Handler را تعریف میکند:
1 2 3 4 5 6 7 | .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop |
پاسخ پیشفرض به یک وقفه، حلقه بینهایت است که ماشین را کاملاً بیاستفاده میکند البته بهتر است بگوییم تقریباً بیاستفاده زیرا ماشین همچنان به دیباگر و راهاندازی مجدد (ریست) پاسخ میدهد.
ما USART2_IRQHandler خود را برای پاسخگویی به خالی بودن TDR مینویسیم و بدینوسیله هندلر پیشفرض را با چیزی مفیدتر جایگزین میکنیم.
حالا بیایید برنامه ورودی/خروجی سریال خود را از فصل 9 تغییر دهیم تا به جای بررسی مداوم، از وقفهها برای نوشتن یک رشته استفاده کند. تمام ارتباطی که بین لایه بالایی (برنامه اصلی) و لایه پایین (روتین وقفه) برقرار میشود، یک متغیر سراسری واحد است:
const char* volatile usart2String = NULL; // رشتهای که در حال ارسال هستیم.
واژه کلیدی const به کامپایلر C میگوید که داده کاراکتر ثابت است و ما هرگز سعی نمیکنیم آن را تغییر دهیم. واژه کلیدی volatile به کامپایلر C میگوید که این متغیر ممکن است در هر زمانی توسط چیزی خارج از جریان یک برنامه C معمولی، مانند یک تابع وقفه، تغییر کند.
برای اینکه موضوع باتوجهبه پیچیدگی دستور زبان C در این نقطه روشنتر شود، const قبل از اعلان char ظاهر میشود و به معنی ثابتبودن داده کاراکتر است. const بعد از عملگر اشارهگر (*) ظاهر نمیشود، بنابراین اشارهگر ثابت نیست و میتواند تغییر کند. کلمه کلیدی volatile بعد از عملگر اشارهگر ظاهر میشود و نشان میدهد که اشارهگر ممکن است تغییر کند. نبود const بعد از عملگر اشارهگر به این معنی است که برنامه میتواند این مقدار را تغییر دهد.
میتوان یک متغیر const volatile داشت که به کامپایلر C میگوید برنامه نمیتواند مقدار متغیر را تغییر دهد، اما چیزی خارج از محدوده برنامه میتواند آن را تغییر دهد. بهعنوانمثال، رجیستر دریافت UART میتواند const volatile باشد. ما نمیتوانیم آن را تغییر دهیم، اما هر بار که یک کاراکتر وارد میشود، تغییر میکند.
ما باید با متغیرهایی که توسط هر دولایه استفاده میشوند، محتاط باشیم. خوشبختانه، در این مثال فقط یک متغیر، یعنی usart2String را استفاده میکنیم. لیستهای زیر گردش کار این متغیر را نشان میدهند:
هر دولایه بالایی و پایینی اشارهگر را افزایش میدهند. ما باید هنگام فعالکردن وقفه بسیار مراقب باشیم. (مطمئن شوید هر دولایه سعی نمیکنند همزمان از اشارهگر استفاده کنند.) لایه بالایی تا زمانی که usart2String برابر NULL نشود کاری انجام نمیدهد و لایه پایین فقط زمانی usart2String را روی NULL تنظیم میکند که اطلاعات تمام شده باشند و وقفه UART2 را غیرفعال کند. لایه بالایی با فعالنکردن وقفهها تا بعد از انجام افزایش، از خود محافظت میکند؛ بنابراین، روتین وقفه نمیتواند کد را دستکاری کند.
روالی که توضیح دادیم بسیار مهم است. اگر این روند یا اینجام نشود و یا بهدرستی انجام نشود میتواند باعث شود اجرای برنامه با شکست روبهرو شود. مشکل بزرگتر البته این است که چنین شکستهایی بهصورت تصادفی و در زمانهای غیرقابلپیشبینی رخ میدهند که پیداکردن دلیل و رفع آن (دیباگکردن آن) یکی از سختترین کارهاییست که ممکن است با آن مواجه شوید.
بهعنوان یک تجربهی شخصی من حدود سه سال را صرف یافتن یکی از این باگها کردم. این مشکل فقط برای یک مشتری و فقط حدود یکبار در هر دو ماه رخ میداد. ما قادر به بازتولید آن در آزمایشگاه نبودیم. خوشبختانه، مشتری بسیار خوشاخلاق بود و تمایل داشت برای حل مشکل با ما همکاری کند. در ادامه این فصل، بررسی میکنیم که چه اتفاقی میافتد زمانی که این تحلیل انجام نمیشود و در مورد برخی از تکنیکها برای تشخیص باگهای مرتبط با وقفه صحبت میکنیم.
کد 10-1 حاوی برنامه ورودی/خروجی سریال مبتنی بر وقفه است.
1 | /** |
@brief ارسال پیام “Hello World” از طریق ورودی/خروجی سریال.
از اینتراپتها بهجای پولینگ استفاده شده است.
1 2 3 4 5 6 7 | */ #include <stdbool.h> #include "stm32f0xx_nucleo.h" #include "stm32f0xx.h" |
const char hello[] = “Hello World!\r\n”; // پیامی که قرار است ارسال شود.
int current; // کاراکتر در پیامی که در حال ارسال آن هستیم.
1 | UART_HandleTypeDef uartHandle; |
مقداردهی اولیه //UART
//… Error_Handler مشابه با کد 9-3) (…
const char* volatile usart2String = NULL; // رشتهای که در حال ارسال آن هستیم.
1 | /** |
@brief کنترلکردن اینتراپت USART2.
به طور جادویی توسط سیستم اینتراپت تراشه فراخوانی میشود.
نام ثابت است به دلیل کد راهاندازی که بردار اینتراپت را پر میکند.
1 2 3 4 5 6 7 | */ void USART2_IRQHandler(void) { if ((uartHandle.Instance->ISR & USART_ISR_TXE) != 0) { |
// این نباید هرگز اتفاق بیفتد، اما ما نمیخواهیم در صورت بروز این اتفاق کرش کنیم.
1 | if (usart2String == NULL) { |
// خاموشکردن اینتراپت.
1 2 3 4 5 6 7 | uartHandle.Instance->CR1 &= ~ (USART_CR1_TXEIE); return; } if (*usart2String == '\0') { |
// ما از رشته استفاده کردهایم.
1 | usart2String = NULL; |
// خاموشکردن اینتراپت.
1 2 3 4 5 6 7 | uartHandle.Instance->CR1 &= ~ (USART_CR1_TXEIE); return; } uartHandle.Instance->TDR = *usart2String; |
ارسال کاراکتر به UART. //
++usart2String; // اشاره به کاراکتر بعدی.
1 2 3 4 5 | return; } { |
ازآنجاکه تنها اینتراپتی که فعال کردهایم TXE است، نباید به اینجا برسیم. //
// هنگامی که سایر اینتراپتها را فعال میکنیم، باید کدی برای مدیریت آنها در اینجا قرار دهیم.
نسخه خودمان از تابع puts
رشته دقیق داده شده را به خروجی ارسال میکند.
@param str رشتهای که ارسال میشود.
@note فرض میشود که str خالی نیست و به رشته خالی اشاره نمیکند.
1 2 3 4 5 | */ void myPuts(const char* str) { |
// اگر کسی رشتهای ارسال کند، منتظر آن باشید.
1 2 3 | while (usart2String != NULL) continue; |
// به روت اینتراپت میگوییم که از چه رشتهای استفاده کند.
1 | usart2String = str; |
ارسال کاراکتر به UART. //
1 | uartHandle.Instance->TDR = *usart2String; |
// اشاره به کاراکتر بعدی.
1 | ++usart2String; |
// فعالکردن اینتراپت.
1 2 3 4 5 6 7 | uartHandle.Instance->CR1 |= USART_CR1_TXEIE; } int main(void) { |
HAL_Init(); // مقداردهی اولیه سختافزار.
1 2 3 | led2_Init(); uart2_Init(); |
به تراشه میگوییم که میخواهیم بردار اینتراپت برای USART2 را فعال کنیم //
1 | NVIC_EnableIRQ(USART2_IRQn); |
// ارسال پیام به مدتزمان طولانی.
1 2 3 4 5 6 | for (;;) { myPuts(hello); HAL_Delay(500); } { |
1 | 10-1: 10.serial.int/main.c |
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.