دسترسی به رجیسترهای پریفرال در زبان C - بخش ۲: ماکروها

در بخش ۱ این مقاله گفتیم که برای دسترسی به هر آدرسی از فضای حافظه، از جمله آدرس‌های مربوط به رجیسترهای پریفرال، می‌تونیم از متغیرهای اشاره‌گر استفاده کنیم. کافیه که آدرس رجیستر مورد نظر خودمون رو داخل متغیر اشاره‌گر بریزیم و بهش دسترسی پیدا کنیم. در اون مقاله، از همین روش استفاده کردیم و یه مقداری رو ریختیم روی رجیستر MODER از GPIOA داخل میکروکنترلر STM32F030K6T6. اما همون‌طوری که در پایان اون مقاله گفتیم، استفاده از «متغیر» برای یه همچین کاری معایب خودش رو داره؛ مثلاً ممکنه که فضای ارزشمند RAM رو هدر بده، ناخواسته به یه متغیر global تبدیل بشه و مشکلات دیگه‌ای برامون به‌وجود بیاره، یا شاید فرصت بهینه‌سازی رو از کامپایلر بگیره. خلاصه این که استفاده از «متغیر» اشاره‌گر برای دسترسی به رجیسترهای پریفرال، راه‌حل مطلوبی نیست. ای کاش که متغیر اشاره‌گر حذف می‌شد و یه‌جوری مستقیم به آدرس‌های حافظه دسترسی پیدا می‌کردیم… ای کاش…

دسترسی مستقیم به یک آدرس

راستش حقیقت امر اینه که ما برای دسترسی به یه آدرس از حافظه، اصلاً نیازی به تعریف یه «متغیر» اشاره‌گر نداریم! این امکان وجود داره که ما مستقیماً و در یک کلام (!) به آدرس موردنظر خودمون دسترسی پیدا کنیم. در مقاله‌ی قبلی، متغیر اشاره‌گر gpioa_moder رو تعریف کردیم، مقدار آدرس رجیستر متناظر رو درش قرار دادیم و از این طریق، به رجیستر MODER از GPIOA دسترسی پیدا کردیم. گفتیم که برای تضمین کارکرد صحیح کدمون هم، رجیستر رو به‌شکل volatile تعریف می‌کنیم. کد ما با استفاده از متغیر اشاره‌گر، این شکلی بود:

volatile uint32_t *gpioa_moder = (volatile uint32_t *)(0x48000000UL);
*gpioa_moder = 1;

حالا ببینید، می‌تونیم تعریف متغیر اشاره‌گر gpioa_moder رو کلاً حذف کنیم و به‌جاش، آدرس موجود در خط اول رو داخل خط دوم جایگذاری کنیم. پس کاری که انجام می‌دیم اینه که (volatile uint32_t *)(0x48000000UL) رو جایگزین gpioa_moder در خط دوم می‌کنیم و خط اول رو حذف می‌کنیم. برای اطمینان از اولویت عملگر یگانی * در خط دوم و تمیزتر شدن کد، یه‌جفت پرانتز هم دور (volatile uint32_t *)(0x48000000UL) قرار می‌دیم. تک‌خط زیر به‌دست میاد:

*((volatile uint32_t *)(0x48000000UL)) = 1;

اینجا ما دیگه اومده‌ایم در یک کلام، به کامپایلر گفته‌ایم که به عدد ثابت 0x48000000 به‌شکل یه آدرس نگاه کن و بعد مقدار 1 رو بریز توی اون آدرس! توجه می‌کنید که خط بالا، به‌جز اون پرانتزهای اضافه، دقیقاً معادل خط دوم کد قبلی هست. پس کار اون دو خط قبلی رو اومده‌ایم داخل یک خط انجام داده‌ایم. حالا دیگه هیچ متغیر اشاره‌گری وجود نداره که بخواد یه‌وقتی فضایی رو از RAM ارزشمند ما اشغال کنه. خیلی هم خوب شد.

«خیلی هم خوب شد»؟ واقعاً؟

متغیر رو حذف کردیم، اما حالا یه مشکل خیلی، خیلی، خیلی، شدیداً اساسی ایجاد شد! یه بار دیگه به کد نگاه کنید:

*((volatile uint32_t *)(0x48000000UL)) = 1;

حقیقتاً اگر یه جایی این کد رو ببینید، پیش خودتون چه فکری می‌کنید؟ درسته، متوجه هستیم که این کد، داره در آدرس 0x48000000 حافظه، مقدار عددی 1 رو می‌ریزه، ولی خب آخه اون آدرس عددی، چه معنی‌ای برای ما داره؟ باید به دیتاشیت و داکیومنت‌های رسمی مراجعه کنیم تا متوجه بشیم که: «آها! این همون آدرس مربوط به رجیستر MODER از GPIOA هست!» درحقیقت، ما با یه حرکت، اومدیم خوانایی کد رو به‌شدت کاهش دادیم. علاوه بر این، فرض کنید که صد جای دیگه داخل کد خودمون، باز هم قراره که تنظیمات ورودی/خروجی پین‌های پورت A رو تغییر بدیم یا به سایر رجیسترهای سیستم دسترسی پیدا کنیم! حالا باید این آدرس‌های عجیب‌وغریب و بی‌معنی رو اون صد جای دیگه هم کپی کنیم. داریم امکان بروز خطا رو در اثر این کپی/پِیست‌ها افزایش می‌دیم. قصه رو طولانی نکنیم: این راه‌حل، به‌شکل کنونی، به هیچ دردی نمی‌خوره. لازمه که تغییری توی این راه‌حل بدیم تا قابل‌استفاده بشه.

