برای کار با حافظه بافر دایرهای، باید به ارسال تکتک کاراکترها با استفاده از نسخه دیگری از تابع myPutchar بازگردیم. در ادامه، کدی که وظیفهی ارسال را بر عهده دارد، بررسی میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void myPutchar(const char ch) { // Wait until there is room. 1 while (buffer.nCharacters == BUFFER_SIZE) continue; 2 buffer.data[buffer.putIndex] = ch; 3 ++buffer.putIndex; if (buffer.putIndex == BUFFER_SIZE) buffer.putIndex = 0; // We've added another character. 4 ++buffer.nCharacters; // Now we're done. // Enable the interrupt (or reenable it). 5 uartHandle.Instance->CR1 |= USART_CR1_TXEIE; } |
روال وقفه دادهها را از بافر میخواند و به UART ارسال میکند. اگر کاراکتری در بافر وجود داشته باشد، روال آن را به UART ارسال و از بافر حدف میکند. اگر چیزی در بافر نباشد، وقفه به وسیله روال غیرفعال میشود.
در ادامه کد این روش آمده است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void USART2_IRQHandler(void) { 1 if ((uartHandle.Instance->ISR & USART_ISR_TXE) != 0) { if (buffer.nCharacters == 0) { // خاموش کردن وقفه. uartHandle.Instance->CR1 &= ~(USART_CR1_TXEIE); return; } ارسال به UART // 2 uartHandle.Instance->TDR = buffer.data[buffer.getIndex]; 3 ++buffer.getIndex; if (buffer.getIndex == BUFFER_SIZE) buffer.getIndex = 0; 4 --buffer.nCharacters; 5 if (buffer.nCharacters == 0) uartHandle.Instance->CR1 &= ~(USART_CR1_TXEIE); return; } از آنجایی که تنها وقفه فعال شده TXE بود،// // نباید به اینجا برسیم. وقتی که سایر وقفهها را فعال کنیم، // باید کد پردازش آنها را اینجا قرار دهیم. { |
کد زیر برنامه کامل را نشان می دهد.
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /** * @brief Write Hello World to the serial I/O * using a circular buffer. * * @note Contains a race condition to demonstrate * how not do do this program */ #include <stdbool.h> #include "stm32f0xx_nucleo.h" #include "stm32f0xx.h" const char hello[] = "Hello World!\r\n"; // The message to send int current; // The character in the message we are sending UART_HandleTypeDef uartHandle; // UART initialization #define BUFFER_SIZE 8 // The data buffer size struct circularBuffer { uint32_t putIndex; // Where we will put the next character uint32_t getIndex; // Where to get the next character uint32_t nCharacters; // Number of characters in the buffer char data[BUFFER_SIZE]; // The data in the buffer }; // A simple, classic circular buffer for USART2 volatile struct circularBuffer buffer = {0,0,0, {'\0'}}; ... Error_Handler from Listing 9-3 ... /** * Handle the USART2 interrupt. * * Magically called by the chip's interrupt system. * Name is fixed because of the startup code that * populates the interrupt vector. */ void USART2_IRQHandler(void) { if ((uartHandle.Instance->ISR & USART_ISR_TXE) != 0) { if (buffer.nCharacters == 0) { // Turn off interrupt. uartHandle.Instance->CR1 &= ~ (USART_CR1_TXEIE); return; } // Send to UART. uartHandle.Instance->TDR = buffer.data[buffer.getIndex]; ++buffer.getIndex; if (buffer.getIndex == BUFFER_SIZE) buffer.getIndex = 0; --buffer.nCharacters; if (buffer.nCharacters == 0) uartHandle.Instance->CR1 &= ~ (USART_CR1_TXEIE); return; } // Since the only interrupt we enabled was TXE, we should never // get here. When we do enable other interrupts, we need to put // code to handle them here. } /** * Put a character in the serial buffer. * * @param ch The character to send */ void myPutchar(const char ch) { // Wait until there is room. while (buffer.nCharacters == BUFFER_SIZE) continue; buffer.data[buffer.putIndex] = ch; ++buffer.putIndex; if (buffer.putIndex == BUFFER_SIZE) buffer.putIndex = 0; // We've added another character. ++buffer.nCharacters; // Now we're done. // Enable the interrupt (or reenable it). uartHandle.Instance->CR1 |= USART_CR1_TXEIE; } /** * Our version of puts * * Outputs the exact string given to the output * * @param str String to send * * @note Assumes that str is not null */ void myPuts(const char* str) { for (/* str set */; *str != '\0'; ++str) myPutchar(*str); } ... led2_init and uart2_init same as Listing 9-3 ... int main(void) { HAL_Init(); // Initialize hardware. led2_Init(); uart2_Init(); // Tell the chip that we want the interrupt vector // for USART2 to be enabled. NVIC_EnableIRQ(USART2_IRQn); // Keep sending the message for a long time. for (;;) { myPuts(hello); HAL_Delay(500); } } ... HAL_UART_MspInit and HAL_UART_MspDeInit same as Listing 9-3 ... |
10.serial.buffer.bad/src/main.c
هنگام اجرای برنامه، انتظار داریم خروجی زیر را مشاهده کنیم:
1 2 3 4 5 | Hello World! Hello World! Hello World! |
اما به جای آن، خروجی زیر را دریافت میکنیم:
1 2 3 4 5 6 7 | Hello World! ello World! Hello Wold! Hello World! |
هیچ الگوی مشخصی برای بروز این مشکل وجود ندارد، جز اینکه هرچه دادههای بیشتری به بافر بفرستیم، احتمال وقوع مشکل بیشتر میشود.
در این برنامهی کوچک، بازتولید این مشکل در شرایط واقعی سخت خواهد بود، چون نیاز به زمانبندی بسیار دقیقی دارد. احتمالاً بعد از پیادهسازی و تست این کد، وقتی ماژول را در یک برنامهی دیگر با شرایط خاصتر استفاده کنیم، احتمال بروزِ این مشکل زمانی بیشتر میشود.
باگهای زمانی (Timing bugs) بدناماند که بهسختی میتوان آنها را بهصورت دستی و در لحظه ایجاد کرد، اما در این کد یکی از آنها وجود دارد.
میدانیم که هر چه سیستم شلوغتر باشد، احتمال وقوع خطا بیشتر میشود. به علاوه، سرنخ دیگری هم داریم: نتوانستهایم خطا را در حین وقوع مشاهده کنیم، اما بعد از بررسی با دیباگر، متوجه شدیم که:
1 2 3 | nCharacters == 0 getIndex != putIndex |
در یک برنامهای که درست کار میکند، هرگز نباید هر دو شرط همزمان برقرار باشند.
دو راه برای رویارویی با این مشکل وجود دارد: نخست اینکه کد را ابزارگذاری (instrument) کنیم تا بفهمیم چه اتفاقی میافتد. دوم اینکه یک تحلیل بسیار دقیق روی تمام دادههایی که بین لایه بالایی و لایه پایینی به اشتراک گذاشته میشوند و کدی که این دادهها را تغییر میدهد انجام دهیم.
بیایید هر دو روش را انجام بدهیم.
ابزارگذاری (Instrumenting) کد یعنی اضافه کردن دستورهای دیباگ موقتی که کمک میکنند مشکل را پیدا کنیم. در بیشتر کدها، این یعنی نوشتن دستورهای printf برای چاپ دادههای میانی که بتوانیم حین اجرای برنامه آنها را بررسی کنیم. گاهی هم دادهها را داخل یک فایل لاگ ذخیره میکنیم تا بعد از وقوع مشکل، آن را تحلیل کنیم. هیچکدام از این دو گزینه برای برنامهی امبدد ما قابل استفاده نیستند. نمیتوانیم از printf استفاده کنیم چون خروجی آن به کنسول سریال میرود، و این همان بخشی است که مشکل دارد. نمیتوانیم هم لاگفایل بنویسیم چون فایلسیستمی برای ذخیره اطلاعات لاگ نداریم.
ما به یک بافر log نیاز داریم که ۱۰۰ رویداد (event) آخر را ذخیره کند. هنگامی که با خطایی برخورد میکنیم، میتوانیم به عقب برگردیم و رویدادها را بررسی کنیم تا ببینیم چه چیزی منجر به خطا شده است. این گزارشهای log دادههای مرتبط (getIndex، putIndex، nCharacters) و شمارهی خط فراخوانی به کد ثبت رویداد log را ذخیره میکند.
هنگامی که خطا رخ میدهد و فرصتی برای متوقف کردن برنامه در دیباگر پیدا میکنیم، میتوانیم log را بررسی کنیم. اگر خوششانس باشیم، باید بتوانیم چند ورودی log را پیدا کنیم که در خط X، با اطلاعات بافر سازگار بوده و در خط Y، خراب شدهاند؛ این نشان میدهد که خطا بین خطوط X و Y رخ داده است.
کد ۱۰-۳ کد ثبت رویداد را نشان میدهد. این کد را بعد از سایر تعریفها و اعلانهای متغیر اضافه کنید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #define N_EVENTS 100 events. uint32_t nextEvent = 0; next event // Store 100 // Where to put the struct logEvent debugEvents[N_EVENTS]; // The log data void debugEvent(const uint32_t line) { debugEvents[nextEvent].line = line; debugEvents[nextEvent].putIndex = buffer.putIndex; debugEvents[nextEvent].getIndex = buffer.getIndex; debugEvents[nextEvent].nCharacters = buffer.nCharacters; ++nextEvent; if (nextEvent == N_NEVETS) nextEvent = 0; } |
کد ثبتکننده رویداد
این ثبتکننده رویداد حاوی همان نوع خطایی است که در حال تلاش برای یافتن آن هستیم، اما فعلا فرض میکنیم که برای مکانیابی خطا کار میکند.
حالا برای اینکه ببینیم آیا میتوانیم خطا را پیدا کنیم، نیاز به چند فراخوانی به تابع debugEvent داریم. از آنجایی که nCharacters ما را کلافه کرده است، قبل و بعد از هر عملیاتی که روی nCharacters انجام میدهیم، یک فراخوانی به debugEvent قرار میدهیم:
1 2 3 4 5 6 7 8 9 10 | void USART2_IRQHandler(void) --snip-- debugEvent(__LINE__); --buffer.nCharacters; debugEvent(__LINE__); void myPutchar(const char ch) --snip-- debugEvent(__LINE__); ++buffer.nCharacters; debugEvent(__LINE__); |
همچنین یک بررسی سازگاری(consistency check) در ابتدای تابع myPutchar قرار میدهیم تا مطمئن شویم بافر سالم است. به طور خاص، اگر شرایطی را ببینیم که بافر ناسازگار باشد
1 2 3 4 5 | void myPutchar(const char ch) { if ((buffer.nCharacters == 0) && (buffer.getIndex != buffer.putIndex)) Error_Handler(); |
بیایید برنامه را زیر دیباگر اجرا کنیم و یک نقطه توقف (breakpoint) روی Error_Handler بگذاریم تا ببینیم میتوانیم خطا را بگیریم یا نه. در نهایت، به این نقطه توقف رسیدیم و با استفاده از دیباگر، مقدار debugEvents را بررسی کردیم. سپس با برگشتن به عقب در ردگیریها (trace) توسط دیباگر، به مورد زیر رسیدیم:
چرا nCharacters بین دو رویداد آخر با ۲ پرش کرده است؟ خطوط مرتبط به شرح زیر هستند:
1 2 3 4 5 6 7 | void USART2_IRQHandler(void) --snip-- debugEvent(__LINE__); --buffer.nCharacters; debugEvent(__LINE__); |
و:
1 2 3 4 5 6 7 | void myPutchar(const char ch) --snip-- debugEvent(__LINE__); ++buffer.nCharacters; debugEvent(__LINE__); |
مشکلی در خط ۹۰ یا ۱۲۰ وجود دارد، که این موضوع همچنین نکته مهمی را به ما میگوید. بین خط ۱۱۹ و قبل از خط ۱۲۱، یک وقفه (interrupt) رخ داده است. اکنون خطا را به چند خط کد و یک وقفه محدود کردهایم. بیایید روش خود را تغییر دهیم و از تحلیل کد برای رسیدن به همین نتیجه استفاده کنیم. ما هر دو روش را بررسی میکنیم زیرا گاهی اوقات یک روش کار میکند و دیگری کار نمیکند.
راه دیگر برای فهمیدن مشکل، تحلیل اتفاقات در حال رخ دادن و تلاش برای شناسایی نقاط بالقوهی مشکل است. تحلیل با شناسایی دادههای مشترک بین لایههای بالا و پایین شروع میشود؛ به عبارت دیگر، بافر:
1 2 3 4 5 6 | struct circularBuffer { uint32_t putIndex; // محل قرارگیری کاراکتر بعدی uint32_t getIndex; // محل دریافت کاراکتر بعدی uint32_t nCharacters; // تعداد کاراکترها در بافر char data[BUFFER_SIZE]; // دادههای بافر }; |
از آنجایی که putIndex و getIndex هر کدام فقط توسط یک لایه (به ترتیب لایه بالا و لایه پایین) استفاده میشوند، نباید مشکلی ایجاد کنند. آرایهی data توسط هر دو لایه به اشتراک گذاشته میشود، اما توسط لایهی بالا نوشته شده و توسط لایهی پایین خوانده میشود، بنابراین هر لایه در مورد این آرایه وظیفهی متمایزی دارد. علاوه بر این، putIndex قسمتی از آرایه را که لایهی بالا استفاده میکند، کنترل میکند و getIndex قسمتی را که لایهی پایین استفاده میکند، کنترل میکند. آنها به عناصر متفاوتی از آرایه اشاره میکنند و هیچ چیزی که وارد یا خارج از data میشود، نمیتواند بر روی ایندکسها (indices) یا شمارندهی کاراکتر تأثیر بگذارد. بنابراین، آرایهی data مشکل نیست.
تنها چیزی که باقی میماند nCharacters است که لایهی بالا آن را افزایش میدهد و لایهی پایین آن را کاهش میدهد، بنابراین دو خط بالقوهی مشکل وجود دارد. یکی در روال وقفه است:
1 | --buffer.nCharacters; |
و دیگری در تابع myPutchar است:
1 | ++buffer.nCharacters; |
این همان دو خطی است که کد ابزارگذاریشدهی ما نشان داد ممکن است مشکلساز باشد.
بیایید دقیقاً ببینیم چه اتفاقی میافتد زمانی که خط زیر اجرا میشود:
1 | ++buffer.nCharacters; |
در اینجا کد اسمبلی (با توضیحات اضافهشده) برای این خط آمده است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 120:../src/main.c *** ++buffer.nCharacters; 404 loc 2 118 0 405 005c 094B ldr r3, .L22 ; Load r3 with the address in .L22, ; which happens to be "buffer". 406 005e 9B68 ldr r3, [r3, #8] ; Get the value of ; buffer.nCharacters. 407 0060 5A1C 1 adds r2, r3, #1 ; Add 1 to r3 and store the result in r2. 408 0062 084B ldr r3, .L22 ; Get the address again. 409 0064 9A60 str r2, [r3, #8] ; Store buffer.nCharacters. |
این کد، مقدار nCharacters را در رجیستر r3 بارگذاری میکند، آن را یک واحد افزایش میدهد و سپس مقدار جدید را دوباره در nCharacters ذخیره میکند. وقفهها (interrupts) میتوانند در هر زمانی رخ دهند، برای مثال درست بعد از اینکه مقدار در رجیستر r3 بارگذاری شد (فرض کنید nCharacters برابر با ۳ باشد)، که باعث رخ دادن موارد زیر میشود:
رجیستر r3 مقدار صحیح nCharacters را ندارد زیرا یک وقفه درست در زمان مناسب رخ داده و متغیر در همان زمان اصلاح شده است. ما در محافظت از سازگاری دادههای مشترک شکست خوردیم. لایهی بالا در حال تغییر دادن nCharacters بود در حالی که لایهی پایین همزمان آن را تغییر میداد.
اگر وقفه بین برخی دستورالعملهای دیگر رخ دهد، مشکل پیش نمیآید. این مشکل تصادفی است و به ندرت اتفاق میافتد، که باعث میشود حل کردن آن به یکی از مشکلات دشوارتر تبدیل شود.
راهحل این است که از تغییر دادن nCharacters توسط روال وقفه در حین تغییر آن توسط ما جلوگیری شود. برای انجام این کار، وقفهها را قبل از افزایش و بعد از آن خاموش میکنیم:
1 2 3 4 5 | 119 __disable_irq(); 120 ++buffer.nCharacters; 121 __enable_irq(); |
مدت زمانی که وقفهها خاموش هستند را کوتاه نگه دارید. اگر وقفهها برای مدت طولانی خاموش شوند، ممکن است یک وقفه را از دست بدهید و دادهها را رها کنید.
در روال وقفه، ما مقدار nCharacters را کاهش میدهیم، پس آیا نیاز نداریم آن را با __disable_irq و __enable_irq محافظت کنیم؟ خیر، زیرا هنگامی که یک وقفه رخ میدهد، سیستم به طور خودکار مراحل زیر را انجام میدهد:
وقتی روال وقفه تمام میشود، سیستم این مراحل را انجام میدهد:
کارهای حسابداری زیادی در ابتدای و انتهای یک روال وقفه باید انجام شود. خوشبختانه، طراحان خانوادهی پردازندهی ARM تصمیم گرفتند همهی این کارها را در سختافزار انجام دهند. ممکن است سایر پردازندهها اینطور نباشند.
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.