کد زیر شباهت زیادی به برنامه ورودی/خروجی سریال دارد، چون تنظیمات مربوط به ورودی/خروجی همان است البته با جزئیات اضافی بیشتر. اما در این مورد، موارد جدیدی را هم اضافه کردهایم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int main(void) { HAL_Init(); // سختافزار را مقداردهی اولیه میکند led2_Init(); uart2_Init(); به تراشه میگوییم که میخواهیم بردار وقفه برای USART2 فعال شود// NVIC_EnableIRQ(USART2_IRQn); } |
تابع NVIC_EnableIRQ سختافزار کنترلکننده وقفه تودرتو (NVIC) را پیکربندی میکند. این بخش از سختافزار وظیفه دارد هنگام وقوع یک وقفه (Interrupt) تصمیم بگیرد پردازنده چه کاری انجام دهد. در اینجا، این تابع وقفهی USART2 را فعال میکند.
وقتی پردازنده ریست میشود، همه وقفهها بهصورت پیشفرض غیرفعال هستند، بنابراین باید به طور صریح اعلام کنیم که میخواهیم وقفهی USART2 فعال و پردازنده در زمان وقوع آن، واکنش نشان دهد.
حالا بیایید به تابع myPuts نگاه کنیم که یک رشته (بهجای یک کاراکتر واحد، مانند myPutchar ) را به دستگاه سریال ارسال میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void myPuts(const char* str) { // اگر کسی در حال ارسال رشته است، منتظر بمانید. 1 while (usart2String != NULL) continue; // به روال وقفه اطلاع دهید که از کدام رشته استفاده کند. 2 usart2String = str; 3 uartHandle.Instance->TDR = *usart2String; کاراکتر را به UART ارسال کنید// ++usart2String; // به کاراکتر بعدی اشاره کنید. // وقفه را فعال کنید. 4 uartHandle.Instance->CR1 |= USART_CR1_TXEIE; } |
این که کدام وقفهها در میکروکنترلر فعال هستند را با نمادهایی (symbols) نشان داده و کنترل میشوند. برای مثال بیت USART_CR1_TXNEIE به UART میگوید که وقتی بافر داده فرستنده خالی شد، وقفه ایجاد کند. در اینجا برخی از نمادهایی که باید به آنها توجه کنید، آورده شده است:
وقتی میخواهیم اولین کاراکتر را بفرستیم، رجیستر TDR پر میشود. با انتقال داده از TDR به TSR، رجیستر TDR خالی شده و وقفه مربوطه فعال میشود. از این مرحله به بعد، روال وقفه وظیفه ارسال ادامه رشته را بر عهده خواهد داشت.
روال وقفه واقعی به شرح زیر است:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 1 void USART2_IRQHandler(void) { 2 if ((uartHandle.Instance->ISR & USART_ISR_TXE) != 0) { // این نباید رخ دهد، اما اگر اتفاق بیفتد نباید کرش کنیم. 3 if (usart2String == NULL) { // خاموشکردن وقفه. uartHandle.Instance->CR1 &= ~(USART_CR1_TXEIE); return; } 4 if (*usart2String == '\0') { usart2String = NULL; //کار ما با رشته تمام شد // خاموشکردن وقفه uartHandle.Instance->CR1 &= ~(USART_CR1_TXEIE); return; } ارسال کاراکتر به // UART 5 uartHandle.Instance->TDR = *usart2String; // اشاره به کاراکتر بعدی. 6 ++usart2String; return; } تنها وقفهای که فعال کردهایم TXE بوده است پس نباید به اینجا برسیم//. // هنگامی که سایر وقفهها را فعال کنیم، باید کدی برای کدهای مربوط به آنها را اینجا واد کنیم (مانند کدی که در بالا داشتیم). } |
اعلان تابع (function declaration) از یک «نام خاص» یا همان magic name استفاده میکند که به کامپایلر یا سیستم میفهماند این تابع همان روال وقفه (Interrupt Service Routine) شماره ۱ در کد بالا است.
وقتی این تابع اجرا شود، میفهمیم که یک وقفه از USART2 دریافت شده است.
بااینحال، این فقط به ما میگوید که وقفهای از سمت USART2 آمده، ولی نوع دقیق آن را مشخص نمیکند، زیرا USART میتواند چندین نوع وقفه مختلف ایجاد کند، مثل:
همه این وقفهها منجر به فراخوانی USART2_IRQHandler میشوند.
به زبان سادهتر: تابع وقفه فقط شروع ماجراست؛ ما باید آن را بررسی کنیم تا مشخص شود چه رویدادی در USART2 اتفاق افتاده تا واکنش مناسب آن را انجام دهیم.
ابتدا باید بررسی شود که آیا وقفه «بافر ارسال خالی» (Transmit Buffer Empty Interrupt) رخ داده است یا خیر.
ابتدا باید مشخص شود که آیا وقفه Transmit Buffer Empty (TXE) رخ داده است یا خیر.
این مرحله نقطه شروعِ روال وقفه است.
usart2String
به صورت منطقی، تابع وقفه نباید زمانی اجرا شود که متغیر usart2String
مقدار NULL
داشته باشد.
اما در عمل، تکیه بر فرضیات میتواند خطرناک باشد، بنابراین:
usart2String
برابر NULL
باشد:در صورت انجام ندادن این بررسی، ممکن است یک Dereference روی اشارهگر NULL
انجام شود.
این یعنی تلاش برای دسترسی به دادهای که آدرس معتبر ندارد.
اگر مقدار usart2String
معتبر باشد، مرحله بعد بررسی پایان رشته است:
usart2String
به NULL
تغییر داده میشود.بعد از قرار دادن کاراکتر در TDR:
زمانی که کاراکتر فعلی ارسال شد و TDR دوباره خالی شد، وقفه جدید ایجاد میشود.
این چرخه تا ارسال آخرین کاراکتر و غیرفعال شدن وقفه ادامه دارد.
سختافزار این پردازنده وظیفه ذخیره و بازیابی تمام وضعیتهای لازم (Register State) را برای اجرای ایمن روال وقفه بر عهده دارد.
بااینحال، همه پردازندهها چنین قابلیتی ندارند.
برای مثال، در خانواده پردازندههای PIC لازم است پیش از هر روال وقفه، از یک کلمه کلیدی اختصاصی با نام interrupt (بهعنوان یک افزونه خاص PIC) استفاده شود:
1 | interrupt PIC_GPIO_Interrupt(void) |
مهندسان سختافزار در STMicroelectronics با لطف فراوان این فرایند را با یک نمودار توضیح دادهاند (شکل زیر را ببینید).
نگاشت وقفه USART
این شکل نشان میدهد که اگر بیت فعالسازی وقفه ارسال کاراکتر (TCIE) روشن باشد (1) و یک کاراکتر ارسال شده باشد (2) (TC)، خروجی گیت AND درست (true) است (3). این نتیجه با خروجی سه وقفه دیگر ترکیب میشود و اگر هر کدام از آنها درست باشد، نتیجه نهایی گیت OR (4) درست (true) است. سپس این نتیجه با خروجی یک گیت OR دیگر (5) که برای تمام سیگنالهای دیگر است ترکیب میشود و خروجی آخرین گیت OR سیگنال وقفه USART است. توجه داشته باشید که این شکل برای سادهسازی فرایند است. اگر میخواهید معنی مخفف کلمههای ورودی را بدانید، باید دفترچه راهنمای 800 صفحهای این پردازنده را مطالعه کنید.
وقفهها ابزارهای بسیار قدرتمندی برای کنترل سختافزار و همچنین رسیدگی به رویدادها بهصورت لحظهای هستند. با این حال، استفاده از وقفهها میتواند منجر به بروز مشکلات منحصر به فرد و دشواری شود.
اولین مشکل، وقفهها میتوانند در هر زمانی جریان عادی برنامه را قطع کنند. برای مثال، کد زیر را در نظر بگیرید:
1 2 3 4 5 6 7 | i = 5; if (i == 5) { // ... } |
بعد از اجرای این کد، مقدار i چه عددی است؟ پاسخ واضح 5 است، مگر اینکه درست قبل از اجرای این کد، یک روال وقفه اجرا شده و مقدار i را تغییر داده باشد:
1 2 3 4 5 6 7 8 9 | i = 5; وقفه رخ میدهد، مقدار i را به 6 تغییر میدهد ←----- if (i == 5) { // دیگر درست نیست // ... } |
روالهای وقفه (Interrupt Routines) بهصورت غیرهمزمان (Asynchronous) اجرا میشوند، به این معنا که ممکن است در هر لحظهای اجرا شوند.
به همین دلیل، خطاهایی که در اثر پیادهسازی نامناسب این روالها ایجاد میشوند، میتوانند بسیار دشوار برای بازتولید (Reproduce) باشند.
در یک نمونه عملی، مشاهده شد که یک خطا تنها بهصورت تصادفی و پس از حدود دو هفته آزمونگیری ظاهر شد؛ چراکه وقوع وقفه باید دقیقاً همزمان با اجرای یکی از دو دستور خاص اتفاق میافتاد — دو دستور در میان دهها هزار دستور موجود در کد.
کشف این مشکل نیازمند حجم زیادی از آزمون و بررسی بود.
به دلیل مشکلات ذاتی روالهای وقفه، بهتر است با آنها محترمانه برخورد کنیم. مهمترین قانون طراحی هنگام کار با روالهای وقفه کوچک و ساده نگهداشتن آنها است. هر چه روال کار کمتری انجام دهد، احتمال خطای کمتری وجود دارد. بهتر است کارهای پیچیده را به کد سطح بالاتر واگذار کنید، جایی که دیباگ بهراحتی انجام میپذیرد و بازتولید خطا مشکل نیست.
همچنین روالهای وقفه باید بهسرعت اجرا شوند، زیرا در حین اجرای یک روال وقفه، سایر وقفهها تا پایانیافتن آن به تعویق میافتند. اگر در یک روال وقفه که در حال خواندن یک کاراکتر از UART1 است، زمان زیادی صرف کنید، دستگاه دیگری مانند UART2 ممکن است دادههایی را از دست بدهد؛ زیرا وقفه آن نمیتواند بهموقع اجرا شود.
سیستمی که پیشتر شرح داده شد محدودیتهایی دارد. این سیستم تنها قادر به ارسال یک پیام در هر لحظه است.
بهعنوانمثال، اگر بخواهیم چندین پیام کوتاه را پشتسرهم ارسال کنیم، هر پیام باید منتظر اتمام ارسال پیام قبلی باشد:
1 2 3 4 5 6 7 8 9 10 11 | myPuts("There are "); if (messageCount == 0) myPuts(" no "); // مسدودشدن تا پایان پیام قبلی else myPuts(" some ");// مسدودشدن تا پایان پیام قبلی myPuts("messages waiting\r\n"); // مسدودشدن تا پایان پیام قبلی |
در این حالت، هر فراخوانی تابع myPuts تا زمانی که ارسال پیام قبلی کامل نشده باشد، متوقف میماند (Blocking).
یکی از راهحلهای این مشکل، ایجاد یک بافر برای ذخیره دادههای کاراکتری است تا روال وقفه بتواند آن را بهمرور پردازش و ارسال کند.
این روش باعث افزایش پیچیدگی در طراحی برنامه میشود، اما سرعت ارسال داده توسط لایه اصلی برنامه (Top-Level) را به شکل قابلتوجهی بهبود میبخشد.
استفاده از ورودی/خروجی سریال (Serial I/O) همواره نیازمند درنظرگرفتن تعادل میان سرعت و سادگی پیادهسازی است.
این رابطه میان افزایش سرعت و افزایش پیچیدگی در اکثر برنامهها و سامانههای نرمافزاری صادق است.
برای حل این مشکل، بیایید دوباره به بافر برگردیم. ما از یک بافر حلقهای (circular buffer) با ساختار پایه مشابه زیر استفاده خواهیم کرد:
به این ساختار بافر حلقوی (Circular Buffer) گفته میشود؛ زیرا اندیسها (Indices) در هنگام رسیدن به انتهای بافر، مجدداً به ابتدای آن بازمیگردند.
به بیان دیگر، پس از قراردادن یک کاراکتر در آخرین خانه آرایه داده، مقدار putIndex از ۷ (معادل BUFFER_SIZE – 1) به 0 بازمیگردد.
بهصورت گرافیکی، این شکل شبیه به شکل زیر است.
بافر حلقوی در حال اجرا
پیادهسازی ما بهصورت عمدی شامل یک خطای جدی خواهد بود تا بتوانیم روشها و فرایندهای شناسایی چنین خطاهایی را در یک برنامه کوچک و کنترلشده بررسی کنیم.
در ادامه، علائم این خطا، تکنیکهای عیبیابی مورداستفاده برای شناسایی محل آن، و راهکار اصلاحی را در حین مرور برنامه نشان خواهم داد.
برنامه سطح بالا (فرستنده) داده را درون بافر قرار میدهد و روال وقفه سطح پایین (گیرنده) داده را از بافر خارج میکند.
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.