الآن مشکل دقیقاً چیه؟ مشکل، این عبارت عجیب‌وغریبه: *((volatile uint32_t *)(0x48000000UL)). اگر می‌شد که به‌جای نوشتن و تکرار این عبارت بی‌معنی:

*((volatile uint32_t *)(0x48000000UL)) = 1;

بتونیم یه چیز خیلی قشنگ و معناداری رو مثل:

GPIOA_MODER = 1;

بنویسیم، دیگه راضی می‌شدیم. داخل خط بالا، دیگه خبری از اون آدرس عجیب نیست، بلکه یه اسم درست‌وحسابی داریم که منظورمون رو به‌خوبی می‌رسونه. پس الآن مشکل ما این‌طوری بیان می‌شه: چطوری می‌شه که بتونیم GPIOA_MODER رو به‌جای *((volatile uint32_t *)(0x48000000UL)) بنویسیم؟ یعنی ما دلمون می‌خواد که هرجا GPIOA_MODER رو نوشتیم، بگیم که منظورمون *((volatile uint32_t *)(0x48000000UL)) بوده! برای حل این نوع از مشکلات، استفاده از ماکروها راه‌گشاست.

تعریف ماکرو برای دسترسی به رجیسترهای پریفرال

کافیه که یه ماکرو تعریف کنیم تا مقدار *((volatile uint32_t *)(0x48000000UL)) رو جایگزین GPIOA_MODER کنه. کد زیر رو ببینید:

#define GPIOA_MODER (*((volatile uint32_t *)(0x48000000UL)))

داخل تعریف بالا، محض احتیاط یه‌جفت پرانتز هم دور *((volatile uint32_t *)(0x48000000UL)) قرار داده‌ایم تا مطمئن باشیم که رفتار ماکروی ما، همیشه درسته. حالا بعد از تعریف این ماکرو، هر موقع داخل کد خودمون خط زیر رو بنویسیم:

GPIOA_MODER = 1;

Preprocessor میاد ماکروی ما رو پردازش می‌کنه و به‌جای خط بالا، خط زیر رو قرار می‌ده:

(*((volatile uint32_t *)(0x48000000UL))) = 1;

که به‌جز اون پرانتزهای اضافه، دقیقاً معادل همون چیزی هست که قبلاً داشتیم. با این کار، هم از شر تعریف متغیر خلاص شدیم و هم خوانایی کد خودمون رو قربانی نکردیم. چون آدرس رو هم فقط یک بار موقع تعریف ماکرو نوشتیم، احتمال خطا رو هم کاهش دادیم.

رجیسترهای پریفرال و فایل‌های هِدِر (header)

معمولاً توی حالت کلی، تعریف رجیسترهای پریفرال داخل فایل‌های هِدِر قرار می‌گیره و بعد از اون، درون فایل‌های سورس‌کد ازش استفاده می‌شه؛ مثلاً می‌شه که تعریف رجیستر MODER از GPIOA رو داخل فایل gpio.h به‌شکل زیر انجام داد:

// file: gpio.h

#include <stdint.h>

#define GPIOA_MODER (*((volatile uint32_t *)(0x48000000UL)))

بعد از اون، می‌تونیم که فایل gpio.h رو داخل فایل‌های سورس کد خودمون #include کنیم و از رجیستر(های) مدنظرمون استفاده کنیم:

// file: sample.c

#include "gpio.h"

void sample_function(void)
{
	// ...
	
	GPIOA_MODER = 1;
	
	// ...
}

جمع‌بندی

در این مقاله دیدیم که می‌شه متغیر اشاره‌گر رو حذف کرد و مستقیماً به یه آدرس از حافظه دسترسی داشت؛ اما از اون طرف، حذف متغیر اشاره‌گر از سورس‌کد، خوانایی کد رو به‌شدت کاهش داد، کد ما رو مستعد خطا کرد و نگهداری اون رو هم مشکل کرد. به همین خاطر، اومدیم رجیسترهای خودمون رو با استفاده از ماکروها تعریف کردیم: هم از تعریف متغیرهای اشاره‌گر نجات پیدا کردیم و هم خوانایی کد و نگهداری از اون رو بهبود دادیم. در نهایت، دیدیم که می‌شه این تعریف‌ها رو درون فایل‌های هدر قرار بدیم و داخل فایل‌ها و برنامه‌های مختلف، ازشون استفاده کنیم. روشی که بیان کردیم، خیلی رایجه و به‌خصوص اگر با AVR کار کرده باشید، حتماً با اون آشنا هستید.

ما تا الآن فقط مقدمات دسترسی به آدرس‌های حافظه و رجیسترهای پریفرال رو بررسی می‌کردیم، به همین دلیل هم فقط با یک رجیستر نمونه کار داشتیم؛ اما در سیستم‌های واقعی، معمولاً تعداد زیادی رجیستر و پریفرال وجود داره و یه راه برای تعریف اون رجیسترها، اینه که هرکدوم رو دونه‌به‌دونه به همین شکلی که توضیح دادیم، نشون بدیم. در بخش ۳ این مقاله، با افزایش تعداد رجیسترهای خودمون، نگاه واقع‌گرایانه‌ای به مسأله‌ی تعریف رجیسترهای پریفرال می‌اندازیم، مزایا و معایب احتمالی این روش رو هم بررسی می‌کنیم، یه‌کمی با Preprocessor بازی می‌کنیم تا تعریف‌ها رو بهبود بدیم، و نهایتاً احتمال وجود یه راه‌حل دیگه رو هم مطرح می‌کنیم. پس إن‌شاءالله بحث رو در بخش ۳ ادامه می‌دیم.