در قسمت پنجم از آموزش STM32 با توابع LL، در رابطه با GPIO در حالت خروجی صحبت کردیم و به بررسی جزئیات رجیسترهای GPIO پرداختیم که همین بررسی جزئیات و البته یک سری توضیحات دیگر، درک ما را از تفاوت سرعت فاحش بین توابع HAL و LL بیشتر کرد.
در این قسمت قصد داریم GPIO در حالت ورودی را راهاندازی کنیم.
شاید از قسمت قبل برایتان سوال پیش آمده باشد که اگر بخواهیم چندین پین از میکروکنترلر را در حالت خروجی راهاندازی کنیم باید چه کار کنیم؟ (چون در قسمت قبل فقط یک پین را در حالت خروجی راهاندازی کردیم) این کار راههای متفاوتی دارد، اما ما بهترین و اصولیترین راه را در این قسمت برای شما شرح خواهیم داد.
در واقع هدف ما در این قسمت این است که ترکیبی از GPIO در حالت ورودی و خروجی را با هم راهاندازی کنیم تا هم با GPIO در حالت ورودی آشنا شویم و هم یاد بگیریم که اگر خواستیم چندین پین از میکروکنترلر را در حالت خروجی راهاندازی کنیم باید از چه توابعی استفاده کنیم و نحوهی کار به چه صورت است.
کلیت کار به این صورت است که وضعیت ورودی با استفاده از یک کلید بررسی میشود و هر زمان که کلید فشرده شد متغیری در درون برنامه یک واحد افزایش پیدا میکند (این متغیر وقتی به عدد 9 رسید دوباره 0 میشود) و مقدار این متغیر که عددی بین 0 تا 9 است با استفاده از دیکدر، بر روی سون سگمنت نمایش داده خواهد شد.
فکر میکنم که اکثر افرادی که قرار است این مقاله را مطالعه کنند با سون سگمنت و دیکدر آن؛ که البته خیلی هم ساده است آشنا باشند پس لازم به توضیح بیشتر نیست.
ما تا الان در قسمت GPIO، رجیسترهای BSRR و BRR را بررسی کردیم و تنها دو رجیستر دیگر باقی میماند که در این قسمت بررسی میشود. (البته یک سری رجیستر دیگر برای پیکرهبندی اولیه مانند این که در حالت ورودی باشیم یا خروجی، یا اینکه سرعت پین چقدر باشد، نیز وجود دارد که ما به بررسی این رجیسترها نخواهیم پرداخت و این رجیسترها در نرمافزار STM32CubeMX زمانی که به صورت گرافیکی تنظیمات اولیه را انجام میدهیم مقداردهی میشوند)
همانطور که در قسمت قبل گفتیم رجیستر BSRR هم برای 1 کردن و هم برای 0 کردن پینهای خروجی به کار میرود، اما رجیستر BRR تنها برای 0 کردن پینهای خروجی به کار میرود.
دو رجیستری که میخواهیم در این قسمت بررسی کنیم رجیسترهای ODR و IDR خواهند بود. رجیستر ODR برای 0 یا 1 کردن پین خروجی و رجیستر IDR برای خواندن پینهای ورودی به کار میرود.
شاید بپرسید فرق رجیستر ODR با رجیستر BSRR و BRR در چیست؟ یا وقتی رجیستر ODR وجود دارد چه نیازی به رجیسترهای BSRR و BRR است؟
جواب این سوال در داکیومنتهای ST به صراحت ذکر شده است، برای اینکه تنها یک بیت را به صورت مستقیم بخواهیم 0 یا 1 کنیم باید از رجیسترهای BSRR و BRR استفاده کنیم، چون این کار با استفاده از رجیستر ODR به صورت مستقیم امکانپذیر نیست. با هر بار خواندن یا نوشتن رجیستر ODR به کل بیتها با هم دسترسی داریم نه یک بیت تنها.
به همین دلیل است که در قسمت قبلی که میخواستیم فقط یک پین را 0 و 1 کنیم از رجیسترهای BSRR و BRR استفاده کردیم، اما در این قسمت که میخواهیم چندین پین را 0 و 1 کنیم از رجیستر ODR استفاده میکنیم.
پس نتیجه این است که برای تغییر یک بیت بهتر است از رجیسترهای BSRR و BRR استفاده کنیم، اما برای تغییر چندین بیت باهم بهتر است که از رجیستر ODR استفاده بکنیم.
خب بهتر است که به موضوع اصلی این مقاله یعنی تشریح رجیسترهای ODR و IDR برگردیم.
رجیستر ODR یک رجیستر 32 بیتی است که ما فقط به 16 بیت اول آن دسترسی داریم و این 16 بیت هم خواندنی هستند و هم نوشتنی. رجیستر IDR هم یک رجیستر 32 بیتی است که ما فقط به 16 بیت اول آن دسترسی داریم، اما این 16 بیت فقط خواندنی هستند و ما قادر به نوشتن در این بیتها نیستیم.
اجازه بدهید که جزئیات بیشتر را در نرمافزار و هنگام برنامهنویسی توضیح بدهم تا موضوع برایتان ملموستر باشد.
ابتدا وارد نرمافزار STM32CubeMX میشویم تا تنظیمات اولیه پروژه را انجام بدهیم. توجه کنید که تنظیمات قسمت کلاک و پروگرام از این به بعد در اغلب موارد مانند قسمت پنجم تنظیم میشود و در هر مقاله دوباره به تشریح کامل این مورد نخواهیم پرداخت، فقط در صورت لزوم اشاره مختصری به این موضوع خواهیم داشت.
همانند تصویر زیر پینهای PA0 تا PA6 را برای سون سگمنت در نظر میگیریم و آنها را به عنوان خروجی تعریف میکنیم. همینطور پین PB0 را برای کلید و به عنوان ورودی تعریف میکنیم.
همانطور که گفتیم ما قصد داریم با هر بار فشردن کلید یک واحد به متغیری در درون برنامه اضافه شود و مقدار این متغیر بر روی سون سگمنت نشان داده شود. برای این کار از یک شرط استفاده خواهیم کرد و خود این شرط در دو حالت میتواند نوشته شود.
حالت اول این است که پین ورودی ابتدا 1 منطقی باشد و با فشردن کلید 0 منطقی شود. در این حالت ما باید با استفاده از شرط، 0 شدن پین را بررسی کنیم.
و حالت دوم این است که پین ورودی ابتدا 0 منطقی باشد و با فشردن کلید 1 منطقی شود. در این حالت ما باید با استفاده از شرط، 1 شدن پین را بررسی کنیم.
هر دو حالت بالا عملکرد مشابهی دارند، فقط با دو منطق متفاوت.
در حالت اول معمولا یک مدار بسیار ساده به نام مدار Pull-Up و در حالت دوم مدار ساده دیگری به نام مدار Pull-Down را در کنار میکروکنترلر قرار میدهند تا قبل از زدن کلید، پین در یکی از دو منطق 0 یا 1 باشد و بعد از زدن کلید منطق عوض شود.
برای درک بهتر پاراگراف بالا به مدار زیر توجه کنید:
اما خبر خوب اینکه معمولا این مدارات به صورت پیشفرض، از قبل در میکروکنترلرها قرار داده شده است و نیازی به مدار خارجی نیست. و خبر خوبتر اینکه میکروکنترلرهای ARM بر خلاف میکروکنترلرهای AVR که فقط مدار Pull-Up را دارند، هم مدار Pull-Up و هم مدار Pull-Down را به صورت داخلی دارند.
در اینجا ما قصد داریم که از حالت اول استفاده بکنیم، یعنی پین ورودی ابتدا 1 منطقی باشد و با فشردن کلید 0 منطقی بشود. پس باید از مدار Pull-Up استفاده بکنیم. برای این کار نیاز است که در نرمافزار STM32CubeMX مشخص کنیم که پین PB0 در حالت Pull-Up داخلی قرار دارد.
برای اینکه پین به صورت داخلی Pull-Up شود، باید در نرمافزار تنظیمات زیر را انجام داد:
اکنون از پروژه خروجی میگیریم و برای نوشتن کد به نرمافزار Keil میرویم.
در ابتدای برنامه یک آرایه به طول 10، برای 10 عدد مختلفی که باید بر روی سون سگمنت نمایش داده شود و یک متغیر برای شمارنده در نظر خواهیم گرفت:
uint8_t SevenSegNumber[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; uint8_t i =0;
آرایهای که برای سون سگمنت در نظر گرفتیم، مطابق تصویر زیر که همان دیکدر BCD به سون سگمنت است، نوشته شده است:
پس از تعریف کردن متغیرها برای اینکه سون سگمنت به صورت مستمر شروع به شمارش کند، باید کد زیر درون حلقهی while نوشته شود:
LL_GPIO_WriteOutputPort(GPIOA, SevenSegNumber[i]); if (((LL_GPIO_ReadInputPort(GPIOB)) & (1<<0)) == 0) { i++; LL_mDelay(200); if(i > 9) i = 0; }
در ادامه، قطعه کد بالا را با جزئیات کامل مفصلا شرح خواهیم داد.
در اولین خط، از تابع LL_GPIO_WriteOutputPort استفاده کردیم و در ورودیهای آن به ترتیب از GPIOA و SevenSegNumber[i] استفاده کردیم. اجازه بدهید به تعریف تابع برویم تا ببینیم که چه اتفاقی بر روی این دو ورودی خواهد افتاد.
به تعریف تابع توجه کنید:
__STATIC_INLINE void LL_GPIO_WriteOutputPort(GPIO_TypeDef *GPIOx, uint32_t PortValue) { WRITE_REG(GPIOx->ODR, PortValue); }
همانطور که از تعریف تابع مشخص است، این تابع ورودی دوم، که همان مقدار پورت است را در ورودی اول خود، که یکی از پورتهای GPIO است، قرار میدهد.
پس عملکرد این تابع به این صورت است که مقدار پورت و اسم پورت را از کاربر دریافت میکند و سپس مقدار را بر روی پورت قرار میدهد.
چون در شروع برنامه مقدار متغیر i صفر است، پس اولین عضو از آرایه SevenSegNumber که معادل همان عدد صفر است بر روی سون سگمنت نمایش داده میشود.
اکنون ما باید کنترل کنیم که در هر لحظه از زمان چه مقداری بر روی پورت قرار بگیرد. میتوانستیم این کار را با یک زمانبندی و توالی خاص انجام بدهیم که پس از پروگرام کردن برنامه، سون سگمنت شروع به شمارش کند، اما همانطور که در ابتدای مقاله ذکر کردیم قصد داریم که با GPIO در حالت ورودی آشنا بشویم، پس این کار را با استفاده از فشردن یک کلید انجام خواهیم داد.
فشردن کلید را با استفاده از ساختار شرطی if بررسی میکنیم. برای اینکه بدانیم دقیقا باید چه عبارتی را درون شرط بنویسیم، لازم است که با رجیستر IDR آشنا باشیم و عملکرد آن را وقتی پین در حالت ورودی است بررسی کنیم.
اگر پین در حالت ورودی باشد، هر مقدار منطقی که خارج از میکروکنترلر بر روی پین اعمال شود، در بیت متناظر با همان پین در رجیستر IDR ذخیره میشود. مثلا اگر ما پین PB8 را به سطح ولتاژ 3.3 ولت یا همان 1 منطقی متصل کنیم، هشتمین بیت از رجیستر IDR به صورت خودکار 1 میشود.
چون ما کلید را به پین PB0 وصل کردیم تنها کاری که باید بکنیم این است که بیت صفرم رجیستر IDR از GPIOB را بخوانیم و هر موقع این مقدار، 0 منطقی شد، شرط برقرار شود و عملیات مورد نظرمان را انجام بدهیم.
اما به صورت مستقیم خواندن بیت صفرم یا هر بیت دیگر از رجیستر IDR امکانپذیر نیست، چرا؟
چون که طبق مستندات ST ما نمیتوانیم تنها یک بیت از این رجیستر را بخوانیم و با هر بار خواندن این رجیستر باید 16 بیت آن باهم خوانده شود.
برای خواندن رجیستر IDR از تابع LL_GPIO_ReadInputPort استفاده میکنیم و اگر به تعریف تابع LL_GPIO_ReadInputPort نیز توجه کنید متوجه خواهید شد که کل بیتهای رجیستر IDR باهم خوانده میشوند:
__STATIC_INLINE uint32_t LL_GPIO_ReadInputPort(GPIO_TypeDef *GPIOx) { return (READ_REG(GPIOx->IDR)); }
پس با این تفاسیر راهحل چیست؟
راهحل این است که ابتدا کل 16 بیت را باهم بخوانیم و سپس مقدار بیت موردنظر را از آن جدا کنیم.
قبل از اینکه کدی که برای خواندن یک بیت نوشتیم، را بررسی کنیم، به این نکته توجه کنید که در مستندات ST گفته شده است که مقدار پیشفرض بیتهای رجیستر IDR معلوم نیست و میتواند هر مقداری باشد (Reset value: 0x0000 XXXX)، مگر اینکه پین متناظر با آن بیت مشخصا به یک سطح ولتاژ متصل باشد.
خب ما تنها پین صفرم را Pull-Up کردیم، پس تنها بیت صفرم رجیستر IDR مقدارش مشخص است و سایر بیتها میتوانند هر مقداری داشته باشند.
بیت صفرم چون Pull-Up شده است، در حالت عادی مقدارش 1 منطقی است. و زمانی که کلید فشرده شود این بیت مقدارش 0 منطقی خواهد شد.
با توجه به توضیحات بالا؛ ما در شرط if، از عبارت ((LL_GPIO_ReadInputPort(GPIOB)) & (1<<0)) استفاده کردیم، یعنی همهی بیتها به جز بیت صفرم را با 0 منطقی AND کردیم تا بیتهایی که مقدارشان مشخص نبود، همگی صفر شوند. و بیت صفرم را با 1 منطقی AND کردیم تا اگر مقدار بیت 0 بود همان 0 و اگر 1 بود همان 1 را داشته باشیم.
پس مقدار درون شرط if تا زمانی که کلید فشرده نشود “0x0001” است، به مجرد اینکه کلید فشرده شود این مقدار به “0x0000” تغییر میکند و ما با استفاده از شرط if تعیین کردیم که اگر عبارت ((LL_GPIO_ReadInputPort(GPIOB)) & (1<<0)) برابر با “0x0000” شد، دستورات درون شرط if اجرا شود.
ما با استفاده از تکنیکهای سادهای موفق شدیم که بر محدودیتهایی که ذکر شد غلبه کنیم و شرط موردنظرمان را توصیف کنیم. حال وقت آن است که دستورات درون شرط را توضیح بدهیم.
درون شرط if گفتیم ابتدا متغیر یک واحد افزایش پیدا کند و پس از 200 میلی ثانیه تاخیر، اگر متغیر از عدد 9 بزرگتر بود به صفر برگردانده شود و در نهایت از شرط خارج شود. پس از خارج شدن از شرط، برنامه به ابتدای حلقه برمیگردد و مقدار پورت را با توجه به مقدار جدید i آپدیت میکند.
در این برنامه به صورت مستمر چک میشود که آیا کلید فشرده شده است یا خیر و اگر کلید فشرده شد، به متغیر یک واحد اضافه شود و بر روی سون سگمنت نمایش داده شود.
شاید برایتان سوال پیش آمده باشد که چرا از تاخیر 200 میلی ثانیه استفاده کردیم! به این دلیل که وقتی ما کلید را فشار میدهیم، با توجه به سرعت بالای برنامه متغیر سریعا افزایش مییابد و دوباره به ابتدای شرط برمیگردد و چون ما هنوز دستمان بر روی کلید است دوباره متغیر افزایش مییابد. در واقع وقتی ما دستمان را بر روی دکمه گذاشتیم تا بخواهیم آن را برداریم متغیر n بار افزایش پیدا میکند. به همین علت است که ما در برنامه از تاخیر استفاده کردیم.
البته استفاده از تاخیر در برنامه راهی اصولی نیست، اما چون هنوز در ابتدای مسیر هستیم و با سایر پریفرالها آشنا نیستیم، استفاده از تاخیر در برنامه مانعی ندارد.
در قسمت هفتم در رابطه با Interrupt یا همان وقفه صحبت خواهیم کرد.
منبع:سیسوگ