در قسمت چهارم از آموزش STM32 با توابع LL، با واحد RCC آشنا شدیم و جزئیات و دلیل وجود کلاک در مدارات دیجیتال را بررسی کردیم، همچنین گفتیم که کلاک ورودی به میکروکنترلر چگونه در میکروکنترلر با استفاده از PLL افزایش و با استفاده از Prescaler کاهش مییابد. در ادامه مدار Reset که برای ریست کردن میکروکنترلر استفاده میشود را معرفی کردیم و گفتیم که با ریست کردن میکروکنترلر عملا در برنامه چه اتفاقی خواهد افتاد و این عمل باعث چه چیزی خواهد شد. در نهایت هم یک برد آموزشی بسیار ساده اما کاربردی به اسم blue pill board را برای پیشبرد ادمهی آموزش به شما معرفی کردیم.
پس از گذشت چهار قسمت تازه میتوان گفت که از مقدمات عبور کردیم و قرار است که به صورت جدی وارد فاز برنامهنویسی میکروکنترلر بشویم. پس با ما همراه باشید تا گام اول را به خوبی برداریم و نقشهی راه را برای ادامهی مسیر مشخص کنیم.
همانطور که بیان شد چون این قسمت، اولین قسمتی است که به طور جدی وارد فاز برنامهنویسی میکروکنترلر میشویم، میخواهیم از جنبههایی متفاوتی یک واحد جانبی را بررسی و تحلیل کنیم و یک سری نتایج را به صورت مستدل بیان کنیم.
این تحلیلها به گونهای ارائه خواهند شد که جامعیت داشته باشند و قابلیت تعمیم به جاهای دیگر را نیز داشته باشند. در واقع هدف این است که ماهیگیری به شما آموخته شود تا به نقطهای برسید که نقشهی راه برایتان مشخص شود و اگر جایی نیاز بود که خودتان کاری را پیش ببرید، ادامهی مسیر برایتان هموار باشد.
اما در این قسمت قصد داریم چه مواردی را بررسی کنیم؟
اگر به خاطر داشته باشید در قسمت سوم یک کد برای GPIO نوشتیم و به شما قول دادیم که کد نوشته شده را در قسمت مربوط به GPIO به طور کامل بررسی و تحلیل کنیم.
پس مشخصا در این قسمت قصد داریم که در رابطه با GPIO صحبت کنیم.
اما آن بررسی و تحلیلهایی که گفتیم قرار است نقشهی راه را برای شما مشخص و ادامهی مسیر را هموار کند شامل چه چیزهایی میشود؟
ما در ابتدا کد GPIO با توابع LL را مینویسم و توضیح خواهیم داد که چگونه باید از این توابع در برنامه استفاده کرد. سپس همین کد GPIO را با استفاده از توابع HAL خواهیم نوشت و در هر دو حالت سرعت پین GPIO که خروجی کردیم را اندازهگیری و مقایسه میکنیم.
پس از مقایسه مشاهده خواهیم کرد که سرعت توابع LL به طور چشمگیری بیشتر است. این تفاوت فاحش سرعت، چندین دلیل دارد که ما همهی این دلایل را به طور کامل بررسی و تحلیل خواهیم کرد.
خب اکنون اجازه بدهید کمی در رابطه با خود GPIO و این که چه چیزی هست صحبت بکنیم.
GPIO (General-purpose input/output)
به طور خیلی ساده، وظیفهی اصلی واحد GPIO کنترل وضعیت پینهای میکروکنترلر است. ما با استفاده از رجیسترهای این واحد، میتوانیم ورودی یا خروجی بودن پین، مقدار و سرعت پین در حالت خروجی و … را مشخص کنیم. اما همانطور که میدانید قرار نیست که ما با استفاده از رجیسترها، وضعیت پینها را کنترل کنیم، بلکه با استفاده از توابع LL این کار را انجام خواهیم داد.
به کدی که در قسمت سوم نوشتیم دقت کنید:
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_mDelay(500); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_mDelay(500);
اولا که این کد در حلقهی while برنامه نوشته شده است، یعنی مداوم یک پین از میکروکنترلر 0 و 1 خواهد شد.
تابع LL_GPIO_SetOutputPin، برای High کردن پینی از میکروکنترلر که از قبل به عنوان خروجی تنظیم شد (همان تنظیماتی که در نرمافزار STM32CubeMX انجام میدادیم در واقع پین را به عنوان خروجی تنظیم میکرد) به کار میرود.
بیایید به تعریف تابع برویم، ببینیم که در بدنهی تابع چه اعمالی انجام شده است که باعث میشود یک پین خروجی High شود.
به تعریف تابع توجه کنید:
__STATIC_INLINE void LL_GPIO_SetOutputPin(GPIO_TypeDef *GPIOx, uint32_t PinMask) { WRITE_REG(GPIOx->BSRR, (PinMask >> GPIO_PIN_MASK_POS) & 0x0000FFFFU); }
در تعریف تابع فقط عبارت زیر وجود دارد:
WRITE_REG(GPIOx->BSRR, (PinMask >> GPIO_PIN_MASK_POS) & 0x0000FFFFU);
حال باید به تعریف تابع WRITE_REG برویم، تا ببینیم که این تابع چه کاری انجام میدهد:
#define WRITE_REG(REG, VAL) ((REG) = (VAL))
با توجه به تعریف بالا، این تابع ورودی دوم را در ورودی اول خود قرار میدهد.
ورودی دوم، عبارتی شامل 0 و 1 منطقی است و ورودی اول یک رجیستر از GPIO است، یعنی رجیستر BSRR. پس برای High کردن یک پین میکروکنترلر، باید مقداری متناظر با همان پین در رجیستر BSRR نوشته شود.
این تابع بر اساس مستندات میکروکنترلر نوشته شده است. در مستندات گفته شده است که برای High کردن یک پین از میکروکنترلر باید در بیت متناظر با آن در رجیستر BSRR مقدار 1 منطقی قرار داده شود.
حال اگر شما در عبارت (PinMask >> GPIO_PIN_MASK_POS) & 0x0000FFFFU) که همان ورودی دوم تابع است به جای PinMask یکی از مقادیری که در توضیحات تابع گفته شده است را قرار بدهید، و عبارت بالا را محاسبه کنید میبینید که بیت متناظر با آن پین از میکروکنترلر که قرار است High شود مقدار 1 منطقی را دارد. ما در اینجا به جای PinMask مقدار LL_GPIO_PIN_0 را قرار دادیم.
توضیحات تابع کجاست؟
دو روش برای پیدا کردن توضیحات تابع وجود دارد، یک روش خواندن فایل Description of STM32F1 HAL and low-layer drivers – User manual، که هم توابع LL را توضیح میدهد و هم توابع HAL را. روش دیگر در نرمافزار است، قبل از تعریف هر تابع، در فایل مربوط به آن، توضیحات آن تابع نیز وجود دارد که میتوانید از این توضیحات استفاده بکنید.
مثلا در نرمافزار برای تابع LL_GPIO_SetOutputPin، قبل از تعریف تابع، توضیحات زیر آورده شده است:
/** * @brief Set several pins to high level on dedicated gpio port. * @rmtoll BSRR BSy LL_GPIO_SetOutputPin * @param GPIOx GPIO Port * @param PinMask This parameter can be a combination of the following values: * @arg @ref LL_GPIO_PIN_0 * @arg @ref LL_GPIO_PIN_1 * @arg @ref LL_GPIO_PIN_2 * @arg @ref LL_GPIO_PIN_3 * @arg @ref LL_GPIO_PIN_4 * @arg @ref LL_GPIO_PIN_5 * @arg @ref LL_GPIO_PIN_6 * @arg @ref LL_GPIO_PIN_7 * @arg @ref LL_GPIO_PIN_8 * @arg @ref LL_GPIO_PIN_9 * @arg @ref LL_GPIO_PIN_10 * @arg @ref LL_GPIO_PIN_11 * @arg @ref LL_GPIO_PIN_12 * @arg @ref LL_GPIO_PIN_13 * @arg @ref LL_GPIO_PIN_14 * @arg @ref LL_GPIO_PIN_15 * @arg @ref LL_GPIO_PIN_ALL * @retval None */ __STATIC_INLINE void LL_GPIO_SetOutputPin(GPIO_TypeDef *GPIOx, uint32_t PinMask) { WRITE_REG(GPIOx->BSRR, (PinMask >> GPIO_PIN_MASK_POS) & 0x0000FFFFU); }
همانطور که در بالا مشاهده میکنید ابتدا توضیحاتی در مورد عملکرد تابع آورده شده است و سپس پارامترهای ورودی را توضیح میدهد و میگوید که برای هر ورودی چه مقادیری میتوانند قرار بگیرند.
مثلا برای اینکه PA0 مقدارش High شود باید عبارت LL_GPIO_PIN_0 به عنوان دومین پارامتر در تابع قرار بگیرد.
البته شما تنها با خواندن توضیحات تابع نیز میتوانید یک واحد را راهاندازی کنید و نیازی به اینکه در تابع چه چیزی نوشته شده است ندارید، اما بررسی تعریف و جزئیات تابع و همینطور ارتباطش با سختافزار و رجیسترها درک شما را عمیقتر میکند.
در بعضی اوقات اصلا شما اجازه و دسترسی به بدنهی تابع را ندارید و فقط میتوانید با توجه به توضیحات تابع از آن استفاده کنید، این مورد از این جهت گفته شد که فکر نکنید باید همیشه همه چیز تابع مشخص باشد و همیشه هم جزئیات تابع بررسی شود. اما خب در اینجا دسترسی به جزئیات تابع امکانپذیر است و شما در صورت نیاز میتوانید آنها را بررسی کنید. پیشنهاد میشود در ابتدای کار سعی کنید جزئیات توابع را به خوبی بررسی و تحلیل کنید و رابطهی توابع با سختافزار را پیدا کنید.
پس از تابع LL_GPIO_SetOutputPin به تابع LL_mDelay میرسیم که به اندازهی عددی که در ورودیاش قرار میگیرد بر حسب ms تاخیر ایجاد میکند. چون این تابع بر اساس systick timer نوشته شده است در این قسمت جزئیات این تابع را بررسی نمیکنیم و فقط با نحوهی عملکردش که همان تاخیر به میزان عدد ورودی بر حسب ms است، آشنا میشویم.
تابع دیگر، تابع GPIO_ResetOutputPin است که برای Low کردن پینی از میکروکنترلر که از قبل به عنوان خروجی تنظیم شد به کار میرود.
به بدنهی تابع توجه کنید:
__STATIC_INLINE void LL_GPIO_ResetOutputPin(GPIO_TypeDef *GPIOx, uint32_t PinMask) { WRITE_REG(GPIOx->BRR, (PinMask >> GPIO_PIN_MASK_POS) & 0x0000FFFFU); }
این تابع همان عملیات تابع LL_GPIO_SetOutputPin را انجام میدهد، اما بر روی رجیستر BRR. برای Low کردن پین میکروکنترلر باید مقداری متناظر با همان پین در رجیستر BRR قرار داده شود.
جزئیات این تابع به دلیل مشابهت با تابع LL_GPIO_SetOutputPin بررسی نمیشود.
پس تا اینجا نتیجه میگیریم که برای High کردن پین باید در رجیستر BSRR و برای Low کردن آن باید در رجیستر BRR مقدار 1 منطقی را در بیت متناظر با پین موردنظر نوشت.
اگر به خاطر داشته باشد در قسمت دوم گفتیم که سرعت توابع LL بسیار بیشتر از توابع HAL است و بررسی اینکه چرا سرعت توابع LL بیشتر است را به آینده موکول کردیم، اکنون همان آیندهای است که قولش را داده بودیم. پس با ما همراه باشید تا دلیل این امر را متوجه بشوید.
این بار به کد GPIOای که با توابع HAL نوشته شده است توجه کنید:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_Delay(500);
ابن کد دقیقا عملکرد همان کدی را دارد که با توابع LL نوشتیم.
ابتدا تست را انجام میدهیم تا اختلاف سرعت توابع LL و HAL را ببینیم، سپس توابع HAL را نیز بررسی خواهیم کرد. اما قبل از تست، به سناریوی تست که در ادامه ذکر میگردد توجه کنید.
سناریوی تست به این صورت است که میخواهیم سرعت 0 و 1 شدن یک پین از میکروکنترلر را با استفاده از لاجیک آنالایزر رصد کنیم.
در واقع ما تابع Delay را به این دلیل که چشم قادر به دیدن 0 و 1 شدن پین میکروکنترلر بر روی LED باشد، به کار بردیم. حال که قرار است با استفاده از لاجیک آنالایزر نتیجه را رصد کنیم دیگر نیازی به تابع Delay نیست.
و نکتهی دیگر اینکه هر سری که شرط حلقهی while چک میشود باید زمانی صرف شود. همین زمانی که صرف چک کردن شرط حلقهی while میشود، بر روی سناریوی تست اثر بد میگذارد. برای رفع این مشکل کدی که درون حلقهی while مینویسیم باید به صورت زیر باشد:
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_0); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_0); . . .
در واقع ما با تکرار کد، اثر زمانِ بررسی شرط حلقهی while را کم میکنیم. ما در بالا فقط سه بار کد را تکرار کردیم، اما اگر خودتان خواستید تست را انجام بدهید باید تعداد تکرار را بسیار بیشتر باشد.
در هر دو حالت؛ برنامهی نوشته شده را بر روی حافظهی Flash میکروکنترلر دانلود میکنیم و با استفاده از لاجیک آنالایزر سرعت پین میکروکنترلر را اندازه میگیریم.
ابتدا به تصاویر زیر دقت کنید:
همانطور که مشاهده میکنید، سرعت با توابع LL تقریبا 5.6 برابر بیشتر از توابع HAL است!
آیا این امر اتفاقی بوده است؟ مشخص است که خیر.
چندین عامل مختلف باعث شده است که سرعت تا این اندازه بیشتر شود، در ادامه همهی این عوامل را بررسی میکنیم.
به طور کلی اگر بخواهیم تنها یک دلیل ارائه بدهیم، آن دلیل چیزی نیست به جز نحوهی پیادهسازی توابع LL و HAL. اما خود این دلیل جزئیاتی دارد که باید خیلی دقیق بررسی شود تا مسئله به خوبی روشن شود.
اگر به برنامهی نوشته شده با توابع LL توجه بکنید، میبینید که برای 0 و 1 کردن پین خروجی دو تابع LL_GPIO_SetOutputPin و LL_GPIO_ResetOutputPin به کار رفته است و در این دو تابع از دو رجیستر BSRR و BRR استفاده شده است. اما در برنامهی نوشته شده با توابع HAL همین کار را تنها با یک تابع به اسم HAL_GPIO_WritePin انجام داده است و در ابن تابع فقط از رجیستر BSRR استفاده شده است!
کار این دو رجیستر چیست؟ رجیستر BRR یک رجیستر 16 بیتی است که برای 0 کردن پینهای میکروکنترلر به کار میرود. و رجیستر BSRR یک رجیستر 32 بیتی است که 16 بیت اول آن برای 1 کردن و 16 بیت دوم آن برای 0 کردن پینهای میکروکنترلر به کار میرود.
در واقع ST گفته است که برای 1 کردن پینهای میکروکنترلر از 16 بیت اول رجیستر BSRR و برای 0 کردن پینهای میکروکنترلر از رجیستر BRR استفاده کنید، اما اگر به هر دلیلی نمیخواهید که از رجیستر BRR استفاده بکنید، هم 0 کردن و هم 1 کردن پینها با استفاده از رجیستر BSRR امکانپذیر است.
با توضیحات و عکسهای بالا نتیجه میگیریم که با وجود رجیستر BSRR، دیگر نیازی به رجیستر BRR نیست. بله این نتیجه درست است، اما به هزینهی اینکه سرعت را فدا کنیم. کاری که دقیقا در توابع HAL انجام شده است.
ابتدا به پیادهسازی تابع HAL_GPIO_WritePin توجه کنید:
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) { /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); assert_param(IS_GPIO_PIN_ACTION(PinState)); if (PinState != GPIO_PIN_RESET) { GPIOx->BSRR = GPIO_Pin; } else { GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U; } }
در این تابع با استفاده از یک شرط میگوید که اگر ورودی سوم یعنی PinState برابر با GPIO_PIN_RESET نبود پس قصد 1 کردن پین میکروکنترلر است و مقدار ورودی دوم یعنی GPIO_Pin باید در 16 بیت اول رجیستر BSRR قرار بگیرد، در غیر اینصورت هدف صفر کردن پین میکروکنترلر است و باید مقدار ورودی دوم در 16 بیت دوم رجیستر BSRR قرار بگیرد.
خب اولا که در این تابع از دستورات شرطی استفاده شده است و ما میدانیم که بررسی هر دستور شرطی خود مستلزم صرف زمان است، از سمتی دیگر در این دستور شرطی از دستور شیفت بیتی (GPIO_Pin << 16U) نیز استفاده شده است که این دستور هم صرف زمانی دیگر را میطلبد.
پس تا اینجا استفاده از دستورات شرطی و شیفت بیتی از عوامل کاهش سرعت در توابع HAL محسوب میشوند.
عامل دیگر این اختلاف سرعت این است که توابع LL به صورت توابع معمولی پیادهسازی نشدهاند، بلکه این توابع به صورت INLINE پیادهسازی شدهاند.
در زبان C توابع به دو صورت در برنامه به کار برده میشوند، یک روش این است که وقتی برنامه به تابع موردنظر رسید، برنامه آن تابع را فراخوانی میکند. روش دیگر این است که قبل از کامپایل، بدنه یا کدی که درون تابع نوشته شده است در محلی از برنامه که از تابع استفاده شده است، کپی میشود و دیگر در زمان اجرای برنامه هیچ تابعی فراخوانی نمیشود. به این نوع توابع، توابع INLINE میگویند.
سرعت توابع INLINE بیشتر از توابع معمولی است و اگر دقت کرده باشید در تعریف توابع LL از عبارت STATIC_INLINE__ استفاده شده است که نشان از INLINE بودن این توابع دارد.
پس سه عاملی که باعث اختلاف سرعت فاحش شدند، شامل دستورات شرطی و شیفت بیتی و توابعی که به صورت INLINE پیادهسازی شدند، بودند. البته عوامل تاثیرگذار دیگر مثل ساختارهای شیگرائی که در توابع HAL به وفور و بیشتر از توابع LL یافت میشود نیز وجود دارد که بنا به اهمیت کمتر از عوامل ذکر شده به آنها پرداخته نشده است.
ما در این مقاله از جهات مختلفی به شرح مسائلی که ممکن است تا به حال برایتان پیش آمده باشد و جوابشان را نمیدانستید یا در آینده با این مسائل برخورد کنید، پرداختیم. در قسمتهای بعد سعی میشود تمرکز بر روی نکات دیگری گذاشته شود تا هر آن چیزی که برای کار کردن با میکروکنترلرها نیاز است پوشش داده شود. اما از آنجایی که سعی شده است اصول به گونهای بیان شود که قابل تعمیم به سایر بخشها نیز باشد، انتظار میرود که پس از خواندن این مقالات خودتان تحلیل و تمرین کنید تا مهارتتان هرچه بیشتر شود. و اگر سوالی یا مشکلی داشتید با ما در میان بگذارید.
در قسمت ششم در رابطه با GPIO در حالت Input صحبت خواهم کرد.
منبع:سیسوگ