بررسی کلی وقفه
مدیریت سخت افزار در واقع مدیریت رویدادهای ناهمزمان است که اغلب از واحدهای سختافزاری جانبی میآیند. به عنوان مثال، یک تایمر که به مقدار زمان تنظیم شده خود میرسد، یا یک UART که در مورد دریافت دادهها پیغام میدهد. دیگر رویدادها توسط المانهای خارج از برد، سرچشمه میگیرند. برای مثال، کاربر یک سوئیچ را فشار میدهد.
همه میکروکنترلرها قابلیتی به نام وقفه را ارائه میدهند. وقفه یک رویداد ناهمزمان است که باعث توقف اجرای کد فعلی بر اساس اولویت میشود (هر چه وقفه مهمتر باشد، اولویت آن بالاتر است؛ این امر باعث میشود که وقفه با اولویت پایینتر به حالت تعلیق درآید). کدی که وقفه را سرویس میدهد، روال سرویس وقفه (ISR) Interrupt Service Routine نامیده میشود.
وقفهها منبع multiprogramming هستند: سخت افزار در مورد آنها آگاه است و مسئول ذخیره محتویات فعلی کد (یعنی stack frame، شمارنده برنامه فعلی و چند چیز دیگر) قبل از تغییر به ISR است. آنها توسط سیستم عامل های Real Time برای معرفی مفهوم وظایف (notion of tasks)، مورد استفاده قرار میگیرند. بدون کمک سختافزار، داشتن یک سیستم پیشگیرانه واقعی غیرممکن است، که اجازه دهد بین چندین کد اجرایی بدون از دست دادن جریان اجرای کد فعلی، سوئیچ کنید.
وقفهها میتوانند هم توسط سخت افزار و هم خود نرمافزار ایجاد شوند. معماری ARM بین این دو نوع تمایز قائل میشود: وقفههایی که توسط سختافزار ایجاد میشوند، استثناها exceptions ایجاد شده توسط نرمافزار (به عنوان مثال، دسترسی به مکانی از حافظه نامعتبر). در اصطلاح ARM، وقفه نوعی استثنا exceptions است.
پردازندههای Cortex-M واحدی را ارائه میدهند که به مدیریت استثنائات exceptions اختصاص دارد. این واحد کنترلر وقفه برداری تودرتو (NVIC) Nested Vectored Interrupt Controller نامیده میشود و این بخش در مورد برنامه نویسی مدیریت وقفهها میباشد.
واحد کنترل کننده NVIC
NVIC یک واحد سختافزاری اختصاصی در داخل میکروکنترلرهای مبتنی بر Cortex-M است که مسئولیت رسیدگی به استثنائات exceptions را بر عهده دارد. شکل زیر رابطه بین واحد NVIC، هسته پردازنده و واحدهای جانبی را نشان میدهد. در اینجا ما باید دو نوع از واحدهای جانبی را تشخیص دهیم: آنهایی که خارج از هسته Cortex-M هستند، اما واحدهای جانبی داخلی برای میکروکنترلر STM32 (مانند تایمرها، UARTS و غیره) محسوب میشوند و آنهایی که به طور کلی در خارج از میکروکنترلر قرار دارند. منبع وقفههایی که از دومین نوع واحد جانبی مذکور میآیند، ورودی/خروجی I/O میکروکنترلر است که میتواند هم بهعنوان ورودی/خروجی I/O عمومی (مثلاً یک تکت سوئیچ متصل به یک پین که بهعنوان ورودی پیکربندی شده است) یا برای راهاندازی یک المان جانبی پیشرفته خارجی پیکربندی شود. (به عنوان مثال I/Oهایی که برای تبادل داده با اترنت فیتر از طریق رابط RMII پیکربندی شده اند). همانطور که در ادامه خواهیم دید، یک کنترلر قابل برنامه ریزی اختصاصی، به نام کنترل کننده وقفه/رویداد خارجی (EXTI) External Interrupt/Event Controller، وظیفه اتصال بین سیگنال های ورودی/خروجی خارجی و کنترل کننده NVIC را بر عهده دارد.
همانطور که قبلاً گفته شد، ARM بین استثناهای exceptions سیستم که از داخل هسته CPU منشا میگیرند و استثناهای exceptions سخت افزاری ناشی از تجهیزات جانبی خارجی که درخواست وقفه (IRQ) Interrupt Requests نیز نامیده میشود، تمایز قائل میشود.
برنامهنویسان استثناها exceptions را با استفاده از ISRهای خاص که در سطح بالاتر کدگذاری شدهاند (اغلب با استفاده از زبان C) مدیریت میکنند. پردازنده به لطف یک جدول غیرمستقیم حاوی آدرسهای موجود در حافظه برای هر روال سرویس وقفه، میداند این روالها را کجا قرار دهد. این جدول معمولاً جدول برداری نامیده میشود و برای هر میکروکنترلر STM32 تعریف شده است.
جدول بردار در STM32
همه پردازندههای Cortex-M مجموعهای ثابت از استثناها exceptions را تعریف میکنند (پانزده مورد برای هستههای Cortex-M3/4/7 و سیزده مورد برای هستههای Cortex-M0/0+) که برای همه خانوادههای Cortex-M و برای همه سریهای STM32 مشترک هستند.
Reset: این استثنا exception درست پس از ریست CPU مطرح میشود. کنترل کننده آن (handler) نقطه ورود واقعی سیستم عامل در حال اجرا است. در یک برنامه STM32 همه چیز از این استثنا exception شروع میشود. این handler شامل برخی از توابع اسمبلی است که برای مقداردهی اولیه محیط اجرایی طراحی شدهاند، مانند پشته اصلی، ناحیه .bss، و غیره.
NMI: این یک استثنا (exception) خاص است که بعد از Reset بالاترین اولویت را دارد. مانند استثنای Reset، نمیتوان آن را پنهان کرد (غیرفعال)، و میتواند به فعالیتهای حیاتی و غیر قابل تعویق مرتبط شود. در میکروکنترلرهای STM32 به سیستم امنیتی کلاک (CSS) Clock Security System متصل است. CSS یک واحد جانبی خود تشخیص بوده که خرابی HSE را تشخیص میدهد. اگر این اتفاق بیفتد، HSE به طور خودکار غیرفعال میشود (به این معنی که HSI داخلی به طور خودکار فعال میشود) و یک وقفه NMI ایجاد میشود تا به نرم افزار اطلاع دهد که مشکلی در HSE وجود دارد.
Hard Fault: استثنای exception خرابی (خطا) عمومی است، از این رو به وقفههای نرمافزار مربوط میشود. هنگامی که سایر استثناهای (exception) خطا غیرفعال باشند، به عنوان یک جمع کننده برای انواع استثناها exceptions عمل میکند (به عنوان مثال، دسترسی به یک مکان نامعتبر از حافظه، باعث اعلان Hard Fault میشود اگر Bus Fault فعال نباشد).
خطای مدیریت حافظه: زمانی رخ میدهد که در زمان اجرای کد تلاش میشود به یک مکان نامعتبر از حافظه دسترسی پیدا کرده یا قانون واحد حفاظت از حافظه (MPU) Memory Protection Unit نقض شود.
Bus Fault : زمانی رخ میدهد که رابط AHB یک پاسخ خطا از یک bus slave دریافت کند (اگر واکشی دستورالعمل باشد prefetch abort، یا اگر دسترسی به داده باشد، data abort، نیز نامیده میشود ). همچنین میتواند ناشی از دسترسیهای غیرقانونی دیگر باشد (مانند دسترسی به یک مکان حافظه SRAM که وجود ندارد).
Usage Fault: زمانی رخ میدهد که یک خطای برنامه مانند دستور غیرقانونی، مشکل alignment، یا تلاش برای دسترسی به یک پردازنده مشترک (که وجود نداشته باشد) انجام پذیرد.
SVCCall: این مورد یک وضعیت خطا نیست و زمانی که دستورالعمل فراخوانی سرپرست (SVC) Supervisor Call فراخوانی میشود، مطرح میشود. این مورد توسط Real Time Operating Systems برای اجرای دستورالعملها در حالتی ویژه استفاده میشود (یک task که نیاز به اجرای عملیات ویژه دارد دستور SVC را اجرا میکند و سیستم عامل عملیات درخواستی را انجام میدهد – این همان رفتار فراخوانی سیستم در سایر سیستم عاملها است).
Debug Monitor : این استثنا زمانی که هسته پردازنده در حالت Debug-Mode بوده و یک رویداد debug نرم افزاری در حال انجام است، ایجاد میشود. همچنین در مواردی که از debug مبتنی بر نرمافزار استفاده میشود، به عنوان استثنا exception برای رویدادهای debug مانند breakpoints و watchpoints استفاده میشود.
PendSV: این یک exception دیگر مربوط به RTOS است. برخلاف استثناء SVCall که بلافاصله پس از اجرای دستور SVC اجرا میشود، PendSV میتواند به تأخیر بیفتد. این امر به RTOS اجازه میدهد تا وظایف را با اولویتهای بالاتر تکمیل کند.
SysTick: این exception نیز معمولاً مربوط به فعالیتهای RTOS است. هر RTOS به یک تایمر نیاز دارد تا به طور دورهای اجرای کد فعلی را قطع کند و به کار دیگری سوئیچ کند. همهمیکروکنترلرهای STM32 یک تایمر SysTick را در داخل هسته Cortex-M ارائه میکنند. حتی اگر از هر تایمر دیگری برای برنامهریزی و زمان بندی فعالیتهای سیستم استفاده شود، وجود یک تایمر اختصاصی، قابلیت حمل را در بین تمام خانوادههای STM32 تضمین میکند (به دلیل بهینهسازی مربوط به بدنه داخلی MCU، همه تایمرها نمیتوانند بهعنوان تجهیزات جانبی خارجی در دسترس باشند). علاوه بر این، حتی اگر از RTOS در برنامه خود استفاده نمیکنیم، مهم است که به خاطر داشته باشیم که ST CubeHAL از تایمر SysTick برای انجام فعالیتهای داخلی مرتبط با زمان استفاده میکند ( همچنین تایمر SysTick برای تولید یک وقفه در هر 1 میلی ثانیه پیکربندی شده است).
استثناهای باقی ماندهای که میتوان برای یک MCU تعریف کرد، مربوط به مدیریت IRQ است. هستههای Cortex-M0/0+ تا 32 وقفه خارجی را امکان پذیر میکند، در حالی که هسته های Cortex-M3/4/7 به سازندگان سیلیکون اجازه میدهد تا حداکثر 240 وقفه تعریف کنند.
خب از کجا میتوانیم لیست وقفه های قابل استفاده برای میکروکنترلرهای STM32 را پیدا کنیم؟ دیتاشیت آن MCU مطمئنا منبع اصلی در مورد وقفههای موجود است. با این حال، میتوانیم به سادگی به جدول برداری ارائه شده توسط ST در HAL آن مراجعه کنیم. این جدول در داخل فایل راهاندازی startup برای MCU ما تعریف شده است، فایل اسمبلیای که با S. ختم میشود. (به عنوان مثال، برای یک MCU STM32F030R8 نام فایل startup_stm32f030x8.S است). با باز کردن آن فایل، میتوانیم کل جدول برداری را برای آن MCU پیدا کنیم.
حتی اگر جدول برداری حاوی آدرس روتینهای handler باشد، هسته Cortex-M به راهی برای یافتن جدول برداری در داخل حافظه نیاز دارد. طبق قرارداد، جدول برداری از آدرس سخت افزاری 0x0000 0000 در تمام پردازندههای مبتنی بر Cortex-M شروع میشود. اگر جدول برداری در حافظه فلش داخلی قرار داشته باشد (که معمولاً اتفاق میافتد) و از آنجایی که فلش در تمام MCUهای STM32 از آدرس 0x0800 0000 شروع شده است، شروع آن از آدرس 0x0800 0000 بوده که هنگام بوت شدن CPU به آدرس 0x0000 0000 هدایت میشود.
شکل زیر نحوه سازماندهی جدول برداری در حافظه را نشان میدهد. ورودی صفر این آرایه آدرس نشانگر پشته اصلی (MSP) Main Stack Pointer در داخل SRAM است. معمولاً این آدرس مربوط به انتهای SRAM است، یعنی آدرس پایه + اندازه. با شروع از ورودی دوم این جدول، ما میتوانیم تمام استثناها و کنترلکننده وقفه ها را پیدا کنیم. یعنی جدول برداری دارای طولی برابر با 48 برای میکروکنترلرهای مبتنی بر Cortex-M0/0+ و طولی برابر با 256 برای Cortex-M3/4/7 است.
توضیح برخی موارد در مورد جدول برداری مهم است:
- نام کنترل کنندههای handler استثنا فقط یک قرارداد است و شما کاملا آزادید که اگر نام دیگری را دوست دارید تغییر دهید. آنها فقط نماد هستند (همانطور که متغیرها و توابع داخل یک برنامه هستند). با این حال، به خاطر داشته باشید که نرم افزار CubeMX برای تولید ISR با این نام ها طراحی شده است که یک قرارداد ST هستند. بنابراین، شما باید نام ISR را نیز تغییر دهید.
2. همانطور که قبلا گفته شد، جدول برداری باید در ابتدای حافظه فلش، جایی که پردازنده انتظار دارد آن را پیدا کند، قرار گیرد. این کار وطیفه ویرایشگر پیوند Link Editor است که جدول برداری را در ابتدای داده فلش هنگام تولید فایل مطلق (absolute file) قرار دهد، یک فایل باینری که در فلش آپلود میکنیم.
فعال کردن Interrupts
هنگامی که یک میکروکنترلر STM32 بوت میشود، تنها استثناهای Reset، NMI و Hard Fault به طور پیش فرض فعال هستند. بقیه استثناها و وقفههای جانبی غیرفعال هستند و در صورت درخواست باید فعال شوند. برای فعال کردن یک IRQ، CubeHAL تابع زیر را ارائه میدهد:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
که در آن IRQn_Type یک enumeration از تمام استثناها و وقفههای تعریف شده برای آن میکروکنترلر مورد نظر است. enum IRQn_Type بخشی از ST Device HAL می باشد و در داخل یک فایل هدر خاص برای میکروکنترلر STM32 در پوشه/include/cmsis تعریف شده است. این فایلها stm32fxxxx.h نام دارند. به عنوان مثال، برای یک میکروکنترلر STM32F030R8 نام فایل stm32f030x8.h است. تابع مربوطه برای غیرفعال کردن IRQ عبارت است از:
void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);
ذکر این نکته مهم است که دو تابع قبلی یک وقفه را در سطح کنترلکننده NVIC فعال یا غیرفعال میکنند. هر لاین وقفه توسط واحد جانبی متصل به آن ، تحریک می شود. برای مثال، واحد جانبی USART2 لاین وقفه USART2_IRQn در داخل کنترلر NVIC را، تحریک میکند. در واقع واحد جانبی مورد نظر باید به درستی پیکربندی شود تا در حالت وقفه کار کند. همانطور که در ادامه خواهیم دید، اکثر تجهیزات جانبی STM32 برای کار کردن در حالت وقفه طراحی شدهاند. با استفاده از توابع HAL میتوانیم وقفه را در سطح peripheral فعال کنیم. به عنوان مثال، با استفاده از HAL_USART_Transmit_IT() ما به طور ضمنی واحد جانبی USART را در حالت وقفه پیکربندی میکنیم. همچنین لازم است وقفه مربوطه را در سطح NVIC نیز با فراخوانی HAL_NVIC_EnableIRQ() فعال کنید.
وقفه های خارجی و NVIC
میکروکنترلرهای STM32 تعداد متغیری از منابع وقفه خارجی متصل به NVIC را از طریق کنترلر EXTI فراهم میکنند، که به نوبه خود قادر به مدیریت چندین لاین EXTI است. تعداد منابع و لاینهای وقفه به خانواده STM32 مورد نظر بستگی دارد. GPIO به خطوط EXTI متصل بوده و امکان فعال کردن وقفه برای هر GPIO وجود دارد، حتی اگر اکثر آنها از یک لاین مشترک وقفه استفاده کنند. به عنوان مثال، برای یک میکروکنترلر STM32F 4، حداکثر 114 GPIO به 16 خط EXTI متصل است. با این حال، تنها 7 مورد از این خطوط دارای وقفه مستقل هستند. شکل زیر لاینهای EXTI 0، 10 و 15 را در یک میکروکنترلر STM32F4 نشان میدهد. تمام پینهای Px0 به EXTI0، تمام پینهای Px10 به EXTI10 و تمام پینهای Px15 به EXTI15 متصل هستند. با این حال، لاینهای EXTI 10 و 15 یک IRQ مشترک را در داخل NVIC به اشتراک میگذارند (و از این رو توسط همان ISR سرویس دهی میشوند):
فقط یک پین PxY میتواند منبع وقفه باشد. به عنوان مثال، ما نمیتوانیم هر دو PA0 و PB0 را به عنوان پایههای وقفه ورودی تعریف کنیم.
برای خطوط EXTI یک IRQ مشترک را در داخل NVIC به اشتراک میگذارند، باید ISR مربوطه را کدنویسی کنیم تا بتوانیم تشخیص دهیم که کدام یک از خطوط وقفه را ایجاد کرده است.
مثال زیر نشان میدهد چگونه میتوان با استفاده از وقفهها با هر بار فشار دادن دکمه (متصل به PA3) توسط کاربر وضعیت LED ، که به پین PB6 متصل است را تغییر داد. ابتدا PA3 را طوری تنظیم میکنیم تا هر بار که از سطح یک منطقی به سطح صفر منطقی تغییر وضعیت داد، یک وقفه ایجاد کند. این امر با تنظیم GPIO. Mode برابر با GPIO_MODE_IT_FALLING محقق میشود. سپس، وقفه خط EXTI مرتبط با پینهای Px3 یعنی EXTI3_IRQn را فعال میکنیم.
39 int main(void) {
40 GPIO_InitTypeDef GPIO_InitStruct;
41 42 HAL_Init();
43 44 /* GPIO Ports Clock Enable */
45 __HAL_RCC_GPIOC_CLK_ENABLE();
46 __HAL_RCC_GPIOA_CLK_ENABLE();
47 48 /*Configure GPIO pin : PA3 - USER BUTTON */
49 GPIO_InitStruct.Pin = GPIO_PIN_3;
50 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
51 GPIO_InitStruct.Pull = GPIO_PULLDOWN;
52 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
53 54 /*Configure GPIO pin : PB6 - LED */
55 GPIO_InitStruct.Pin = GPIO_PIN_6;
56 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
57 GPIO_InitStruct.Pull = GPIO_NOPULL;
58 GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
59 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
60 61 HAL_NVIC_EnableIRQ(EXTI3_IRQn);
62 63 while(1);
64 }
65
66 void EXTI3_IRQHandler(void) {
67 __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_3);
68 HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_6);
69 }
در نهایت، باید تابع void EXTI3_IRQHandler() را تعریف کنیم، که روال ISR مرتبط با IRQ برای لاین EXTI3 در داخل جدول برداری است (خطوط 66:69). محتوای ISR واقعا ساده است. هر بار که ISR فعال میشود، وضعیت PB6 را تغییر میدهیم. همچنین باید بیت اتنظار مرتبط با خط EXTI را پاک کنیم. خوشبختانه، ST HAL مکانیزمی را ارائه میدهد که ما را از پرداختن به این جزئیات بی نیاز میکند.
طول عمر وقفه
کسی که با وقفهها کار میکند، باید درک درستی از چرخه زندگی آنها داشته باشد. این بخش نگاهی به چرخه عمر وقفهها از «دیدگاه HAL» میدهد.
یک وقفه میتواند:
1. یا غیر فعال (پیش فرض) و یا فعال باشد.
- با فراخوانی تابع HAL_NVIC_EnableIRQ()/HAL_NVIC_DisableIRQ() آن را فعال/غیرفعال می کنیم.
- در حالت تعلیق (در انتظار سرویسدهی به یک درخواست است) یا غیر تعلیق باشد.
- در حال ارائه خدمات یا غیر آن باشد.
حال مهم است بررسی کنیم وقتی وقفه رخ میدهد چه اتفاقی می افتد. هنگامی که یک وقفه فعال میشود، تا زمانی که پردازنده بتواند به آن خدماتی را ارائه کند، به عنوان pending در نظرگرفته میشود. اگر در حال حاضر هیچ وقفه دیگری در حال پردازش نباشد، حالت معلق pending آن به طور خودکار توسط پردازنده پاک میشود، و بلافاصله شروع به رسیدگی به آن وقفه میکند.
شکل بالا نشان میدهد که چگونه این پروسه انجام میشود. وقفه A در زمان t0 فعال میشود و از آنجایی که CPU وقفه دیگری را سرویس نمیدهد، بیت pending آن پاک شده و اجرای آن بلافاصله شروع میشود (وقفه فعال می شود). در زمان t1 وقفه B رخ میدهد، اما چون در اینجا فرض بر این است که اولویت کمتری نسبت به A دارد، بنابراین تا زمانی که ISR روال A عملیات خود را به پایان نرسانده، در حالت معلق pending رها میشود. هنگامی که ISR روال A عملیات خود را به پایان رساند، بیت pending آن به طور خودکار پاک شده و ISR مربوطه فعال میشود.
شکل بالا یک مورد مهم دیگر را نشان میدهد. در اینجا میبینیم که وقفه A فعال میشود و CPU میتواند بلافاصله آن را سرویس دهی کند. وقفه B در حین سرویس A فعال میشود، بنابراین تا زمانی که A تمام شود در حالت معلق pending باقی میماند. وقتی این اتفاق افتاد، بیت معلق pending وقفه B پاک و فعال میشود. اما پس از مدتی وقفه A دوباره شلیک میشود و چون اولویت بیشتری دارد وقفه B تعلیق شده (غیرفعال میشود) و اجرای A بلافاصله شروع میشود. وقتی این کار تمام شد، وقفه B دوباره فعال میشود و کار خود را کامل میکند.
واحد NVIC درجه بالایی از انعطاف پذیری را برای برنامه نویسان فراهم میکند. همانطور که در شکل بالا نشان داده شده است، میتوان یک وقفه را مجبور کرد که در طول اجرای خود دوباره فعال شود، به سادگی بیت معلق آن را دوباره یک کنید. به همین ترتیب، همانطور که در شکل زیر نشان داده شده است، اجرای یک وقفه را میتوان با پاک کردن بیت معلق pending آن در حالی که در حالت pending است لغو کرد.
در این بخش میخواهیم که یک مسئله مهم مربوط به نحوه هشدار دادن واحدهای جانبی به کنترل کننده NVIC در مورد درخواست وقفه را روشن کنیم. هنگامی که یک وقفه رخ میدهد، اکثر واحدهای جانبی STM32 یک سیگنال اختصاصی را به واحد NVIC ارسال میکنند. این امر از طریق یک بیت اختصاصی در حافظه واحد جانبی انجام میشود. این بیت درخواست وقفه واحد جانبی تا زمانی که به صورت دستی توسط کد برنامه پاک شود، یک میماند. در مثال قبل، ما باید بیت معلق IRQ خط EXTI را با استفاده از ماکرو __HAL_GPIO_EXTI_CLEAR_IT() صفر کردیم. اگر آن بیت را پاک نکنیم، یک وقفه جدید دیگر تا زمانی که صفر گردد، اجرا میشود.
شکل بالا به وضوح رابطه بین حالت انتظار (معلق) IRQ واحد جانبی و حالت در انتظار (معلق) ISR را نشان میدهد. سیگنال I/O، المان خارجی است که I/O را هدایت میکند (به عنوان مثال یک کلید فشاری متصل به پین). هنگامی که سطح سیگنال تغییر میکند، خط EXTI متصل به آن I/O یک IRQ تولید میکند و بیت معلق مربوطه یک میشود. در نتیجه واحد NVIC وقفه را ایجاد میکند. هنگامی که پردازنده شروع به سرویس ISR میکند، بیت معلق ISR به طور خودکار پاک میشود، اما بیت در انتظار (معلق) IRQ واحد جانبی، تا زمانی که توسط کد برنامه پاک شود، یک نگه داشته میشود.
شکل بالا مورد دیگری را نشان میدهد. در اینجا ISR را با یک کردن بیت pending آن، مجبور به اجرا میکنیم. چون این بار واحد جانبی خارجی درگیر نیست، نیازی به پاک کردن بیت pending معلق IRQ مربوطه نیست.
از آنجایی که وجود بیت معلق IRQ وابسته به پریفرال (واحد جانبی) است، همیشه مناسب است که از توابع ST HAL برای مدیریت وقفهها استفاده کنیم، و تمام جزئیات زیربنایی را به اجرای HAL واگذار کنیم (مگر اینکه بخواهیم کنترل کامل داشته باشیم). با این حال، به خاطر داشته باشید که برای جلوگیری از از دست دادن وقفههای مهم، بهتر است بیت وضعیت معلق IRQ واحد جانبی را با شروع سرویسدهی ISR، پاک کنیم.با توجه به اینکه هسته پردازنده وقفه ها را ردیابی نمی کند (وقفه ها را در صف قرار نمی دهد)، بنابراین اگر بیت وضعیت معلق IRQ واحد جانبی را در انتهای یک ISR پاک کنیم، ممکن است IRQ های مهمی را که در وسط ISR رخ میدهند، از دست بدهیم.
برای اینکه ببینیم آیا یک وقفه در حالت انتظار است (یعنی رخ داده اما اجرا نمیشود)، میتوانیم از تابع HAL استفاده کنید:
uint32_t HAL_NVIC_GetPendingIRQ(IRQn_Type IRQn);
که اگر IRQ در انتظار نباشد 0 و در غیر این صورت 1 را برمیگرداند.
برای تنظیم بیت انتظار IRQ میتوانیم از تابع HAL زیر استفاده کنیم:
void HAL_NVIC_SetPendingIRQ(IRQn_Type IRQn);
این تابع باعث ایجاد وقفه میشود. یکی از ویژگیهای متمایز پردازندههای Cortex-M این است که طی کدنویسی میتوان یک وقفه را در داخل روتین ISR یک وقفه دیگر اجرا کرد.
برای پاک کردن بیت انتظار IRQ ، میتوانیم از تابع زیر استفاده کنیم:
void HAL_NVIC_ClearPendingIRQ(IRQn_Type IRQn);
یک بار دیگر برای یادآوری، امکان پاک کردن اجرای یک وقفه در انتظار، در داخل ISR یک IRQ دیگر که در حال سرویس میباشد نیز وجود دارد.
برای بررسی اینکه آیا یک ISR فعال است (IRQ در حال سرویس دهی است)، میتوانیم از تابع زیر استفاده کنیم:
uint32_t HAL_NVIC_GetActive(IRQn_Type IRQn);
که اگر IRQ فعال باشد 1 و در غیر این صورت 0 را برمیگرداند.
سطح اولویت در وقفه ها
یکی از ویژگیهای متمایز معماری ARM Cortex-M توانایی اولویتبندی وقفهها است (به جز سه استثنای نرم افزاری اول که دارای اولویت ثابت هستند). اولویت به وقفه اجازه میدهد تا دو چیز را تعریف کنید:
- ISRهایی که در صورت وقفههای همزمان ابتدا اجرا میشوند.
- آن دسته از روتینهایی که به صورت اختیاری میتوان از آنها برای شروع اجرای ISR با اولویت بالاتر استفاده کرد.
مکانیسم اولویت NVIC به طور اساسی بین هستههای Cortex-M0/0+ و Cortex-M3/4/7 متفاوت است.
اولویت وقفه ها در Cortex-M3/4/7
مکانیسم اولویت وقفه در میکروکنترلرهای Cortex-M3/4/7 نسبت به میکروکنترلرهای مبتنی بر CortexM0/0+ پیشرفتهتر بوده و دارای انعطاف پذیری بیشتری هستند.
در هستههای Cortex-M3/4/7 اولویت هر وقفه از طریق رجیستر IPR تعریف میشود. یک رجیستر 8 بیتی در معماری هسته ARMv7-M که حداکثر 255 سطح اولویت مختلف را ارائه میدهد. با این حال، در عمل، میکروکنترلرهای STM32 که این هستهها را پیاده سازی میکنند، تنها از چهار بیت بالای این رجیستر استفاده میکنند و بقیه بیتها را برابر با صفر میبینند.
شکل بالا به وضوح نشان میدهد که چگونه محتوای IPR تفسیر میشود. این بدان معنی است که ما حداکثر شانزده سطح اولویت داریم: 0x00، 0x10، 0x20، 0x30، 0x40، 0x50، 0x60، 0x70، 0x80، 0x90، 0xA0، 0xB0، 0xC0، 0xE. هرچه این عدد کمتر باشد، اولویت بیشتر است. یعنی IRQ با اولویت x10 دارای اولویت بالاتری نسبت به IRQ با سطح اولویت xA0 است. اگر دو وقفه به طور همزمان رخ دهد، ابتدا وقفه با اولویت بالاتر سرویسدهی میشود. اگر پردازنده از قبل در حال سرویس دهی به یک وقفه باشد و وقفهای با اولویت بالاتر فعال میشود، وقفه فعلی به حالت تعلیق در آمده و کنترل به وقفه با اولویت بالاتر منتقل میشود. وقتی این کار تکمیل شد، اگر در این مدت هیچ وقفه دیگری با اولویت بالاتر رخ ندهد، اجرای برنامه به وقفه قبلی برمیگردد.
رجیسر IPR میتواند به طور منطقی به دو بخش تقسیم شود: یک سری بیت که اولویت اصلی preemption priority (پیشدستی) را تعریف میکنند و یک سری بیت که اولویت فرعی sub-priority را تعریف میکنند. سطح اولویت اول، اولویتهای بین ISR ها را مشخص میکند. اگر یک ISR دارای اولویت بالاتر از دیگری باشد، در صورت رخ دادن، از اجرای ISR با اولویت پایینتر جلوگیری میکند. اما اولویت فرعی sub-priority تعیین میکند که در صورت وجود چند ISR در حال انتظار، ابتدا کدام ISR اجرا شود و بر اساس preemption priority اولویت اول ISR عمل نمیکند.
شکل بالا نمونه ای از اولویت در وقفهها را نشان میدهد. A یک IRQ با کمترین اولویت است که در زمان t0 رخ میدهد. ISR مربوطه شروع به اجرا میشود اما IRQ B که دارای اولویت بالاتر است، در زمان t1 رخ میدهد و اجرای ISR مربوط به A متوقف میشود. پس از مدتی، C IRQ در زمان t2 رخ میدهد و اجرای ISR مربوط به B متوقف میشود و ISR مربوط به C شروع به اجرا میکند. وقتی این کار تمام شد، اجرای B ISR تا زمانی که تمام شود از سر گرفته میشود. هنگامی که این اتفاق نیز افتاد، اجرای A ISR از سر گرفته میشود. این مکانیسم “تودرتو” ناشی از اولویتهای وقفه، منجر به نام گذاری کنترلکننده NVIC میشود که کنترلکننده وقفه بردار تودرتو است.
شکل بالا نشان میدهد که چگونه اولویت فرعی sub-priority بر اجرای چندین ISR در انتظار تأثیر می ذارد. در اینجا سه وقفه داریم که همه با حداکثر اولویت یکسان هستند. در زمان t0 وقفه IRQ A رخ میدهد و بلافاصله سرویس دهی میشود. در زمان شلیک t1 وقفه B IRQ رخ میدهد، اما از آنجایی که دارای هم سطح اولویت IRQ های دیگر است، در حالت انتظار رها میشود. در زمان t2 نیز C IRQ رخ میدهد، اما به همان دلیلی قبلی، توسط پردازنده در حالت انتظار رها میشود. هنگامی که A ISR تمام میشود، ابتدا C IRQ سرویس دهی میشود، زیرا اولویت فرعی sub-priority بالاتری نسبت به B دارد. تنها زمانی که C ISR تمام شود، B IRQ میتواند اجرا شود.
نحوه تقسیم منطقی بیتهای IPR توسط رجیستر SCB->AIRCR (یک زیرگروه از بیتهای رجیستر System Control Block (SCB)) تعریف میشود. محتوای رجیستر IPR برای همه ISR ها یکی است. هنگامی که یک طرح اولویت را تعریف کردیم (که در HAL به priority grouping شناخته میشود)، این طرح برای تمام وقفههای استفاده شده در سیستم مشترک است.
شکل بالا هر پنج حالت ممکن برای رجیستر IPR را نشان میدهد. جدول زیر حداکثر تعداد مجاز سطوح اولویت اصلی preemption و سطوح اولویت فرعی sub-priority در هر طرح تقسیمبندی را نشان میدهد.
کتابخانهCubeHAL تابع زیر را برای تعیین اولویت برای هر IRQ ارائه میدهد:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
کتابخانه HAL طوری طراحی شده است که میتوان PreemptPriority و SubPriority را با یک عدد سطح اولویت از 0 تا 16 پیکربندی کرد. مقدار وارد شده به صورت خودکار به مهمترین بیتهای مربوطه منتقل میشود. این امر انتقال کد به میکروکنترلرهای دیگر با تعداد بیت های اولویت متفاوت را ساده میکند (به همین دلیل است که تنها قسمت سمت چپ رجیستر IPR استفاده میشود).
برای تعریف priority grouping ، یعنی نحوه تقسیم رجیستر IPR بین اولویت preemption و اولویت sub-priority، میتوان از تابع زیر استفاده کرد:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
که در آن پارامتر PriorityGroup یکی از ماکروهای ستون NVIC Priority Group در جدول بالاست.
مثال نحوه عملکرد اولویت وقفه را نشان میدهد.
59 uint8_t blink = 0;
60
61 int main(void) {
62 GPIO_InitTypeDef GPIO_InitStruct;
63
64 HAL_Init();
65
66 /* GPIO Ports Clock Enable */
67 __HAL_RCC_GPIOC_CLK_ENABLE();
68 __HAL_RCC_GPIOB_CLK_ENABLE();
69 __HAL_RCC_GPIOA_CLK_ENABLE();
70
71 /*Configure GPIO pin : PC13 */
72 GPIO_InitStruct.Pin = GPIO_PIN_13 ;
73 GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
74 GPIO_InitStruct.Pull = GPIO_PULLDOWN;
75 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
76
77 /*Configure GPIO pin : PB2 */
78 GPIO_InitStruct.Pin = GPIO_PIN_2 ;
79 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
80 GPIO_InitStruct.Pull = GPIO_PULLUP;
81 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
82
83 /*Configure GPIO pin : PA5 */
84 GPIO_InitStruct.Pin = GPIO_PIN_5;
85 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
86 GPIO_InitStruct.Pull = GPIO_NOPULL;
87 GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
88 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
89
90 HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0x1, 0);
91 HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
92
93 HAL_NVIC_SetPriority(EXTI2_IRQn, 0x0, 0);
94 HAL_NVIC_EnableIRQ(EXTI2_IRQn);
95
96 while(1);
97 }
98
99 void EXTI15_10_IRQHandler(void) {
100 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
101 }
102
103 void EXTI2_IRQHandler(void) {
104 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
105 }
106
107 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
108 if(GPIO_Pin == GPIO_PIN_13) {
109 blink = 1;
110 while(blink) {
111 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
112 for(int i = 0; i < 1000000; i++);
113 }
114 }
115 else {
116 blink = 0;
117 }
118 }
در اینجا ما دو IRQ مرتبط با خطوط EXTI 2 و 13 داریم. ISR های مربوطه HAL HAL_GPIO_EXTI_IRQHandler() را فراخوانی میکنند که در واقع این تابع با فراخوانی HAL_GPIO_EXTI_Callback() GPIO درگیر وقفه را مشخص میکند. وقتی کلید متصل به پین PC13 فشار داده میشود، ISR یک حلقه بینهایت را تا زمانی که متغیره سراسری blink > 0 باشد، شروع میکند. این حلقه باعث می شود LED LD2 به سرعت چشمک بزند. هنگامی که کلید متصل به پین PB2 فشرده میشود، EXTI2_IRQHandler() فعال شده و باعث میشود تابع HAL_GPIO_EXTI_IRQHandler() متغیر blink را روی 0 مقداردهی کند. اکنون تابع EXTI15_10_IRQHandler() میتواند پایان یابد.
لطفاً توجه داشته باشید که این روش واقعاً بدی برای مقابله با وقفهها است. قفل کردن MCU در داخل یک وقفه بسیار اشتباه است. این برنامه تنها یک مثال است. هر ISR باید به گونهای طراحی شود که تا حد امکان کمترین دوام را داشته و سریع روتین مربوطه پایان یابد. در غیر این صورت سایر ISR های اساسی میتوانند برای مدت طولانی دیده نشوند و اطلاعات مهمی را که از سایر واحدهای جانبی می آید از دست بدهیم.
توجه به چند نکته اساسی در مورد وقفهها ضروری است. اول از همه، بر خلاف میکروکنترلرهای مبتنی بر Cortex-M0/0+، هستههای Cortex-M3/4/7 امکان تغییر dynamic اولویت یک وقفه را فراهم میکنند، حتی اگر از قبل فعال شده باشد. ثانیاً زمانی که اولویت گروه بندی به صورت dynamic کاهش پیدا میکند، باید مراقب بود.
مثلا سه ISR با سه اولویت کاهش داده شده داریم (اولویت در داخل پرانتز مشخص شده است): A(0x0)، B(0x10)، C(0x20). فرض کنید زمانی که priority grouping برابر با NVIC_PRIORITYGROUP_4 بود، این اولویت ها را تعریف کردهایم. اگر آن را به سطح NVIC_PRIORITYGROUP_1 کاهش دهیم، سطوح preemption فعلی به عنوان Sub-priorities تفسیر میشوند. این امر باعث میشود که روتینهای سرویس وقفه A، B و C سطح preemption یکسانی داشته باشند (یعنی x0)، و امکان جلوگیری از آنها وجود نخواهد داشت. به عنوان مثال، با نگاهی به شکل زیر، میتوانیم ببینیم که وقتی گروهبندی اولویت از 4 به 1 کاهش مییابد، چه اتفاقی برای اولویت ISR C میافتد. هنگامی که گروهبندی اولویت روی 4 تنظیم میشود، اولویت C ISR فقط دو سطح از حداکثر سطح اولویت (که 0 است) پایین تر است (بالاترین سطح بعدی x10 است که اولویت B است). این به معنی است که A و B هر دو میتوانند از اجرای روتین C جلوگیری کنند. با این حال، اگر گروه بندی اولویت را به 1 کاهش دهیم، اولویت C 0x0 می شود (فقط بیت 7 به عنوان اولویت عمل میکند) و بیت های باقی مانده توسط کنترل کننده NVIC به عنوان sub-priority تفسیر می شوند که میتواند منجر به سناریوی زیر شود:
- هیچ کدام از وقفه ها نمیتوانند از یکدیگر پیشی بگیرند.
2. اگر وقفه C ایجاد شود و CPU در حال سرویس دهی به وقفه دیگر نباشد، روتین C بلافاصله اجرا میشود.
3. اگر CPU در حال ارائه سرویس به C ISR باشد و پس از مدت کوتاهی A و B فعال شوند، CPU ابتدا روتین A و سپس روتین B را پس از تکمیل C اجرا میکند.
4. اگر CPU در حال ارائه سرویس به ISR دیگری باشد، اگر وقفه C رخ دهد و پس از مدت کوتاهی A و B فعال شوند، ابتدا A و سپس B و سپس C اجرا میشوند.
برای به دست آوردن اولویت یک وقفه، HAL تابع زیر را تعریف میکند:
void HAL_NVIC_GetPriority(IRQn_Type IRQn,uint32_t PriorityGroup, uint32_t* pPreemptPriority,
\ uint32_t* pSubPriority);
این تابع کمی مبهم است، زیرا با HAL_NVIC_SetPriority(): تفاوت دارد .در اینجا باید PriorityGroup را نیز مشخص کنیم، در حالی که تابع HAL_NVIC_SetPriority () آن را به صورت داخلی محاسبه میکند.
priority grouping فعلی را میتوان با استفاده از تابع زیر بدست آورد:
uint32_t HAL_NVIC_GetPriorityGrouping(void);
پوشاندن همه وقفه ها به یکباره یا بر اساس اولویت
گاهی اوقات میخواهیم مطمئن شویم که کد ما اجازه اجرای وقفهها یا کدهای دارای اولویت بیشتر را نمیدهد. یعنی میخواهیم مطمئن شویم که کد ما امن thread-safe است. پردازندههای مبتنی بر Cortex-M میتوانند با استفاده از دو رجیستر ویژه به نامهای PRIMASK و FAULTMASK به طور موقت اجرای تمام وقفه ها و استثناها را بدون غیرفعال کردن یک به یک آنها، پنهان کنند.
حتی اگر این رجیسترها 32 بیتی باشند، فقط اولین بیت برای فعال/غیرفعال کردن وقفهها و استثناها استفاده میشود. دستور assembly CPSID i با تنظیم بیت PRIMASK روی 1، تمام وقفهها را غیرفعال و که دستورالعمل CPSIE i با صفر کردن PRIMASK آنها را فعال میکند. همچنین دستورالعمل CPSID f با تنظیم بیت FAULTMASK بر روی 1، همه استثناها (به جز NMI) را غیرفعال و که دستورالعملهای CPSIE f آنها را فعال میکند.
پکیج CMSIS-Core چندین ماکرو ارائه میدهد تا بتوانیم از آنها برای انجام این عملیات استفاده کنیم: __disable_irq() و __enable_irq() به طور خودکار PRIMASK را فعال و غیر فعال میکنند. هر task مهمی را میتوان بین این دو ماکرو قرار داد، همانطور که در زیر نشان داده شده است:
... __disable_irq();
/* All exceptions with configurable priority are temporarily disabled. You can place critical code here */
... __enable_irq();
با این حال، در نظر داشته باشید که، به عنوان یک قاعده کلی، وقفه باید فقط برای مدت کوتاهی پوشانده (غیر فعال) شود، در غیر این صورت ممکن است وقفه های مهم را از دست بدهید. ( وقفهها در صف قرار نمی گیرند)
ماکرو دیگری که میتوانیم استفاده کنیم __set_PRIMASK(x) است که x محتوای رجیستر PRIMASK (0 یا 1) است. ماکرو __get_PRIMARK() محتوای رجیستر PRIMASK را برمیگرداند. در عوض، ماکروهای __set_FAULTMASK(x) و __get_FAULTMASK() امکان دستکاری رجیستر FAULTMASK را میدهند.
نکته مهم اینکه وقتی رجیستر PRIMASK دوباره روی صفر تنظیم میشود، تمام وقفههای در انتظار با توجه به اولویت خود اجرا (سرویس دهی) میشوند: PRIMASK باعث می شود که بیت pending وقفه یک شود اما ISR سرویس دهی نمیشود. به همین دلیل است که میگوییم وقفه ها پنهان هستند و غیرفعال نیستند. وقفه ها به محض پاک شدن PRIMASK شروع به سرویس دهی میشوند.
هستههای Cortex-M3/4/7 میتوانند به طور انتخابی وقفه ها را بر اساس اولویت پنهان کنند. رجیستر BASEPRI استثناها یا وقفه ها را بر اساس اولویت پنهان میکند. طول رجیستر BASEPRI برابر با IPR است ( 4 بیت بالای MCU ). وقتی BASEPRI روی 0 تنظیم شود، غیرفعال میشود. هنگامی که روی یک مقدار غیر صفر تنظیم میشود، استثناها (از جمله وقفهها) را که سطح اولویت یکسان یا پایین تر دارند را مسدود میکند، در حالی که همچنان اجازه میدهد استثناهایی با سطح اولویت بالاتر توسط پردازنده پذیرفته شوند. برای مثال، اگر رجیستر BASEPRI روی x60 تنظیم شده باشد، تمام وقفههای با اولویت بین x60-0xFF غیرفعال میشوند. به یاد داشته باشید که در هستههای Cortex-M هر چه مقدار اولویت بیشتر باشد سطح اولویت وقفه کمتر است. ماکرو __set_BASEPRI(x) اجازه میدهد تا محتوای رجیستر BASEPRI را تنظیم کنید: ناگفته نماند که HAL به طور خودکار سطوح اولویت را به بیت های MSB منتقل میکند. بنابراین، اگر بخواهیم تمام وقفهها را با اولویت بالاتر از 2 غیرفعال کنیم، باید مقدار x20 را به ماکرو __set_BASEPRI() منتقل کنیم. همچنین میتوانیم از کد زیر استفاده کنیم:
__set_BASEPRI(2 << (8 - __NVIC_PRIO_BITS));
وقفهها به طور ویژه ای برای میکروکنترلرها مهم هستند. با وقوع وقفه در چرخه عملکرد میکروکنترلر، تابع سرویس وقفه فراخوانی شده و کد داخل آن اجرا میشود. یک وقفه برای وقوع میتواند عوامل مختلفی مانند تحریک وقفه خارجی، وقفه تایمر، وقفه مبدل آنالوگ به دیجیتال، وقفه ارتباط سریال و … داشته باشد که با استفاده از وقفه، قابلیت میکروکنترلر برای ارزیابی سخت افزار های موجود و خارجی متصل به آن افزایش مییابد. وقفهها دارای اولویت در اجرا هستند و همیشه وقفه با اولویت بالا نسبت به سایرین دارای اولویت پایین تر ، اجرا می شوند. در این فصل، وقفه خارجی را در نظر خواهیم گرفت. در میکروکنترلرهای Cortex-M، یک واحد کنترل کننده وقفه بردار تو در تو (NVIC) برای مدیریت وقفهها وجود دارد. وقفه تودرتو به این معنی است که اگر یک تابع سرویس وقفه اجرا شود و وقفه با اولویت بالا فعال شود، عملکرد سرویس وقفه در حال اجرا در آن نقطه متوقف شده و تابع سرویس وقفه با اولویت بالا اجرا خواهد شد. واحد NVIC در میکروکنترلر STM32F303CCT از 16 اولویت وقفه از GPIO_EXTI0 تا GPIO_EXTI15 پشتیبانی میکند. کاربر میتواند نوع وقفه و اولویت را انتخاب کند.
تنظیمات پروژه وقفه خارجی
در مثال زیر قصد داریم PA3 را به عنوان پایه وقفه خارجی انتخاب کرده و با تحریک پایه مربوطه، که در واقع فشار دادن دکمه USER_BUTTON میباشد، وضعیت LED_USER در زیر روال وقفه تغییر پیدا کند. در قسمت نرم افزار STM32CubeMX و Pinout با کلیک بر روی پین PA3 وقفهی خارجی را فعال میکنیم. پس از تنظیم پین PA3 برای وقفه خارجی، باید واحد NVIC را برای وقفه مربوطه تنظیم کنیم. برای این منظور از نوار سمت چپ بخش System Core دکمه NVIC را انتخاب میکنیم. بخش تنظیمات NVIC در شکل زیر نشان داده شده است. هر عاملی که میتواند یک وقفه خارجی ایجاد کند در لیست گنجانده شده است. همانطور که نشان داده شده است، وقفههای مختلف و وقفه های خط EXTI [15:0] در لیست هستند و باید با علامت تیک در مربع های مرتبط فعال شوند. از بخش Preemption Priority، اولویت باید از 0 تا 15 انتخاب شود.
حال با توجه به مدار بسته شده بر روی پین PA3 که به صورت سخت افزاری PULL-UP شده است:
از نوار سمت چپ بخش System Core دکمه GPIO را انتخاب کرده و تنظیمات پین PA3 را مانند شکل زیر انجام میدهیم:
در واقع با توجه به اینکه پین PA3 به صورت سخت افزاری Pull-UP شده است، در بخش PA3 Configuration حالت GPIO Mode را گزینه Falling Edge Detection قرار میدهیم. در اخر کد پروژه را تولید میکنیم.
بدنه تابع MX_GPIO_Init در شکل زیر نشان داده شده است:
توابع سرویس به وقفهها در داخل فایل stm32f1xx_it.c هستند: